You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ilasm: PdbChecksum hashes only the #Pdb stream, and -DET leaves the PDB ID's stamp at 0 #135211
ilasm writes a Portable PDB and a matching debug directory that disagree with PE-COFF.md in two ways.
The PdbChecksum entry hashes only the #Pdb stream, 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 #Pdb stream 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's TimeDateStamp carries the deterministic stamp. PEReader.TryOpenAssociatedPortablePdb builds the expected ID from the CodeView GUID and that entry's stamp (PEReader.cs L785), so it refuses the PDB ilasm wrote alongside the DLL. StackTraceSymbols locates PDBs through that call (L198).
Both came in with #109091 and are unchanged on main (checked at 30f813606ae).
Check 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;usingSystem.Reflection.Metadata;usingSystem.Reflection.PortableExecutable;usingSystem.Security.Cryptography;foreach(vardllinargs){usingvarpe=newPEReader(File.OpenRead(dll));boolopened=pe.TryOpenAssociatedPortablePdb(dll, p =>File.Exists(p)?File.OpenRead(p):null,outvarprovider,out_);provider?.Dispose();varcodeView=pe.ReadDebugDirectory().First(e =>e.Type==DebugDirectoryEntryType.CodeView);byte[]pdb=File.ReadAllBytes(Path.ChangeExtension(dll,".pdb"));usingvarpdbProvider=MetadataReaderProvider.FromPortablePdbImage(System.Collections.Immutable.ImmutableArray.Create(pdb));varheader=pdbProvider.GetMetadataReader().DebugMetadataHeader!;intidAt=header.IdStartOffset;// #Pdb stream: PDB ID (20), EntryPoint (4), ReferencedTypeSystemTables (8), then one row count per table.ulongmask=BitConverter.ToUInt64(pdb,idAt+24);intstreamLength=32+4*BitOperations.PopCount(mask);byte[]Zeroed(byte[]b,intat){varc=(byte[])b.Clone();Array.Clear(c,at,20);returnc;}varstream=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(vareinpe.ReadDebugDirectory().Where(e =>e.Type==DebugDirectoryEntryType.PdbChecksum)){varsum=pe.ReadPdbChecksumDebugDirectoryData(e).Checksum;boolIs(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::ComputeSha256Checksum hashes m_data/m_size, which is the #Pdb stream that PdbHeap::SetData built, not the serialized PDB. Assembler::CreatePEFile calls it right after BuildPdbStream, before the PDB's other streams are serialized.
Under -DET, that call happens while the ID is all zero (L1483). Then ChangePdbStreamGuid patches the GUID into the heap through PdbHeap::SetDataGuid, but SetTimestamp updates only PortablePdbWriter::m_pdbStream, although the comment above the two calls says "update the GUID and timestamp". CreateDebugDirectory reads the stamp from that struct for the CodeView entry (writer.cpp L360), and the heap, which is what is saved, keeps 0.
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 a ChangePdbStreamId(guid, stamp) that patches all 20 bytes.
Extend TestPortablePdbDebugDirectory to compare all 20 bytes of the ID (GUID and stamp) with the CodeView entry, to verify the PdbChecksum entry by the PE-COFF.md rule, and to run with and without -DET.
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.
This issue is about the native ilasm (src/coreclr/ilasm), the same tool as #135214, which carries area-ILTools-coreclr. The bot labelled this one area-System.IO, so the ILTools owners were not tagged. Could someone move it to area-ILTools-coreclr?
Context from the discussion on #135212: @am11 noted that the managed ilasm rewrite 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.
Description
ilasm writes a Portable PDB and a matching debug directory that disagree with PE-COFF.md in two ways.
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.-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 at30f813606ae).Reproduction
Min.il:Assemble it twice, into separate directories, where
ilasmis the build under test:Check 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):Output with ilasm 10.0.0 (
runtime.linux-x64.Microsoft.NETCore.ILAsm), Linux x64. The non-deterministic stamp varies between runs; the-DETvalues depend only on the size of this input's metadata (#135214):As a control, the same program on a Roslyn-built DLL prints
TrueforTryOpenAssociatedPortablePdband for the PE-COFF.md rule, andFalsefor 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.-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
-DET, write the stamp into the heap as well as the GUID, for example with aChangePdbStreamId(guid, stamp)that patches all 20 bytes.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.