From 7855ee809f1180d8a9cab66cc33fc4acf92671f7 Mon Sep 17 00:00:00 2001 From: Christophe Nasarre Date: Wed, 10 Jun 2026 14:55:36 +0200 Subject: [PATCH 1/3] Support Windows PDB format --- .../ChecksumValidator.cs | 60 +++++++++++++++++++ .../SymbolStores/HttpSymbolStore.cs | 16 ++++- src/Tools/dotnet-symbol/Program.cs | 19 ++++-- 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.SymbolStore/ChecksumValidator.cs b/src/Microsoft.SymbolStore/ChecksumValidator.cs index f19ce2da80..f04fe889be 100644 --- a/src/Microsoft.SymbolStore/ChecksumValidator.cs +++ b/src/Microsoft.SymbolStore/ChecksumValidator.cs @@ -7,6 +7,8 @@ using System.Linq; using System.Security.Cryptography; using System.Text; +using Microsoft.FileFormats; +using Microsoft.FileFormats.PDB; using Microsoft.FileFormats.PE; namespace Microsoft.SymbolStore @@ -17,9 +19,67 @@ internal sealed class ChecksumValidator private const uint pdbIdSize = 20; internal static void Validate(ITracer tracer, Stream pdbStream, IEnumerable pdbChecksums) + { + // A portable PDB checksum is computed over the metadata image with the embedded + // PDB id zeroed out, so it can be fully recomputed and validated here. + // + // Windows PDBs (MSF container) and PDZ files (MSFZ container) use a completely + // different on-disk format that this code cannot recompute. The symbol server has + // already matched the returned content against the SymbolChecksum request header, + // so for those we only confirm the download is a structurally valid PDB and accept it. + // + // This happens for ngen or ReadyToRun images. + if (IsPortablePdb(pdbStream)) + { + ValidatePortablePdb(tracer, pdbStream, pdbChecksums); + } + else + { + ValidateWindowsPdb(tracer, pdbStream); + } + } + + /// + /// Returns true if the stream is a portable PDB (an ECMA-335 metadata image, which + /// starts with the "BSJB" signature). + /// + private static bool IsPortablePdb(Stream pdbStream) + { + pdbStream.Position = 0; + byte[] signature = new byte[4]; + int read = pdbStream.Read(signature, 0, signature.Length); + pdbStream.Position = 0; + return read == signature.Length && + signature[0] == 0x42 && // 'B' + signature[1] == 0x53 && // 'S' + signature[2] == 0x4A && // 'J' + signature[3] == 0x42; // 'B' + } + + /// + /// Structurally validates a Windows PDB (MSF) or PDZ (MSFZ) download and accepts it. + /// The cryptographic content match was already enforced by the symbol server through + /// the SymbolChecksum request header, so a byte-level re-validation is not performed. + /// + private static void ValidateWindowsPdb(ITracer tracer, Stream pdbStream) + { + pdbStream.Position = 0; + using (PDBFile pdbFile = new(new StreamAddressSpace(pdbStream))) + { + if (!pdbFile.IsValid()) + { + throw new InvalidChecksumException("The downloaded file is neither a portable PDB nor a valid Windows PDB (MSF/MSFZ) container"); + } + tracer.Information($"Accepting Windows PDB ({pdbFile.ContainerKind}); content was validated by the symbol server via the SymbolChecksum header"); + } + pdbStream.Position = 0; + } + + private static void ValidatePortablePdb(ITracer tracer, Stream pdbStream, IEnumerable pdbChecksums) { uint offset = 0; + pdbStream.Position = 0; byte[] bytes = new byte[pdbStream.Length]; byte[] pdbId = new byte[pdbIdSize]; if (pdbStream.Read(bytes, offset: 0, count: bytes.Length) != bytes.Length) diff --git a/src/Microsoft.SymbolStore/SymbolStores/HttpSymbolStore.cs b/src/Microsoft.SymbolStore/SymbolStores/HttpSymbolStore.cs index 5887261547..08bb938443 100644 --- a/src/Microsoft.SymbolStore/SymbolStores/HttpSymbolStore.cs +++ b/src/Microsoft.SymbolStore/SymbolStores/HttpSymbolStore.cs @@ -141,7 +141,21 @@ protected override async Task GetFileInner(SymbolStoreKey key, { if (needsChecksumMatch) { - ChecksumValidator.Validate(Tracer, stream, key.PdbChecksums); + // Portable PDBs are validated against their checksum here. Windows PDBs (MSF) + // and PDZ (MSFZ) are structurally validated and accepted (the server already + // matched them via the SymbolChecksum header). A truncated/corrupt portable + // PDB can still throw an EndOfStreamException while parsing its metadata; in + // that case skip this candidate so any other bound PDB can be tried (e.g. ngen + // or ReadyToRun images can have multiple bound PDBs). + try + { + ChecksumValidator.Validate(Tracer, stream, key.PdbChecksums); + } + catch (EndOfStreamException) + { + Tracer.Verbose($" {key.FullPathName} is not a valid PDB file."); + return null; + } } return new SymbolStoreFile(stream, uri.ToString()); } diff --git a/src/Tools/dotnet-symbol/Program.cs b/src/Tools/dotnet-symbol/Program.cs index dc7d330eb8..e788dbf409 100644 --- a/src/Tools/dotnet-symbol/Program.cs +++ b/src/Tools/dotnet-symbol/Program.cs @@ -243,18 +243,25 @@ internal async Task DownloadFiles() { using (Microsoft.SymbolStore.SymbolStores.SymbolStore symbolStore = BuildSymbolStore()) { - foreach (SymbolStoreKeyWrapper wrapper in GetKeys().Distinct()) + if (symbolStore != null) { - SymbolStoreKey key = wrapper.Key; - if (symbolStore != null) + foreach (SymbolStoreKeyWrapper wrapper in GetKeys().Distinct()) { - using (SymbolStoreFile symbolFile = await symbolStore.GetFile(key, CancellationToken.None).ConfigureAwait(false)) + SymbolStoreKey key = wrapper.Key; + try { - if (symbolFile != null) + using (SymbolStoreFile symbolFile = await symbolStore.GetFile(key, CancellationToken.None).ConfigureAwait(false)) { - await WriteFile(symbolFile, wrapper).ConfigureAwait(false); + if (symbolFile != null) + { + await WriteFile(symbolFile, wrapper).ConfigureAwait(false); + } } } + catch (InvalidChecksumException) + { + Tracer.Verbose($" Skipped invalid {key.FullPathName} file"); + } } } } From e69496043bcf831291083def1de2c66f493aaf1a Mon Sep 17 00:00:00 2001 From: Christophe Nasarre Date: Thu, 11 Jun 2026 08:51:58 +0200 Subject: [PATCH 2/3] - Get review into account - Handle possible PDBFile exception --- .../ChecksumValidator.cs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.SymbolStore/ChecksumValidator.cs b/src/Microsoft.SymbolStore/ChecksumValidator.cs index f04fe889be..925743b51b 100644 --- a/src/Microsoft.SymbolStore/ChecksumValidator.cs +++ b/src/Microsoft.SymbolStore/ChecksumValidator.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Security.Cryptography; using System.Text; +using System.Web; using Microsoft.FileFormats; using Microsoft.FileFormats.PDB; using Microsoft.FileFormats.PE; @@ -24,9 +25,8 @@ internal static void Validate(ITracer tracer, Stream pdbStream, IEnumerable /// Structurally validates a Windows PDB (MSF) or PDZ (MSFZ) download and accepts it. - /// The cryptographic content match was already enforced by the symbol server through - /// the SymbolChecksum request header, so a byte-level re-validation is not performed. + /// A byte-level re-validation is not performed. /// private static void ValidateWindowsPdb(ITracer tracer, Stream pdbStream) { + const string checksumExceptionMessage = "The downloaded file is neither a portable PDB nor a valid Windows PDB (MSF/MSFZ) container"; pdbStream.Position = 0; - using (PDBFile pdbFile = new(new StreamAddressSpace(pdbStream))) + try { - if (!pdbFile.IsValid()) + using (PDBFile pdbFile = new(new StreamAddressSpace(pdbStream))) { - throw new InvalidChecksumException("The downloaded file is neither a portable PDB nor a valid Windows PDB (MSF/MSFZ) container"); + if (!pdbFile.IsValid()) + { + throw new InvalidChecksumException(checksumExceptionMessage); + } + tracer.Information($"Accepting Windows PDB ({pdbFile.ContainerKind}); No checksum validation is available for this file format"); } - tracer.Information($"Accepting Windows PDB ({pdbFile.ContainerKind}); content was validated by the symbol server via the SymbolChecksum header"); + } + catch (Exception) // The PDBFile constructor or IsValid method can throw an Exception exception + { + // Note: the InvalidChecksumException constructor does not accept an inner exception + throw new InvalidChecksumException(checksumExceptionMessage); } pdbStream.Position = 0; } From d31d13fc7c5c5e5b2e9120baf8c49724dc498e90 Mon Sep 17 00:00:00 2001 From: Noah Falk Date: Fri, 12 Jun 2026 03:33:23 -0700 Subject: [PATCH 3/3] Refactor comments for clarity in HttpSymbolStore.cs --- .../SymbolStores/HttpSymbolStore.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.SymbolStore/SymbolStores/HttpSymbolStore.cs b/src/Microsoft.SymbolStore/SymbolStores/HttpSymbolStore.cs index 08bb938443..ae6eec3f91 100644 --- a/src/Microsoft.SymbolStore/SymbolStores/HttpSymbolStore.cs +++ b/src/Microsoft.SymbolStore/SymbolStores/HttpSymbolStore.cs @@ -142,11 +142,10 @@ protected override async Task GetFileInner(SymbolStoreKey key, if (needsChecksumMatch) { // Portable PDBs are validated against their checksum here. Windows PDBs (MSF) - // and PDZ (MSFZ) are structurally validated and accepted (the server already - // matched them via the SymbolChecksum header). A truncated/corrupt portable - // PDB can still throw an EndOfStreamException while parsing its metadata; in - // that case skip this candidate so any other bound PDB can be tried (e.g. ngen - // or ReadyToRun images can have multiple bound PDBs). + // and PDZ (MSFZ) are structurally validated and accepted. A truncated/corrupt + // portable PDB can still throw an EndOfStreamException while parsing its + // metadata; in that case skip this candidate so any other bound PDB can be + // tried (e.g. ngen or ReadyToRun images can have multiple bound PDBs). try { ChecksumValidator.Validate(Tracer, stream, key.PdbChecksums);