From 7dfbebcef67e00a0bfcb16bbdafbbf4b7e96c514 Mon Sep 17 00:00:00 2001 From: Peter Shrosbree Date: Sun, 4 Oct 2026 21:42:06 -0700 Subject: [PATCH 1/2] Fix ilasm PdbChecksum and the PDB ID under -DET ilasm computed the PdbChecksum debug directory entry over the #Pdb stream only, while PE-COFF.md requires the hash of the entire PDB file with its 20-byte PDB ID zeroed. Under -DET it also wrote only the deterministic GUID into the #Pdb stream, so the PDB ID's timestamp stayed 0 while the CodeView entry carried the deterministic stamp, and PEReader.TryOpenAssociatedPortablePdb rejected the pair. Compute the checksum by serializing the PDB with its ID temporarily zeroed, and write both the GUID and the timestamp into the #Pdb stream. Extend TestPortablePdbDebugDirectory to compare all 20 bytes of the PDB ID with the CodeView entry and to verify the PdbChecksum entry, with and without -DET. --- src/coreclr/ilasm/portable_pdb.cpp | 29 ++++++++++-- src/coreclr/ilasm/portable_pdb.h | 4 +- src/coreclr/ilasm/writer.cpp | 10 ++-- src/coreclr/md/compiler/emit.cpp | 23 ++-------- src/coreclr/md/compiler/regmeta.h | 9 ++-- src/coreclr/md/enc/pdbheap.cpp | 20 ++++---- src/coreclr/md/inc/pdbheap.h | 3 +- src/coreclr/md/inc/portablepdbmdi.h | 9 ++-- .../PortablePdb/IlasmPortablePdbTester.cs | 46 ++++++++++++------- .../IlasmPortablePdbTesterCommon.cs | 4 +- 10 files changed, 86 insertions(+), 71 deletions(-) diff --git a/src/coreclr/ilasm/portable_pdb.cpp b/src/coreclr/ilasm/portable_pdb.cpp index 3aa26fe1597a5f..7fa5b1fb2e92f4 100644 --- a/src/coreclr/ilasm/portable_pdb.cpp +++ b/src/coreclr/ilasm/portable_pdb.cpp @@ -160,15 +160,36 @@ HRESULT PortablePdbWriter::BuildPdbStream(IMetaDataEmit3* peEmitter, mdMethodDef return hr; } -HRESULT PortablePdbWriter::ComputeSha256PdbStreamChecksum(BYTE(&checksum)[32]) +HRESULT PortablePdbWriter::ComputeSha256PdbChecksum(BYTE(&checksum)[32]) { - return m_ilasmPdbWriter->ComputeSha256PdbStreamChecksum(Sha256Hash, checksum); + // The checksum is the hash of the entire PDB file with its 20-byte PDB ID zeroed + // (see "PDB Checksum Debug Directory Entry" in docs/design/specs/PE-COFF.md). + // Serialize the PDB the same way it is saved later, with the ID temporarily zeroed, + // then restore the ID. + HRESULT hr = S_OK; + HRESULT restoreHr = S_OK; + DWORD pdbSize = 0; + BYTE* pdbData = NULL; + + if (FAILED(hr = m_ilasmPdbWriter->ChangePdbStreamId(GUID(), 0))) goto exit; + if (FAILED(hr = m_pdbEmitter->GetSaveSize(cssAccurate, &pdbSize))) goto exit; + pdbData = new BYTE[pdbSize]; + if (FAILED(hr = m_pdbEmitter->SaveToMemory(pdbData, pdbSize))) goto exit; + hr = Sha256Hash(pdbData, pdbSize, checksum, sizeof(checksum)); + +exit: + restoreHr = m_ilasmPdbWriter->ChangePdbStreamId(m_pdbStream.id.pdbGuid, m_pdbStream.id.pdbTimeStamp); + if (SUCCEEDED(hr)) + hr = restoreHr; + delete[] pdbData; + return hr; } -HRESULT PortablePdbWriter::ChangePdbStreamGuid(REFGUID newGuid) +HRESULT PortablePdbWriter::ChangePdbStreamId(REFGUID newGuid, const ULONG newTimestamp) { m_pdbStream.id.pdbGuid = newGuid; - return m_ilasmPdbWriter->ChangePdbStreamGuid(newGuid); + m_pdbStream.id.pdbTimeStamp = newTimestamp; + return m_ilasmPdbWriter->ChangePdbStreamId(newGuid, newTimestamp); } HRESULT PortablePdbWriter::DefineDocument(char* name, GUID* language) diff --git a/src/coreclr/ilasm/portable_pdb.h b/src/coreclr/ilasm/portable_pdb.h index 5fd69d0fbb6034..00a9c0fc8c0efb 100644 --- a/src/coreclr/ilasm/portable_pdb.h +++ b/src/coreclr/ilasm/portable_pdb.h @@ -50,8 +50,8 @@ class PortablePdbWriter void SetTimestamp(const ULONG newTimestamp); Document* GetCurrentDocument(); HRESULT BuildPdbStream(IMetaDataEmit3* peEmitter, mdMethodDef entryPoint); - HRESULT ComputeSha256PdbStreamChecksum(BYTE (&checksum)[32]); - HRESULT ChangePdbStreamGuid(REFGUID newGuid); + HRESULT ComputeSha256PdbChecksum(BYTE (&checksum)[32]); + HRESULT ChangePdbStreamId(REFGUID newGuid, const ULONG newTimestamp); HRESULT DefineDocument(char* name, GUID* language); HRESULT DefineSequencePoints(Method* method); HRESULT DefineLocalScope(Method* method); diff --git a/src/coreclr/ilasm/writer.cpp b/src/coreclr/ilasm/writer.cpp index a985d2ec196c63..7076922965b8ba 100644 --- a/src/coreclr/ilasm/writer.cpp +++ b/src/coreclr/ilasm/writer.cpp @@ -79,8 +79,8 @@ HRESULT Assembler::InitMetaData() if (m_fDeterministic) { - // When build determinism is enabled, the PDB checksum is computed with these fields set to zero. - // The GUID and timestamp will be updated after. + // In deterministic mode, the random GUID and timestamp set by Init() must not reach the output. + // The deterministic PDB ID is written in CreatePEFile, once the metadata hash is known. m_pPortablePdbWriter->SetGuid(GUID()); m_pPortablePdbWriter->SetTimestamp(0); } @@ -1480,16 +1480,14 @@ HRESULT Assembler::CreatePEFile(_In_ __nullterminated WCHAR *pwzOutputFilename) if (FAILED(hr = m_pPortablePdbWriter->BuildPdbStream(m_pEmitter, entryPoint))) goto exit; BYTE pdbChecksum[32]; - if (FAILED(hr = m_pPortablePdbWriter->ComputeSha256PdbStreamChecksum(pdbChecksum))) goto exit; + if (FAILED(hr = m_pPortablePdbWriter->ComputeSha256PdbChecksum(pdbChecksum))) goto exit; if (m_fDeterministic) { // Now that the PDB checksum has been computed, update the GUID and timestamp _ASSERTE(*(m_pPortablePdbWriter->GetGuid()) == GUID()); - if (FAILED(hr = m_pPortablePdbWriter->ChangePdbStreamGuid(deterministicGuid))) goto exit; - _ASSERTE(m_pPortablePdbWriter->GetTimestamp() == 0); - m_pPortablePdbWriter->SetTimestamp(deterministicTimestamp); + if (FAILED(hr = m_pPortablePdbWriter->ChangePdbStreamId(deterministicGuid, deterministicTimestamp))) goto exit; } if (FAILED(hr=CreateDebugDirectory(pdbChecksum))) goto exit; diff --git a/src/coreclr/md/compiler/emit.cpp b/src/coreclr/md/compiler/emit.cpp index 50ff3c1ad2e9d0..7c518799dc5e43 100644 --- a/src/coreclr/md/compiler/emit.cpp +++ b/src/coreclr/md/compiler/emit.cpp @@ -2116,29 +2116,16 @@ STDMETHODIMP RegMeta::DefineLocalVariable( // S_OK or error. } // RegMeta::DefineLocalVariable //******************************************************************************* -// ComputeSha256PdbStreamChecksum +// ChangePdbStreamId //******************************************************************************* -STDMETHODIMP RegMeta::ComputeSha256PdbStreamChecksum( - HRESULT (*computeSha256)(BYTE* pSrc, DWORD srcSize, BYTE* pDst, DWORD dstSize), - BYTE (&checksum)[32]) +STDMETHODIMP RegMeta::ChangePdbStreamId( + REFGUID newGuid, + ULONG newTimestamp) { #ifdef FEATURE_METADATA_EMIT_IN_DEBUGGER return E_NOTIMPL; #else //!FEATURE_METADATA_EMIT_IN_DEBUGGER - return m_pStgdb->m_pPdbHeap->ComputeSha256Checksum(computeSha256, checksum); -#endif //!FEATURE_METADATA_EMIT_IN_DEBUGGER -} - -//******************************************************************************* -// ChangePdbStreamGuid -//******************************************************************************* -STDMETHODIMP RegMeta::ChangePdbStreamGuid( - REFGUID newGuid) -{ -#ifdef FEATURE_METADATA_EMIT_IN_DEBUGGER - return E_NOTIMPL; -#else //!FEATURE_METADATA_EMIT_IN_DEBUGGER - return m_pStgdb->m_pPdbHeap->SetDataGuid(newGuid); + return m_pStgdb->m_pPdbHeap->SetDataId(newGuid, newTimestamp); #endif //!FEATURE_METADATA_EMIT_IN_DEBUGGER } diff --git a/src/coreclr/md/compiler/regmeta.h b/src/coreclr/md/compiler/regmeta.h index b16a1326b02502..9a5bcbf3208904 100644 --- a/src/coreclr/md/compiler/regmeta.h +++ b/src/coreclr/md/compiler/regmeta.h @@ -1133,12 +1133,9 @@ class RegMeta : //***************************************************************************** // IILAsmPortablePdbWriter methods //***************************************************************************** - STDMETHODIMP ComputeSha256PdbStreamChecksum( // S_OK or error. - HRESULT (*computeSha256)(BYTE* pSrc, DWORD srcSize, BYTE* pDst, DWORD dstSize), // [IN] - BYTE (&checksum)[32]); // [OUT] 256-bit Pdb checksum - - STDMETHODIMP ChangePdbStreamGuid( // S_OK or error. - REFGUID newGuid); // [IN] GUID to use as the PDB GUID + STDMETHODIMP ChangePdbStreamId( // S_OK or error. + REFGUID newGuid, // [IN] GUID to use as the PDB GUID + ULONG newTimestamp); // [IN] Timestamp to use as the PDB stamp #endif // FEATURE_METADATA_EMIT_PORTABLE_PDB //***************************************************************************** diff --git a/src/coreclr/md/enc/pdbheap.cpp b/src/coreclr/md/enc/pdbheap.cpp index d402cddc0a7316..1e406a4d2e5f75 100644 --- a/src/coreclr/md/enc/pdbheap.cpp +++ b/src/coreclr/md/enc/pdbheap.cpp @@ -68,23 +68,25 @@ HRESULT PdbHeap::SetData(PORT_PDB_STREAM* data) __checkReturn -HRESULT PdbHeap::SetDataGuid(REFGUID newGuid) +HRESULT PdbHeap::SetDataId(REFGUID newGuid, ULONG newTimestamp) { _ASSERTE(m_size >= sizeof(PDB_ID)); - if (memcpy_s(m_data, m_size, &newGuid, sizeof(GUID))) + PDB_ID id; + id.pdbGuid = newGuid; + id.pdbTimeStamp = newTimestamp; + +#if BIGENDIAN + SwapGuid(&id.pdbGuid); + id.pdbTimeStamp = VAL32(id.pdbTimeStamp); +#endif + + if (memcpy_s(m_data, m_size, &id, sizeof(id))) return E_FAIL; return S_OK; } -__checkReturn -HRESULT PdbHeap::ComputeSha256Checksum(HRESULT (*computeSha256)(BYTE* pSrc, DWORD srcSize, BYTE* pDst, DWORD dstSize), BYTE (&checksum)[32]) -{ - _ASSERTE(m_size >= sizeof(PDB_ID)); - return computeSha256(m_data, m_size, (BYTE*)&checksum, sizeof(checksum)); -} - __checkReturn HRESULT PdbHeap::SaveToStream(IStream* stream) { diff --git a/src/coreclr/md/inc/pdbheap.h b/src/coreclr/md/inc/pdbheap.h index 88bae893f86d7e..1c6387b2ce52cb 100644 --- a/src/coreclr/md/inc/pdbheap.h +++ b/src/coreclr/md/inc/pdbheap.h @@ -21,8 +21,7 @@ class PdbHeap ~PdbHeap(); __checkReturn HRESULT SetData(PORT_PDB_STREAM* data); - __checkReturn HRESULT SetDataGuid(REFGUID newGuid); - __checkReturn HRESULT ComputeSha256Checksum(HRESULT (*computeSha256)(BYTE* pSrc, DWORD srcSize, BYTE* pDst, DWORD dstSize), BYTE (&checksum)[32]); + __checkReturn HRESULT SetDataId(REFGUID newGuid, ULONG newTimestamp); __checkReturn HRESULT SaveToStream(IStream* stream); BOOL IsEmpty(); ULONG GetSize(); diff --git a/src/coreclr/md/inc/portablepdbmdi.h b/src/coreclr/md/inc/portablepdbmdi.h index fffdfbe95a5ff9..3a598744f0c4d3 100644 --- a/src/coreclr/md/inc/portablepdbmdi.h +++ b/src/coreclr/md/inc/portablepdbmdi.h @@ -100,12 +100,9 @@ EXTERN_GUID(IID_IILAsmPortablePdbWriter, 0x8b2db1f0, 0x91f5, 0x4c99, 0xbb, 0x07, #define INTERFACE IILAsmPortablePdbWriter DECLARE_INTERFACE_(IILAsmPortablePdbWriter, IUnknown) { - STDMETHOD(ComputeSha256PdbStreamChecksum)( // S_OK or error. - HRESULT (*computeSha256)(BYTE* pSrc, DWORD srcSize, BYTE* pDst, DWORD dstSize), // [IN] - BYTE (&checksum)[32]) PURE; // [OUT] 256-bit Pdb checksum - - STDMETHOD(ChangePdbStreamGuid)( // S_OK or error. - REFGUID newGuid) PURE; // [IN] GUID to use as the PDB GUID + STDMETHOD(ChangePdbStreamId)( // S_OK or error. + REFGUID newGuid, // [IN] GUID to use as the PDB GUID + ULONG newTimestamp) PURE; // [IN] Timestamp to use as the PDB stamp }; #ifdef __cplusplus diff --git a/src/tests/ilasm/PortablePdb/IlasmPortablePdbTester.cs b/src/tests/ilasm/PortablePdb/IlasmPortablePdbTester.cs index e4089f9812e377..85bcaf29feac93 100644 --- a/src/tests/ilasm/PortablePdb/IlasmPortablePdbTester.cs +++ b/src/tests/ilasm/PortablePdb/IlasmPortablePdbTester.cs @@ -5,7 +5,9 @@ using System.Linq; using Xunit; using System.Collections.Generic; +using System.Collections.Immutable; using System.Reflection.Metadata; +using System.Security.Cryptography; namespace IlasmPortablePdbTests { @@ -27,15 +29,17 @@ public IlasmPortablePdbTester() IlasmFile = IlasmFileName + NativeExtension; } - // Tests whether pe file includes portable pdb codeview debug directory - // and its contents against the generated portable pdb metadata file + // Tests whether pe file includes portable pdb codeview and pdb checksum debug directory entries + // and their contents against the generated portable pdb file, with and without deterministic output [Theory] - [InlineData("TestPdbDebugDirectory1.il")] - [InlineData("TestPdbDebugDirectory2.il")] - public void TestPortablePdbDebugDirectory(string ilSource) + [InlineData("TestPdbDebugDirectory1.il", false)] + [InlineData("TestPdbDebugDirectory1.il", true)] + [InlineData("TestPdbDebugDirectory2.il", false)] + [InlineData("TestPdbDebugDirectory2.il", true)] + public void TestPortablePdbDebugDirectory(string ilSource, bool deterministic) { var ilasm = IlasmPortablePdbTesterCommon.GetIlasmFullPath(CoreRootVar, IlasmFile); - IlasmPortablePdbTesterCommon.Assemble(ilasm, ilSource, TestDir, out string dll, out string pdb); + IlasmPortablePdbTesterCommon.Assemble(ilasm, ilSource, TestDir, out string dll, out string pdb, deterministic); using (var peStream = new FileStream(dll, FileMode.Open, FileAccess.Read)) { @@ -51,22 +55,32 @@ public void TestPortablePdbDebugDirectory(string ilSource) Assert.Equal(1, portablePdbDbgEntry.Age); Assert.Equal(pdb, portablePdbDbgEntry.Path); + var pdbChecksumEntry = Assert.Single(dbgDirEntries, entry => entry.Type == DebugDirectoryEntryType.PdbChecksum); + var pdbChecksum = peReader.ReadPdbChecksumDebugDirectoryData(pdbChecksumEntry); + Assert.Equal("SHA256", pdbChecksum.AlgorithmName); + + var pdbImage = File.ReadAllBytes(pdb); + using (var pdbImageReaderProvider = MetadataReaderProvider.FromPortablePdbImage(ImmutableArray.Create(pdbImage))) + { + var pdbHeader = pdbImageReaderProvider.GetMetadataReader().DebugMetadataHeader; + Assert.NotNull(pdbHeader); + + // check pdb id (guid and stamp) against the codeview entry + var pdbId = new BlobContentId(pdbHeader.Id); + Assert.Equal(portablePdbDbgEntry.Guid, pdbId.Guid); + Assert.Equal(dbgEntry.Stamp, pdbId.Stamp); + + // check pdb checksum: the hash of the entire pdb file with its 20-byte pdb id zeroed + Array.Clear(pdbImage, pdbHeader.IdStartOffset, pdbHeader.Id.Length); + Assert.Equal(SHA256.HashData(pdbImage), pdbChecksum.Checksum.ToArray()); + } + using (var pdbReaderProvider = IlasmPortablePdbTesterCommon.GetMetadataReaderProvider(dll, pdb, peReader, false)) { var portablePdbMdReader = pdbReaderProvider.GetMetadataReader(); Assert.NotNull(portablePdbMdReader); // check pdb stream Assert.NotNull(portablePdbMdReader.DebugMetadataHeader); - // check pdb guid - var pdbGuid = portablePdbDbgEntry.Guid.ToByteArray(); - var pdbId = portablePdbMdReader.DebugMetadataHeader.Id.ToArray(); - int i = 0; - foreach (var pdbGuidByte in pdbGuid) - { - Assert.True(i < pdbId.Length); - Assert.Equal(pdbGuidByte, pdbId[i++]); - } - Assert.Equal(i, pdbGuid.Length); var peMdReader = peReader.GetMetadataReader(); Assert.NotNull(peMdReader); diff --git a/src/tests/ilasm/PortablePdb/IlasmPortablePdbTesterCommon.cs b/src/tests/ilasm/PortablePdb/IlasmPortablePdbTesterCommon.cs index f31fb44f5f06b6..0b6cb4735bec67 100644 --- a/src/tests/ilasm/PortablePdb/IlasmPortablePdbTesterCommon.cs +++ b/src/tests/ilasm/PortablePdb/IlasmPortablePdbTesterCommon.cs @@ -21,7 +21,7 @@ public static string GetIlasmFullPath(string coreRootVar, string ilasmFile) return ilasmFullPath; } - public static void Assemble(string ilasmFullPath, string ilSrc, string testDir, out string dll, out string pdb) + public static void Assemble(string ilasmFullPath, string ilSrc, string testDir, out string dll, out string pdb, bool deterministic = false) { var currentDirectory = Environment.CurrentDirectory; var ilSrcFullPath = Path.Combine(currentDirectory, testDir, ilSrc); @@ -33,7 +33,7 @@ public static void Assemble(string ilasmFullPath, string ilSrc, string testDir, var dllFullPath = Path.Combine(currentDirectory, testDir, dllFileName); var pdbFullPath = Path.Combine(currentDirectory, testDir, pdbFileName); - var ilasmArgs = $"{CommonIlasmArguments} -output={dllFullPath} {ilSrcFullPath}"; + var ilasmArgs = $"{CommonIlasmArguments}{(deterministic ? " -det" : string.Empty)} -output={dllFullPath} {ilSrcFullPath}"; var ilasmPsi = new ProcessStartInfo { UseShellExecute = false, From eda2a28d89cecfd57b2bc95598787a5e8394ba3b Mon Sep 17 00:00:00 2001 From: Peter Shrosbree Date: Mon, 5 Oct 2026 07:18:32 -0700 Subject: [PATCH 2/2] Derive ilasm -DET identities from the assembly's content Under -DET, ilasm derived the MVID, the PE timestamp and the PDB ID from SHA-256 of the metadata block straight after reserving it, before any metadata was written into it. The block was all zeros, so the identities depended only on the metadata's size, and different assemblies shared them. Derive them from the content instead, as PE-COFF.md describes: - The PDB ID is taken from the PdbChecksum, which is the hash of the serialized PDB with its ID zeroed. The CodeView entry carries that ID. - The MVID and the PE timestamp are taken from the hash of the whole image, computed once the image is complete, with both left zero. A new ICeeFileGen::ComputeImageHash fixes the image up and hashes it as GenerateCeeFile will write it, through PEWriter::write(void**), with ilasm supplying the hash function, since mscorpe has none. The MVID is then written into the already serialized metadata, and the timestamp into the file header. The in-memory writer, PEWriter::write(void**), was unreachable through the existing ICeeFileGen::GenerateCeeFile call path. It now sizes the image as write(fileName) writes the file, which with /STRIPRELOC includes the data of the stripped .reloc section; before, it would have written past the end of its buffer. Also fix a second -DET defect: the export directory's timestamp was the current time, so -DET output with exports was not repeatable. It is now 0, which also keeps it out of the MVID. Add TestDeterministicIdentity, which assembles pairs of inputs that differ only in a method body, in metadata of the same size or in sequence points, with and without -DEBUG, and checks that the identities differ where the content differs, that the same input gives the same bytes, and that each identity is the hash of its file with the identity fields zeroed. Add TestDeterministicImageLayout, which checks the same for /STRIPRELOC, PE32+ and an assembly with an export, and that the export timestamp is 0. The test helper now deletes earlier outputs before each run. --- src/coreclr/dlls/mscorpe/ceefilegenwriter.cpp | 13 ++ src/coreclr/dlls/mscorpe/iceefilegen.cpp | 11 ++ src/coreclr/dlls/mscorpe/pewriter.cpp | 42 +++++- src/coreclr/dlls/mscorpe/pewriter.h | 4 + src/coreclr/ilasm/writer.cpp | 100 ++++++++++--- src/coreclr/inc/ceefilegenwriter.h | 2 + src/coreclr/inc/iceefilegen.h | 7 + .../PortablePdb/IlasmPortablePdbTester.cs | 135 ++++++++++++++++++ .../IlasmPortablePdbTesterCommon.cs | 14 +- .../TestFiles/TestDeterministicExport.il | 24 ++++ .../TestFiles/TestDeterministicIdentity.il | 21 +++ src/tests/ilasm/ilasm_tests.csproj | 8 ++ 12 files changed, 348 insertions(+), 33 deletions(-) create mode 100644 src/tests/ilasm/PortablePdb/TestFiles/TestDeterministicExport.il create mode 100644 src/tests/ilasm/PortablePdb/TestFiles/TestDeterministicIdentity.il diff --git a/src/coreclr/dlls/mscorpe/ceefilegenwriter.cpp b/src/coreclr/dlls/mscorpe/ceefilegenwriter.cpp index fe7cc543221d37..4a68e0a8877073 100644 --- a/src/coreclr/dlls/mscorpe/ceefilegenwriter.cpp +++ b/src/coreclr/dlls/mscorpe/ceefilegenwriter.cpp @@ -406,6 +406,19 @@ HRESULT CeeFileGenWriter::generateImage(void **ppImage) return hr; } // HRESULT CeeFileGenWriter::generateImage() +// Hash the image as generateImage would write it now. Fixing up first means the hash covers the +// relocated section contents and the final headers, and generateImage does not fix up again. +HRESULT CeeFileGenWriter::computeImageHash(HRESULT (*computeHash)(BYTE* pSrc, DWORD srcSize, BYTE* pDst, DWORD dstSize), + BYTE* pHash, DWORD hashSize) +{ + HRESULT hr; + + if (!m_fixed) + IfFailRet(fixup()); + + return getPEWriter().computeImageHash(computeHash, pHash, hashSize); +} // HRESULT CeeFileGenWriter::computeImageHash() + HRESULT CeeFileGenWriter::setOutputFileName(_In_ LPWSTR fileName) { if (m_outputFileName) diff --git a/src/coreclr/dlls/mscorpe/iceefilegen.cpp b/src/coreclr/dlls/mscorpe/iceefilegen.cpp index e066877b32270a..c08eab5b7cf831 100644 --- a/src/coreclr/dlls/mscorpe/iceefilegen.cpp +++ b/src/coreclr/dlls/mscorpe/iceefilegen.cpp @@ -457,3 +457,14 @@ HRESULT ICeeFileGen::SetFileHeaderTimeStamp(HCEEFILE ceeFile, DWORD timeStamp) return S_OK; } +HRESULT ICeeFileGen::ComputeImageHash(HCEEFILE ceeFile, + HRESULT (*computeHash)(BYTE* pSrc, DWORD srcSize, BYTE* pDst, DWORD dstSize), + BYTE* pHash, DWORD hashSize) +{ + TESTANDRETURNPOINTER(ceeFile); + TESTANDRETURNPOINTER(computeHash); + TESTANDRETURNPOINTER(pHash); + + CeeFileGenWriter *gen = reinterpret_cast(ceeFile); + return gen->computeImageHash(computeHash, pHash, hashSize); +} diff --git a/src/coreclr/dlls/mscorpe/pewriter.cpp b/src/coreclr/dlls/mscorpe/pewriter.cpp index 5f602656c64061..75e2918f569f58 100644 --- a/src/coreclr/dlls/mscorpe/pewriter.cpp +++ b/src/coreclr/dlls/mscorpe/pewriter.cpp @@ -1445,13 +1445,25 @@ HRESULT PEWriter::write(_In_ LPCWSTR fileName) { return hr; } -HRESULT PEWriter::write(void ** ppImage) +// The size of the file that write(fileName) produces. That is filePos, unless a section's data +// extends beyond it: with stripped relocations, the .reloc section is still written, but its +// size is not counted in filePos. +size_t PEWriter::getImageSize() { - const unsigned RoundUpVal = VAL32(m_ntHeaders->OptionalHeader.FileAlignment); - char *pad = (char *) _alloca(RoundUpVal); - memset(pad, 0, RoundUpVal); + size_t size = filePos; + for (PEWriterSection **cur = getSectStart(); cur < getSectCur(); cur++) { + if ((*cur)->m_header != NULL) { + size_t sectionEnd = (size_t)(*cur)->m_filePos + (*cur)->dataLen() + (*cur)->m_filePad; + if (sectionEnd > size) + size = sectionEnd; + } + } + return size; +} - size_t lSize = filePos; +HRESULT PEWriter::write(void ** ppImage) +{ + size_t lSize = getImageSize(); // allocate the block we are handing back to the caller void * pImage = (void *) CoTaskMemAlloc(lSize); @@ -1480,7 +1492,7 @@ HRESULT PEWriter::write(void ** ppImage) pCur = (char*)pImage + (*cur)->m_filePos; len = (*cur)->writeMem((void**)&pCur); _ASSERTE(len == (*cur)->dataLen()); - COPY_AND_ADVANCE(pCur, pad, (*cur)->m_filePad); + pCur += (*cur)->m_filePad; // the image is already zeroed } } @@ -1494,6 +1506,24 @@ HRESULT PEWriter::write(void ** ppImage) return S_OK; } +// Hash the image that write() would produce now. Must be called after fixup(). +HRESULT PEWriter::computeImageHash(HRESULT (*computeHash)(BYTE* pSrc, DWORD srcSize, BYTE* pDst, DWORD dstSize), + BYTE* pHash, DWORD hashSize) +{ + size_t imageSize = getImageSize(); + if (imageSize > UINT32_MAX) + return COR_E_OVERFLOW; + + void *pImage = NULL; + HRESULT hr = write(&pImage); + if (FAILED(hr)) + return hr; + + hr = computeHash((BYTE*)pImage, static_cast(imageSize), pHash, hashSize); + CoTaskMemFree(pImage); + return hr; +} + HRESULT PEWriter::getFileTimeStamp(DWORD *pTimeStamp) { if (pTimeStamp) diff --git a/src/coreclr/dlls/mscorpe/pewriter.h b/src/coreclr/dlls/mscorpe/pewriter.h index 5016c3a8ac305a..aa73655ab7bb04 100644 --- a/src/coreclr/dlls/mscorpe/pewriter.h +++ b/src/coreclr/dlls/mscorpe/pewriter.h @@ -40,6 +40,8 @@ class PEWriter : public PESectionMan HRESULT fixup(CeeGenTokenMapper *pMapper); HRESULT write(_In_ LPCWSTR fileName); HRESULT write(void **ppImage); + HRESULT computeImageHash(HRESULT (*computeHash)(BYTE* pSrc, DWORD srcSize, BYTE* pDst, DWORD dstSize), + BYTE* pHash, DWORD hashSize); // calling these functions is optional DWORD getSectionAlignment(); @@ -125,6 +127,8 @@ class PEWriter : public PESectionMan return COUNT_T(sectCur - sectStart); } + size_t getImageSize(); + IMAGE_DOS_HEADER m_dosHeader; IMAGE_NT_HEADERS * m_ntHeaders; diff --git a/src/coreclr/ilasm/writer.cpp b/src/coreclr/ilasm/writer.cpp index 7076922965b8ba..b0370bd38232bf 100644 --- a/src/coreclr/ilasm/writer.cpp +++ b/src/coreclr/ilasm/writer.cpp @@ -80,7 +80,7 @@ HRESULT Assembler::InitMetaData() if (m_fDeterministic) { // In deterministic mode, the random GUID and timestamp set by Init() must not reach the output. - // The deterministic PDB ID is written in CreatePEFile, once the metadata hash is known. + // The deterministic PDB ID is written in CreatePEFile, once the PDB content hash is known. m_pPortablePdbWriter->SetGuid(GUID()); m_pPortablePdbWriter->SetTimestamp(0); } @@ -578,9 +578,9 @@ HRESULT Assembler::CreateExportDirectory() // Data blob is ready pending Name Pointer Table values offsetting memset(&exportDirIDD,0,sizeof(IMAGE_EXPORT_DIRECTORY)); - // Grab the timestamp of the PE file. - DWORD fileTimeStamp; - if (FAILED(hr = m_pCeeFileGen->GetFileTimeStamp(m_pCeeFile,&fileTimeStamp))) return hr; + // Grab the timestamp of the PE file. It is the current time, so deterministic output uses 0. + DWORD fileTimeStamp = 0; + if (!m_fDeterministic && FAILED(hr = m_pCeeFileGen->GetFileTimeStamp(m_pCeeFile,&fileTimeStamp))) return hr; // Fill in the directory entry. // Characteristics, MajorVersion and MinorVersion play no role and stay 0 exportDirIDD.TimeDateStamp = VAL32(fileTimeStamp); @@ -1162,14 +1162,50 @@ HRESULT Assembler::AllocateStrongNameSignature() return S_OK; } +// Write the MVID into metadata that has already been serialized into the image. +// The Module row's Mvid column (ECMA-335 II.22.30) is a 1-based index into the #GUID heap (II.24.2.5). +static HRESULT SetSerializedMvid(IMetaDataEmit3* pEmitter, BYTE* metaData, DWORD metaDataSize, REFGUID mvid) +{ + HRESULT hr; + IMetaDataTables* pTables = NULL; + ULONG guidIndex = 0; + + if (FAILED(hr = pEmitter->QueryInterface(IID_IMetaDataTables, (void**)&pTables))) return hr; + hr = pTables->GetColumn(TBL_Module, ModuleRec::COL_Mvid, 1, &guidIndex); + pTables->Release(); + if (FAILED(hr)) return hr; + + STORAGEHEADER header; + ULONG cbStreamHeaders = metaDataSize; + PSTORAGESTREAM pStream = MDFormat::GetFirstStream_Verify(&header, metaData, &cbStreamHeaders); + if (pStream == NULL) return E_FAIL; + BYTE* pEnd = metaData + metaDataSize; + for (USHORT i = 0; i < header.GetiStreams(); i++, pStream = pStream->NextStream()) + { + BYTE* pName = (BYTE*)pStream + 2 * sizeof(ULONG); + if ((pName >= pEnd) || (memchr(pName, 0, pEnd - pName) == NULL)) + return E_FAIL; + + if (strcmp(pStream->GetName(), "#GUID") == 0) + { + if ((pStream->GetOffset() > metaDataSize) || (pStream->GetSize() > metaDataSize - pStream->GetOffset()) || + (guidIndex == 0) || (guidIndex > pStream->GetSize() / sizeof(GUID))) + return E_FAIL; + + BYTE* pMvid = metaData + pStream->GetOffset() + (guidIndex - 1) * sizeof(GUID); + memcpy_s(pMvid, sizeof(GUID), &mvid, sizeof(GUID)); + return S_OK; + } + } + return E_FAIL; +} + HRESULT Assembler::CreatePEFile(_In_ __nullterminated WCHAR *pwzOutputFilename) { HRESULT hr; DWORD mresourceSize = 0; BYTE* mresourceData = NULL; WCHAR* wzScopeName = NULL; - GUID deterministicGuid = GUID(); - ULONG deterministicTimestamp = 0; if(bClock) bClock->cMDEmitBegin = minipal_lowres_ticks(); if(m_fReportProgress) printf("Creating PE file\n"); @@ -1455,21 +1491,13 @@ HRESULT Assembler::CreatePEFile(_In_ __nullterminated WCHAR *pwzOutputFilename) if (m_fDeterministic) { - // Get deterministic GUID and timestamp from the computed hash - BYTE hash[32]; - _ASSERTE(sizeof(GUID) + sizeof(ULONG) <= sizeof(hash)); - if (FAILED(hr = Sha256Hash(metaData, metaDataSize, hash, sizeof(hash)))) goto exit; - - memcpy_s(&deterministicGuid, sizeof(GUID), hash, sizeof(GUID)); - memcpy_s(&deterministicTimestamp, sizeof(ULONG), hash + sizeof(GUID), sizeof(ULONG)); - - // In deterministic mode, the MVID needs to be stabilized for the metadata scope that was - // created in Assembler::InitMetaData, and it is guaranteed that the IMDInternalEmit for - // that scope was already acquired immediately after that scope was created. + // In deterministic mode, the MVID and the PE timestamp are derived from the content of the + // image once it is complete, at the end of this method. Until then both are zero, so that + // neither the random MVID set when the scope was created nor the current time is hashed. + // The IMDInternalEmit for the scope was acquired in Assembler::InitMetaData. _ASSERTE(m_pInternalEmitForDeterministicMvid != NULL); - m_pInternalEmitForDeterministicMvid->ChangeMvid(deterministicGuid); - - if (FAILED(hr = m_pCeeFileGen->SetFileHeaderTimeStamp(m_pCeeFile, deterministicTimestamp))) goto exit; + if (FAILED(hr = m_pInternalEmitForDeterministicMvid->ChangeMvid(GUID()))) goto exit; + if (FAILED(hr = m_pCeeFileGen->SetFileHeaderTimeStamp(m_pCeeFile, 0))) goto exit; } if (m_fGeneratePDB) @@ -1484,10 +1512,17 @@ HRESULT Assembler::CreatePEFile(_In_ __nullterminated WCHAR *pwzOutputFilename) if (m_fDeterministic) { - // Now that the PDB checksum has been computed, update the GUID and timestamp + // The checksum is the hash of the PDB with its ID zeroed, and the PDB is otherwise final, + // so the deterministic PDB ID is derived from it (see docs/design/specs/PE-COFF.md). + // The CodeView entry created below carries this ID, so the PE hash covers it. _ASSERTE(*(m_pPortablePdbWriter->GetGuid()) == GUID()); _ASSERTE(m_pPortablePdbWriter->GetTimestamp() == 0); - if (FAILED(hr = m_pPortablePdbWriter->ChangePdbStreamId(deterministicGuid, deterministicTimestamp))) goto exit; + GUID pdbGuid; + ULONG pdbTimestamp; + _ASSERTE(sizeof(GUID) + sizeof(ULONG) <= sizeof(pdbChecksum)); + memcpy_s(&pdbGuid, sizeof(GUID), pdbChecksum, sizeof(GUID)); + memcpy_s(&pdbTimestamp, sizeof(ULONG), pdbChecksum + sizeof(GUID), sizeof(ULONG)); + if (FAILED(hr = m_pPortablePdbWriter->ChangePdbStreamId(pdbGuid, pdbTimestamp))) goto exit; } if (FAILED(hr=CreateDebugDirectory(pdbChecksum))) goto exit; @@ -1786,6 +1821,27 @@ HRESULT Assembler::CreatePEFile(_In_ __nullterminated WCHAR *pwzOutputFilename) } } + if (m_fDeterministic) + { + // The image is now complete apart from its MVID and PE timestamp, which are zero. + // Derive both from the hash of the image as it will be written: the headers, the IL, + // the metadata, the resources and the debug directory, which carries the PDB ID. + // The image is fixed up before it is hashed, so GenerateCeeFile writes these same bytes, + // apart from the MVID and the timestamp set here. + BYTE hash[32]; + _ASSERTE(sizeof(GUID) + sizeof(ULONG) <= sizeof(hash)); + if (FAILED(hr = m_pCeeFileGen->ComputeImageHash(m_pCeeFile, Sha256Hash, hash, sizeof(hash)))) goto exit; + + GUID deterministicGuid; + ULONG deterministicTimestamp; + memcpy_s(&deterministicGuid, sizeof(GUID), hash, sizeof(GUID)); + memcpy_s(&deterministicTimestamp, sizeof(ULONG), hash + sizeof(GUID), sizeof(ULONG)); + + // The metadata has already been serialized into the image, so the MVID is written there. + if (FAILED(hr = SetSerializedMvid(m_pEmitter, metaData, metaDataSize, deterministicGuid))) goto exit; + if (FAILED(hr = m_pCeeFileGen->SetFileHeaderTimeStamp(m_pCeeFile, deterministicTimestamp))) goto exit; + } + hr = S_OK; exit: diff --git a/src/coreclr/inc/ceefilegenwriter.h b/src/coreclr/inc/ceefilegenwriter.h index bea9a2c57154b7..f30101dc941671 100644 --- a/src/coreclr/inc/ceefilegenwriter.h +++ b/src/coreclr/inc/ceefilegenwriter.h @@ -79,6 +79,8 @@ class CeeFileGenWriter : public CCeeGen HRESULT link(); // Layout the sections and assign their starting addresses HRESULT fixup(); // Apply relocations to any pointer data. Also generate PE base relocs HRESULT generateImage(void **ppImage); + HRESULT computeImageHash(HRESULT (*computeHash)(BYTE* pSrc, DWORD srcSize, BYTE* pDst, DWORD dstSize), + BYTE* pHash, DWORD hashSize); HRESULT setImageBase(size_t imageBase); HRESULT setImageBase64(ULONGLONG imageBase); diff --git a/src/coreclr/inc/iceefilegen.h b/src/coreclr/inc/iceefilegen.h index 43d92e4f2c883b..7af3f334216179 100644 --- a/src/coreclr/inc/iceefilegen.h +++ b/src/coreclr/inc/iceefilegen.h @@ -170,6 +170,13 @@ class ICeeFileGen { ULONG *pNumSections); virtual HRESULT SetVTableEntry64(HCEEFILE ceeFile, ULONG size, void* ptr); + + // Fix up the image and hash it as GenerateCeeFile would write it now. The caller may then + // change bytes it owns (such as the MVID) and the file header timestamp before GenerateCeeFile, + // but must not add section data or relocations, which would no longer be applied. + virtual HRESULT ComputeImageHash(HCEEFILE ceeFile, + HRESULT (*computeHash)(BYTE* pSrc, DWORD srcSize, BYTE* pDst, DWORD dstSize), + BYTE* pHash, DWORD hashSize); }; #endif diff --git a/src/tests/ilasm/PortablePdb/IlasmPortablePdbTester.cs b/src/tests/ilasm/PortablePdb/IlasmPortablePdbTester.cs index 85bcaf29feac93..1c8f17250ebeed 100644 --- a/src/tests/ilasm/PortablePdb/IlasmPortablePdbTester.cs +++ b/src/tests/ilasm/PortablePdb/IlasmPortablePdbTester.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; using System.Security.Cryptography; namespace IlasmPortablePdbTests @@ -96,6 +97,140 @@ public void TestPortablePdbDebugDirectory(string ilSource, bool deterministic) } } + // Tests that deterministic output derives the MVID, the PE timestamp and the PDB ID from the content: + // two inputs that differ only in a method body, in metadata of the same size, or in sequence points + // get different identities, and the same input gives the same bytes. + // Both inputs are assembled from the same source path to the same output path, so that they differ + // only in the replaced text. A change that reaches only the PDB still changes the PE image, through + // the PDB ID in its CodeView entry. + [Theory] + [InlineData("MethodBody", "ldc.i4.1", "ldc.i4.2", false, true)] + [InlineData("MethodBody", "ldc.i4.1", "ldc.i4.2", false, false)] + [InlineData("MetadataOfSameSize", "int32 F()", "int32 G()", false, true)] + [InlineData("MetadataOfSameSize", "int32 F()", "int32 G()", false, false)] + [InlineData("SequencePoints", ".line 10,10", ".line 20,20", true, true)] + [InlineData("SequencePoints", ".line 10,10", ".line 20,20", true, false)] + public void TestDeterministicIdentity(string change, string original, string replacement, bool onlyPdbChanges, bool debug) + { + var ilasm = IlasmPortablePdbTesterCommon.GetIlasmFullPath(CoreRootVar, IlasmFile); + var template = File.ReadAllText(Path.Combine(TestDir, "TestDeterministicIdentity.il")); + Assert.Equal(1, template.Split(original).Length - 1); + + var ilSource = $"TestDeterministicIdentity{change}{(debug ? "Debug" : "NoDebug")}.il"; + var ilPath = Path.Combine(TestDir, ilSource); + + File.WriteAllText(ilPath, template); + var first = AssembleDeterministic(ilasm, ilSource, debug); + var again = AssembleDeterministic(ilasm, ilSource, debug); + File.WriteAllText(ilPath, template.Replace(original, replacement)); + var second = AssembleDeterministic(ilasm, ilSource, debug); + + Assert.Equal(first.Dll, again.Dll); + Assert.Equal(first.Pdb, again.Pdb); + + bool imageChanges = !onlyPdbChanges || debug; + Assert.Equal(imageChanges, first.Mvid != second.Mvid); + Assert.Equal(imageChanges, first.Stamp != second.Stamp); + if (debug) + { + Assert.Equal(onlyPdbChanges, !first.PdbId.SequenceEqual(second.PdbId)); + } + + AssertIdentityIsContentHash(first.Dll, first.Pdb); + AssertIdentityIsContentHash(second.Dll, second.Pdb); + } + + // Tests that deterministic output is repeatable and its identity is the hash of the image as written, + // for image layouts that differ from the default: stripped relocations, PE32+, and an export directory, + // whose timestamp must not be the current time. + [Theory] + [InlineData("TestDeterministicIdentity.il", "-stripreloc", false)] + [InlineData("TestDeterministicIdentity.il", "-pe64 -x64", false)] + [InlineData("TestDeterministicExport.il", "", true)] + public void TestDeterministicImageLayout(string ilSource, string options, bool hasExports) + { + var ilasm = IlasmPortablePdbTesterCommon.GetIlasmFullPath(CoreRootVar, IlasmFile); + var first = AssembleDeterministic(ilasm, ilSource, debug: true, options: options); + var again = AssembleDeterministic(ilasm, ilSource, debug: true, options: options); + + Assert.Equal(first.Dll, again.Dll); + Assert.Equal(first.Pdb, again.Pdb); + AssertIdentityIsContentHash(first.Dll, first.Pdb); + + using (var peReader = new PEReader(ImmutableArray.Create(first.Dll))) + { + Assert.Equal(options.Contains("-stripreloc"), peReader.PEHeaders.CoffHeader.Characteristics.HasFlag(Characteristics.RelocsStripped)); + Assert.Equal(options.Contains("-pe64"), peReader.PEHeaders.PEHeader.Magic == PEMagic.PE32Plus); + + var exportTable = peReader.PEHeaders.PEHeader.ExportTableDirectory; + Assert.Equal(hasExports, exportTable.Size != 0); + if (hasExports) + { + Assert.True(peReader.PEHeaders.TryGetDirectoryOffset(exportTable, out int exportTableOffset)); + Assert.Equal(0u, BitConverter.ToUInt32(first.Dll, exportTableOffset + 4)); + } + } + } + + private static (byte[] Dll, byte[] Pdb, Guid Mvid, uint Stamp, byte[] PdbId) AssembleDeterministic(string ilasm, string ilSource, bool debug, string options = "") + { + IlasmPortablePdbTesterCommon.Assemble(ilasm, ilSource, TestDir, out string dll, out string pdb, deterministic: true, debug: debug, options: options); + + var peImage = File.ReadAllBytes(dll); + Guid mvid; + uint stamp; + using (var peReader = new PEReader(ImmutableArray.Create(peImage))) + { + var mdReader = peReader.GetMetadataReader(); + mvid = mdReader.GetGuid(mdReader.GetModuleDefinition().Mvid); + stamp = (uint)peReader.PEHeaders.CoffHeader.TimeDateStamp; + } + + byte[] pdbImage = null; + byte[] pdbId = null; + if (debug) + { + pdbImage = File.ReadAllBytes(pdb); + using (var pdbReaderProvider = MetadataReaderProvider.FromPortablePdbImage(ImmutableArray.Create(pdbImage))) + { + pdbId = pdbReaderProvider.GetMetadataReader().DebugMetadataHeader.Id.ToArray(); + } + } + + return (peImage, pdbImage, mvid, stamp, pdbId); + } + + // Checks that the MVID and the PE timestamp are the start of the SHA-256 hash of the PE file with both + // zeroed, and that the PDB ID is the start of the SHA-256 hash of the PDB file with its ID zeroed. + private static void AssertIdentityIsContentHash(byte[] peImage, byte[] pdbImage) + { + using (var peReader = new PEReader(ImmutableArray.Create(peImage))) + { + var mdReader = peReader.GetMetadataReader(); + int mvidOffset = peReader.PEHeaders.MetadataStartOffset + + mdReader.GetHeapMetadataOffset(HeapIndex.Guid) + + (MetadataTokens.GetHeapOffset(mdReader.GetModuleDefinition().Mvid) - 1) * 16; + int stampOffset = peReader.PEHeaders.CoffHeaderStartOffset + 4; + var identity = peImage.AsSpan(mvidOffset, 16).ToArray().Concat(peImage.AsSpan(stampOffset, 4).ToArray()).ToArray(); + + var zeroed = (byte[])peImage.Clone(); + Array.Clear(zeroed, mvidOffset, 16); + Array.Clear(zeroed, stampOffset, 4); + Assert.Equal(identity, SHA256.HashData(zeroed).Take(identity.Length).ToArray()); + } + + if (pdbImage is not null) + { + using (var pdbReaderProvider = MetadataReaderProvider.FromPortablePdbImage(ImmutableArray.Create(pdbImage))) + { + var pdbHeader = pdbReaderProvider.GetMetadataReader().DebugMetadataHeader; + var zeroed = (byte[])pdbImage.Clone(); + Array.Clear(zeroed, pdbHeader.IdStartOffset, pdbHeader.Id.Length); + Assert.Equal(pdbHeader.Id.ToArray(), SHA256.HashData(zeroed).Take(pdbHeader.Id.Length).ToArray()); + } + } + } + // Tests whether the portable PDB has all document name properly defined // The test source file includes external source reference and thus has 2 variants depending on OS type [Fact] diff --git a/src/tests/ilasm/PortablePdb/IlasmPortablePdbTesterCommon.cs b/src/tests/ilasm/PortablePdb/IlasmPortablePdbTesterCommon.cs index 0b6cb4735bec67..1050e71074aada 100644 --- a/src/tests/ilasm/PortablePdb/IlasmPortablePdbTesterCommon.cs +++ b/src/tests/ilasm/PortablePdb/IlasmPortablePdbTesterCommon.cs @@ -10,7 +10,7 @@ namespace IlasmPortablePdbTests { public static class IlasmPortablePdbTesterCommon { - public const string CommonIlasmArguments = "-nologo -dll -debug"; + public const string CommonIlasmArguments = "-nologo -dll"; public static string GetIlasmFullPath(string coreRootVar, string ilasmFile) { @@ -21,7 +21,7 @@ public static string GetIlasmFullPath(string coreRootVar, string ilasmFile) return ilasmFullPath; } - public static void Assemble(string ilasmFullPath, string ilSrc, string testDir, out string dll, out string pdb, bool deterministic = false) + public static void Assemble(string ilasmFullPath, string ilSrc, string testDir, out string dll, out string pdb, bool deterministic = false, bool debug = true, string options = "") { var currentDirectory = Environment.CurrentDirectory; var ilSrcFullPath = Path.Combine(currentDirectory, testDir, ilSrc); @@ -33,7 +33,7 @@ public static void Assemble(string ilasmFullPath, string ilSrc, string testDir, var dllFullPath = Path.Combine(currentDirectory, testDir, dllFileName); var pdbFullPath = Path.Combine(currentDirectory, testDir, pdbFileName); - var ilasmArgs = $"{CommonIlasmArguments}{(deterministic ? " -det" : string.Empty)} -output={dllFullPath} {ilSrcFullPath}"; + var ilasmArgs = $"{CommonIlasmArguments}{(debug ? " -debug" : string.Empty)}{(deterministic ? " -det" : string.Empty)} {options} -output={dllFullPath} {ilSrcFullPath}"; var ilasmPsi = new ProcessStartInfo { UseShellExecute = false, @@ -42,15 +42,19 @@ public static void Assemble(string ilasmFullPath, string ilSrc, string testDir, Arguments = ilasmArgs }; + // Remove earlier outputs, so that a stale file cannot pass for this run's output. + File.Delete(dllFullPath); + File.Delete(pdbFullPath); + Process ilasmProcess = Process.Start(ilasmPsi); ilasmProcess.WaitForExit(); Assert.Equal(0, ilasmProcess.ExitCode); Assert.True(File.Exists(dllFullPath)); - Assert.True(File.Exists(pdbFullPath)); + Assert.Equal(debug, File.Exists(pdbFullPath)); dll = dllFullPath; - pdb = pdbFullPath; + pdb = debug ? pdbFullPath : null; } public static MetadataReaderProvider GetMetadataReaderProvider(string dll, string pdb, PEReader peReader, bool embedded) diff --git a/src/tests/ilasm/PortablePdb/TestFiles/TestDeterministicExport.il b/src/tests/ilasm/PortablePdb/TestFiles/TestDeterministicExport.il new file mode 100644 index 00000000000000..4a4907e7ce2c26 --- /dev/null +++ b/src/tests/ilasm/PortablePdb/TestFiles/TestDeterministicExport.il @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +.assembly extern System.Runtime +{ +} + +.assembly TestDeterministicExport +{ +} + +.vtfixup [1] int32 fromunmanaged at VT_01 +.data VT_01 = int32(0) + +.class public auto ansi beforefieldinit TestDeterministicExportType + extends [System.Runtime]System.Object +{ + .method public static void Exported() cil managed + { + .vtentry 1 : 1 + .export [1] as Exported + ret + } +} diff --git a/src/tests/ilasm/PortablePdb/TestFiles/TestDeterministicIdentity.il b/src/tests/ilasm/PortablePdb/TestFiles/TestDeterministicIdentity.il new file mode 100644 index 00000000000000..dfc1f1b8e0e416 --- /dev/null +++ b/src/tests/ilasm/PortablePdb/TestFiles/TestDeterministicIdentity.il @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +.assembly extern System.Runtime +{ +} + +.assembly TestDeterministicIdentity +{ +} + +.class public auto ansi beforefieldinit TestDeterministicIdentityType + extends [System.Runtime]System.Object +{ + .method public hidebysig static int32 F() cil managed + { + .line 10,10 : 9,10 'TestDeterministicIdentity.cs' + ldc.i4.1 + ret + } +} diff --git a/src/tests/ilasm/ilasm_tests.csproj b/src/tests/ilasm/ilasm_tests.csproj index 6e4d9a17c241af..c76cbf6974e9c3 100644 --- a/src/tests/ilasm/ilasm_tests.csproj +++ b/src/tests/ilasm/ilasm_tests.csproj @@ -20,6 +20,14 @@ + + Always + TestFiles\%(FileName)%(Extension) + + + Always + TestFiles\%(FileName)%(Extension) + Always TestFiles\%(FileName)%(Extension)