From 1d1885e01ae4a289cd4dbd8883666ccc526db274 Mon Sep 17 00:00:00 2001 From: Peter Shrosbree Date: Mon, 19 Oct 2026 08:19:38 -0700 Subject: [PATCH] Find loaded PDB documents by token before comparing their fields When PdbState lazily reads a method body, it looks up each sequence point's document again by Url. Changing a loaded Portable PDB document's Url before that read can make the lookup miss, so dnlib adds a second copy of the document and tokenToDocument.Add throws while the module is being written. A document read from a Portable PDB is identified by its Document row, so look it up by token first. For that lookup to return only documents that are still in the PdbState, Remove now deletes the token of the document it actually removes, and only when it removes one. Previously, Remove used its argument's token to choose which tokenToDocument entry to delete. That argument could be an equal copy without a token, or Remove could have removed nothing. Fixes #566. --- src/DotNet/Pdb/PdbState.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/DotNet/Pdb/PdbState.cs b/src/DotNet/Pdb/PdbState.cs index 39146563f..c1be0f4cf 100644 --- a/src/DotNet/Pdb/PdbState.cs +++ b/src/DotNet/Pdb/PdbState.cs @@ -130,6 +130,11 @@ PdbDocument Add_NoLock(PdbDocument doc) { } PdbDocument Add_NoLock(SymbolDocument symDoc) { + // A document read from a Portable PDB is identified by its Document row. Look it up by + // token first, so that editing its Url after loading doesn't make the lazy lookup from + // AddSequencePoints miss it and add the same row a second time (see #566). + if (symDoc.MDToken.HasValue && tokenToDocument.TryGetValue(symDoc.MDToken.Value, out var existing)) + return existing; var doc = PdbDocument.CreatePartialForCompare(symDoc); if (docDict.TryGetValue(doc, out var orig)) return orig; @@ -150,9 +155,13 @@ public bool Remove(PdbDocument doc) { #if THREAD_SAFE theLock.EnterWriteLock(); try { #endif - if (doc.MDToken.HasValue) - tokenToDocument.Remove(doc.MDToken.Value); - return docDict.Remove(doc); + // doc may be an equal copy of the stored document, so remove the stored document's token + if (!docDict.TryGetValue(doc, out var stored)) + return false; + docDict.Remove(doc); + if (stored.MDToken.HasValue) + tokenToDocument.Remove(stored.MDToken.Value); + return true; #if THREAD_SAFE } finally { theLock.ExitWriteLock(); } #endif