From f7b528597fa09f900a1070fb637862000e8ea7e5 Mon Sep 17 00:00:00 2001 From: Bobby Galli Date: Wed, 22 Jul 2026 16:46:22 -0400 Subject: [PATCH] feat: extract Portable PDB (BSJB) debug identity; multi-target net10.0 PDBFile now transparently reads a Portable PDB's debug identity (the {GUID}{age} symbol-server key) alongside MSF/CodeView PDBs and PE images, so a store keyed by PdbLibrary indexes .NET Core / modern-SDK app PDBs under the same key a dump's CodeView record names. The identity is the 16-byte GUID prefix of the metadata "PDB id" with age 1 (Roslyn's fixed RSDS age for portable PDBs). Multi-targeted netstandard2.0;net10.0: portable identity uses in-box System.Reflection.Metadata under #if NET (net10.0), so no package reference is added and the netstandard2.0 flavor stays pure BCL (MSF + PE only). Adds a golden test against a real portable PDB. Co-Authored-By: Claude Opus 4.8 (1M context) --- PdbLibrary/PdbLibrary.cs | 61 ++++++++++++++++++ PdbLibrary/PdbLibrary.csproj | 24 ++++--- UnitTestPdbLibrary/PdbLibraryTests.cs | 13 ++++ .../testdata/MyDotNetCrasher.pdb | Bin 0 -> 13360 bytes 4 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 UnitTestPdbLibrary/testdata/MyDotNetCrasher.pdb diff --git a/PdbLibrary/PdbLibrary.cs b/PdbLibrary/PdbLibrary.cs index 6ce02fb..3c0252a 100644 --- a/PdbLibrary/PdbLibrary.cs +++ b/PdbLibrary/PdbLibrary.cs @@ -318,6 +318,18 @@ public PDBFile(FileInfo pdbFileInfo) byte[] bytes = new byte[PdbSignature.signature.Length]; int n = fp.Read(bytes, 0, PdbSignature.signature.Length); + // A Portable PDB (ECMA-335, "BSJB" magic — the format modern .NET Core / + // SDK projects emit, including a .NET app's own assembly PDB) is CLI + // metadata, not an MSF container. Its debug identity is read from the + // metadata "PDB id" instead of the MSF streams; the resulting {GUID}{age} + // key matches the one a dump's CodeView record names, so it indexes into + // the symsrv store the same way an MSF PDB's key does. + if (n >= 4 && bytes[0] == (byte)'B' && bytes[1] == (byte)'S' && bytes[2] == (byte)'J' && bytes[3] == (byte)'B') + { + this.GUID = ReadPortableIdentity(pdbFileInfo); + return; + } + if (n != PdbSignature.signature.Length) { throw new Exception("Invalid PDB signature"); @@ -373,6 +385,55 @@ public PDBFile(FileInfo pdbFileInfo) } } } + +#if NET + // Portable PDB (BSJB) debug identity: the 16-byte GUID prefix of the metadata + // "PDB id" (System.Reflection.Metadata's DebugMetadataHeader.Id), with age fixed + // at 1 — the value Roslyn writes into the PE's CodeView RSDS record for portable + // PDBs — so GUID.Value() yields the same {GUID}{age} symbol-server key a dump's + // PdbStoreKey computes. The GUID bytes are laid out exactly as a System.Guid + // constructed from the same 16 bytes formats under "N", matching the store key. + // + // In-box on the net10.0 target; the netstandard2.0 flavor has no + // System.Reflection.Metadata, so portable identity is a .NET-target capability + // (the MSF-PDB / PE key extraction stays pure BCL and cross-platform there). + private static GUID ReadPortableIdentity(FileInfo pdbFileInfo) + { + using (FileStream stream = new FileStream(pdbFileInfo.FullName, FileMode.Open, FileAccess.Read)) + using (var provider = System.Reflection.Metadata.MetadataReaderProvider.FromPortablePdbStream(stream)) + { + var reader = provider.GetMetadataReader(); + var header = reader.DebugMetadataHeader; + if (header == null) + { + throw new Exception("Not a portable PDB: file has no debug metadata header"); + } + + var idBlob = header.Id; // 20-byte PDB id: 16-byte GUID + 4-byte stamp + if (idBlob.Length < 16) + { + throw new Exception($"Portable PDB id too short: {idBlob.Length} bytes"); + } + + byte[] id = new byte[idBlob.Length]; + idBlob.CopyTo(id); + + uint data1 = BitConverter.ToUInt32(id, 0); + ushort data2 = BitConverter.ToUInt16(id, 4); + ushort data3 = BitConverter.ToUInt16(id, 6); + byte[] data4 = new byte[8]; + Array.Copy(id, 8, data4, 0, 8); + + const uint PortablePdbAge = 1; // Roslyn's fixed RSDS age for portable PDBs + return new GUID(data1, data2, data3, data4, PortablePdbAge); + } + } +#else + private static GUID ReadPortableIdentity(FileInfo pdbFileInfo) + => throw new NotSupportedException( + "Portable PDB identity requires the .NET (net10.0) target of PdbLibrary; " + + "the netstandard2.0 flavor reads only MSF/CodeView PDBs."); +#endif } public class PEFile diff --git a/PdbLibrary/PdbLibrary.csproj b/PdbLibrary/PdbLibrary.csproj index cb65f9d..2a433a1 100644 --- a/PdbLibrary/PdbLibrary.csproj +++ b/PdbLibrary/PdbLibrary.csproj @@ -1,27 +1,33 @@ - netstandard2.0 + netstandard2.0;net10.0 PdbLibrary PdbLibrary latest disable false - + PdbLibrary - 1.1.0 + 1.2.0 BugSplat BugSplat PdbLibrary - Extract GUIDs from Windows Executables and Symbol Files + Extract debug identity (GUID+age) from Windows PE images, MSF/CodeView PDBs, and Portable (ECMA-335) PDBs Copyright BugSplat, All Rights Reserved diff --git a/UnitTestPdbLibrary/PdbLibraryTests.cs b/UnitTestPdbLibrary/PdbLibraryTests.cs index 752614a..1dcc348 100644 --- a/UnitTestPdbLibrary/PdbLibraryTests.cs +++ b/UnitTestPdbLibrary/PdbLibraryTests.cs @@ -33,6 +33,19 @@ public void TestPDBFileGuid() Assert.That(guid, Is.EqualTo("0C0E0F8243B54897952E4DB3E538A2361")); } + [Test] + public void TestPortablePdbGuid() + { + // A Portable PDB (BSJB) — the format a .NET Core / modern SDK app's own + // assembly PDB ships in. Its {GUID}{age} key must equal the one a dump's + // CodeView record names, so the backend indexes it into the symsrv store + // under the same key bugsplat-cdb later looks it up by. This fixture's key + // is the directory it lives under in bugsplat-cdb's data/symbols-core store. + PDBFile pdbFile = new PDBFile(new FileInfo(TestData("MyDotNetCrasher.pdb"))); + string guid = pdbFile.GUID.Value(); + Assert.That(guid, Is.EqualTo("0023F679780F4F5D8DE2F203E88AB6721")); + } + [Test] public void TestPEFileGuid() { diff --git a/UnitTestPdbLibrary/testdata/MyDotNetCrasher.pdb b/UnitTestPdbLibrary/testdata/MyDotNetCrasher.pdb new file mode 100644 index 0000000000000000000000000000000000000000..a657ffff4453bcfb6733d5d9032f54bcf51ae6f6 GIT binary patch literal 13360 zcmcJ02UHW=_VAlMK=q^MLC1hHWS z1UvSw2x3RC<=VyGd;QKyW*`FI^gV!GTeGY=809bGMIY_OP$f8u>BT@{;jNn*E5V8t< zj)b2*L%iL=6)`{%3JrXBlgc5pgpWgzE;FX^7u|bn_&8&ik~ek7RRA&Z3^71Dm?2uN z`siQ~s{L*YuAT7DfL1$Z2(Iu2{{S5v27g<17bHci55&V4q=Q!z@QMMXGr+fV;Cdfi z*O5rb9+Cl4Xh1?L3=F_%KPn&tSNK`SHXB^vcSG=A1Ac3S1mI7n3*>5ubiqd~X?8&N z2qQbh7`fBI4#U_xvmvKklUICCls}7&!RmgY8J%qoQ{QRE&wuVbpEciS>2(i5%0Ay~W}bd?WF5kJmDlUK?wzLYgcxu}8HcDa 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