Skip to content

ilasm: -DET derives the MVID, PE timestamp and PDB ID from a zero-filled buffer, so different assemblies share them #135214

Description

@pcshrosbree

Description

With -DET, the native ilasm (src/coreclr/ilasm) derives the deterministic MVID, PE timestamp and PDB ID from SHA-256 of the metadata block. But it hashes that block before any metadata has been written into it. The block is still all zeros at that point, so the identity depends only on the metadata's size, not on the assembly's content. Different assemblies whose metadata is the same size get the same MVID, PE timestamp and PDB ID. This also happens without -DEBUG, for the MVID and timestamp.

Reproduction

Save this as A/Min.il, and again as B/Min.il with ldc.i4.1 changed to ldc.i4.2. The two files differ only in the method body, so their metadata is byte-identical:

.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
  }
}
ilasm -DLL -DEBUG -QUIET -DET -OUTPUT=A/Min.dll A/Min.il
ilasm -DLL -DEBUG -QUIET -DET -OUTPUT=B/Min.dll B/Min.il
dotnet run ids.cs -- A/Min.dll B/Min.dll

ids.cs is a file-based app (.NET 10 SDK or later):

#:property UseAppHost=false
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;

foreach (var dll in args)
{
    using var pe = new PEReader(File.OpenRead(dll));
    var md = pe.GetMetadataReader();
    var mvid = md.GetGuid(md.GetModuleDefinition().Mvid);
    using var pdb = MetadataReaderProvider.FromPortablePdbStream(File.OpenRead(Path.ChangeExtension(dll, ".pdb")));
    var id = Convert.ToHexString(pdb.GetMetadataReader().DebugMetadataHeader!.Id.ToArray());
    Console.WriteLine($"{dll}: MVID {mvid} | PE stamp {(uint)pe.PEHeaders.CoffHeader.TimeDateStamp} | PDB ID {id}");
}

Output with ilasm 10.0.0 (runtime.linux-x64.Microsoft.NETCore.ILAsm) on Linux x64:

A/Min.dll: MVID 942b4c7c-410c-6e42-36a4-cf6c83afabab | PE stamp 2978741164 | PDB ID 7C4C2B940C41426E36A4CF6C83AFABAB00000000
B/Min.dll: MVID 942b4c7c-410c-6e42-36a4-cf6c83afabab | PE stamp 2978741164 | PDB ID 7C4C2B940C41426E36A4CF6C83AFABAB00000000

The same identity comes out for a pair whose metadata differs in content but not in size (renaming F to G), and for a pair that differs only in a .line directive, whose PDBs differ in their sequence points. This metadata block is 464 bytes. The MVID is the first 16 bytes of SHA-256 of 464 zero bytes, and the PE stamp is the next 4. The PDB ID's trailing zero stamp is a separate defect, #135211.

Impact

Different module contents get the same MVID, so anything that keys cached method bodies or analysis on the MVID and a metadata token cannot tell them apart. Different PDBs share one PDB ID, so symbol stores keyed on the PDB file name and ID (or GUID) can return the wrong PDB. Whether a consumer then accepts the wrong symbols depends on its other checks; #135211 covers the separate matching and checksum defects. Repeated builds of the same input are still byte-identical, so -DET's repeatability is not affected.

Cause

  • In Assembler::CreatePEFile, GetSectionBlock reserves metaDataSize bytes (L1451). Under -DET the reserved block is hashed straight away (L1461), to produce deterministicGuid and deterministicTimestamp. These feed ChangeMvid (L1470), SetFileHeaderTimeStamp (L1472) and the PDB ID (L1489, L1492).
  • The metadata is written into the block only later, by EmitMetaDataAt.
  • Until then the block is zero, because CBlobFetcher::CPillar::MakeNewBlock zero-fills new storage.

This came in with #109091. The existing determinism checks in src/tests/Common/CLRTest.Jit.targets ("ILASM determinism") assemble the same input twice, so they cannot catch an identity that ignores content.

Suggested fix

Derive the identity from the assembly's content.

  • Hashing the populated metadata is not enough: the two files above have byte-identical metadata, and differ only in the method body in the IL section. The hash has to cover the image, with the MVID, the COFF timestamp and the CodeView GUID and stamp left zero; then those fields and the PDB ID are patched.

  • For comparison, Roslyn (through System.Reflection.Metadata) uses two hashes. PortablePdbBuilder hashes the PDB with its ID zeroed to get the PDB ID. PEBuilder hashes the PE image, with the MVID and timestamp reserved, to get those two. One hash for all three fields would also do for ilasm.

  • The write order seems to allow this:

    • nothing reaches disk before GenerateCeeFile in main.cpp, and the PDB is saved after the DLL;
    • CreatePEFile already patches the emitted metadata buffer in place after EmitMetaDataAt.

    The emitter assigns a random MVID at DefineScope, so it would have to be zeroed before the metadata is emitted.

A regression test would assemble two pairs under -DET: the pair above, and a pair whose metadata differs in content but not in size. It would assert that their MVIDs, timestamps and PDB IDs differ.

Fix: #135227

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

  1. dotnet-policy-service commented on Oct 5, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @dotnet/area-system-reflection-metadata
    See info in area-owners.md if you want to be subscribed.

  2. dotnet-policy-service commented on Oct 5, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @JulieLeeMSFT, @dotnet/jit-contrib
    See info in area-owners.md if you want to be subscribed.

  3. removed
    untriagedNew issue has not been triaged by the area owner
    on Oct 9, 2026
  4. added this to the 12.0.0 milestone on Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions