Repository navigation
ilasm: PdbChecksum hashes only the #Pdb stream, and -DET leaves the PDB ID's stamp at 0 #135211
Description
Description
ilasm writes a Portable PDB and a matching debug directory that disagree with PE-COFF.md in two ways.
- The
PdbChecksumentry hashes only the#Pdbstream, not the PDB file. PE-COFF.md says: "If the symbol format is Portable PDB the checksum is calculated by hashing the entire content of the PDB file with the PDB ID set to 0 (20 zeroed bytes)." ilasm's checksum is SHA-256 of the#Pdbstream alone (64 bytes for the file below): as written without-DET, and with the ID zeroed with-DET. So the checksum never verifies against the PDB it was written with. - With
-DET, the PDB ID's stamp stays 0, while the CodeView entry'sTimeDateStampcarries the deterministic stamp.PEReader.TryOpenAssociatedPortablePdbbuilds the expected ID from the CodeView GUID and that entry's stamp (PEReader.csL785), so it refuses the PDB ilasm wrote alongside the DLL.StackTraceSymbolslocates PDBs through that call (L198).
Both came in with #109091 and are unchanged on main (checked at 30f813606ae).
Reproduction
Min.il:
.assembly extern System.Runtime { .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A) .ver 10:0:0:0 }
.assembly Min { }
.module Min.dll
.class public auto ansi beforefieldinit C extends [System.Runtime]System.Object
{
.method public hidebysig static int32 F() cil managed
{
ldc.i4.1
ret
}
}Assemble it twice, into separate directories, where ilasm is the build under test:
mkdir -p nondet det
ilasm -DLL -DEBUG -QUIET -OUTPUT=nondet/Min.dll Min.il
ilasm -DLL -DEBUG -QUIET -DET -OUTPUT=det/Min.dll Min.ilCheck each pair with this file-based app, which needs a .NET 10 or later SDK (dotnet run check.cs -- nondet/Min.dll det/Min.dll):
#:property UseAppHost=false
using System.Numerics;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Security.Cryptography;
foreach (var dll in args)
{
using var pe = new PEReader(File.OpenRead(dll));
bool opened = pe.TryOpenAssociatedPortablePdb(dll, p => File.Exists(p) ? File.OpenRead(p) : null, out var provider, out _);
provider?.Dispose();
var codeView = pe.ReadDebugDirectory().First(e => e.Type == DebugDirectoryEntryType.CodeView);
byte[] pdb = File.ReadAllBytes(Path.ChangeExtension(dll, ".pdb"));
using var pdbProvider = MetadataReaderProvider.FromPortablePdbImage(System.Collections.Immutable.ImmutableArray.Create(pdb));
var header = pdbProvider.GetMetadataReader().DebugMetadataHeader!;
int idAt = header.IdStartOffset;
// #Pdb stream: PDB ID (20), EntryPoint (4), ReferencedTypeSystemTables (8), then one row count per table.
ulong mask = BitConverter.ToUInt64(pdb, idAt + 24);
int streamLength = 32 + 4 * BitOperations.PopCount(mask);
byte[] Zeroed(byte[] b, int at) { var c = (byte[])b.Clone(); Array.Clear(c, at, 20); return c; }
var stream = pdb.AsSpan(idAt, streamLength).ToArray();
Console.WriteLine(dll);
Console.WriteLine($" PDB ID stamp {BitConverter.ToUInt32(pdb, idAt + 16)}, CodeView entry stamp {codeView.Stamp}");
Console.WriteLine($" TryOpenAssociatedPortablePdb: {opened}");
foreach (var e in pe.ReadDebugDirectory().Where(e => e.Type == DebugDirectoryEntryType.PdbChecksum))
{
var sum = pe.ReadPdbChecksumDebugDirectoryData(e).Checksum;
bool Is(byte[] preimage) => sum.SequenceEqual(SHA256.HashData(preimage));
Console.WriteLine($" checksum == SHA256(whole PDB, ID zeroed) [PE-COFF.md]: {Is(Zeroed(pdb, idAt))}");
Console.WriteLine($" checksum == SHA256(#Pdb stream only, as written): {Is(stream)}");
Console.WriteLine($" checksum == SHA256(#Pdb stream only, ID zeroed): {Is(Zeroed(stream, 0))}");
}
}Output with ilasm 10.0.0 (runtime.linux-x64.Microsoft.NETCore.ILAsm), Linux x64. The non-deterministic stamp varies between runs; the -DET values depend only on the size of this input's metadata (#135214):
nondet/Min.dll
PDB ID stamp 1791172104, CodeView entry stamp 1791172104
TryOpenAssociatedPortablePdb: True
checksum == SHA256(whole PDB, ID zeroed) [PE-COFF.md]: False
checksum == SHA256(#Pdb stream only, as written): True
checksum == SHA256(#Pdb stream only, ID zeroed): False
det/Min.dll
PDB ID stamp 0, CodeView entry stamp 2978741164
TryOpenAssociatedPortablePdb: False
checksum == SHA256(whole PDB, ID zeroed) [PE-COFF.md]: False
checksum == SHA256(#Pdb stream only, as written): False
checksum == SHA256(#Pdb stream only, ID zeroed): True
As a control, the same program on a Roslyn-built DLL prints True for TryOpenAssociatedPortablePdb and for the PE-COFF.md rule, and False for both stream-only lines.
Cause
PdbHeap::ComputeSha256Checksumhashesm_data/m_size, which is the#Pdbstream thatPdbHeap::SetDatabuilt, not the serialized PDB.Assembler::CreatePEFilecalls it right afterBuildPdbStream, before the PDB's other streams are serialized.- Under
-DET, that call happens while the ID is all zero (L1483). ThenChangePdbStreamGuidpatches the GUID into the heap throughPdbHeap::SetDataGuid, butSetTimestampupdates onlyPortablePdbWriter::m_pdbStream, although the comment above the two calls says "update the GUID and timestamp".CreateDebugDirectoryreads the stamp from that struct for the CodeView entry (writer.cppL360), and the heap, which is what is saved, keeps 0. IlasmPortablePdbTester.TestPortablePdbDebugDirectorycompares only the 16 GUID bytes of the ID with the CodeView entry, does not run-DET, and does not check the checksum. So neither defect fails a test.
Suggested fix
- Compute the checksum over the serialized PDB with the 20-byte ID zeroed, as Roslyn does: serialize the PDB with a zero ID, hash it, then write the ID and the debug directory entries.
- Under
-DET, write the stamp into the heap as well as the GUID, for example with aChangePdbStreamId(guid, stamp)that patches all 20 bytes. - Extend
TestPortablePdbDebugDirectoryto compare all 20 bytes of the ID (GUID and stamp) with the CodeView entry, to verify thePdbChecksumentry by the PE-COFF.md rule, and to run with and without-DET.
Fix: #135212
Note
This report and its analysis were prepared with AI assistance (Anthropic Claude and OpenAI Codex) under my direction. AI agents ran the reproduction on my machine. I reviewed the text before posting.
Activity
dotnet-policy-service commentedon Oct 5, 2026
Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.
pcshrosbree commentedon Oct 6, 2026
This issue is about the native
ilasm(src/coreclr/ilasm), the same tool as #135214, which carriesarea-ILTools-coreclr. The bot labelled this onearea-System.IO, so the ILTools owners were not tagged. Could someone move it toarea-ILTools-coreclr?Context from the discussion on #135212: @am11 noted that the managed
ilasmrewrite is the direction, so this report may end up tracked against that implementation rather than the native one. Either way, the ILTools area is the right home.Note
This comment was prepared with AI assistance (Anthropic Claude) at my request.