Repository navigation
Find loaded PDB documents by token before comparing their fields - #586
Open
pcshrosbree wants to merge 1 commit into
Open
pcshrosbree wants to merge 1 commit into
pcshrosbree wants to merge 1 commit into
Conversation
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 0xd4d#566.
This was referenced Oct 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #566
At master, changing a loaded Portable PDB document's
Urlbefore the method bodies that refer to it are read can makeModuleDef.WritethrowArgumentException: An item with the same key has already been added, fromPdbState.Add_NoLock(SymbolDocument). The stack trace in #566 shows this.The cause:
PdbStatereads method bodies lazily. For each sequence point, it builds a partial document from theSymbolDocumentand looks it up indocDict, whose keys hashUrl. When an edit makes that lookup miss, dnlib creates a second copy of the document, andtokenToDocument.Addthrows because the row's token is already registered.This PR makes two changes in
PdbState:Add_NoLock(SymbolDocument)looks the document up intokenToDocumentfirst. A document read from a Portable PDB already has an identity, its Document row, andPdbStaterecords that token for each document it creates when it loads the PDB.Removenow deletes the token of the stored document it removes, and only when it removes one. Previously,Removedeleted its argument's token before trying to remove a document by value. Removing a document through an equal copy with no token therefore left the stored document's token behind. If removal by value failed,Removestill deleted the argument's token. With the first change alone, the token left behind after removal through an equal copy would make the lookup return the removed document.RemoveAllDocumentsalready clears both dictionaries, andAddrecords the token of each document it inserts. So with both changes, the token lookup only returns documents that are still in thePdbState. Documents from a Windows PDB have no token in dnlib:DbiDocument.MDTokenreturnsnull, as does the COM reader's equivalent. The lookup therefore doesn't affect them. I read that code but didn't test with a Windows PDB.I tested dnlib's net6.0 build with .NET SDK 10.0.401 on Linux x64, and read every written PDB back with System.Reflection.Metadata. I used two Debug C# libraries with Portable PDBs: a plain one (tested with the repro below), and one with two
#linenames that differ only by case, the case-sensitivity case from my review of #580.Urlfor every document), on the plain library: master throws. With this PR, it writes, and both methods' sequence points point at the document with the new name.Urledits, on the library with#linenames that differ only by case: master throws in each of the following cases. With this PR, each writes, and the sequence points of the method in the edited file point at its new name:Urldirectly.RemoveAllDocuments(true), edit one document'sUrl, thenAddeach document back (Changing PDB document URLs causes an exception #566's second variant).Removeone document, edit itsUrl, thenAddit back.Removethrough an equal copy, on the same library:NullReferenceExceptionin both versions, because the newPdbDocumenthas nullCustomDebugInfos(New PdbDocument has null CustomDebugInfos, so writing a Portable PDB throws NullReferenceException #584).Removeafter aUrledit returnsfalseat master and with this PR. At master it has already deleted the token, so the written PDB gets a second copy of the document under the old name. With this PR, it changes nothing, and the written PDB keeps the edited document.CheckSumedit; aLanguageedit;RemoveAllDocuments()without adding anything back;Removeof one document. In each case, this PR writes a PDB byte-identical to master's.CheckSumorLanguageedit throws in the same way, as the review on Fix PDB document identity to include all document row fields #580 noted. With this PR applied on top of Fix PDB document identity to include all document row fields #580, those edits and theUrledits all write.Removechange, the Changing PDB document URLs causes an exception #566 cases throw again.One limitation remains for Document rows whose names differ only by case. At master, two such rows are merged into one document when the PDB loads, because
Urlis compared case-insensitively, and only the first row's token is recorded. In my test, which prepends/movedto eachUrl, dnlib can then no longer match the second row to the loaded document. With this PR, dnlib creates a new document with the old name for sequence points that refer to the second row, and the write succeeds. With this PR applied on top of #580, the two rows load as separate documents and each edit applies to its own row. I didn't record the second row's token as an alias, becauseRemovedeletes only the stored document's own token, and the alias would then bring back a removed document.After a
Urledit,RemoveandGetExistingcan still miss the document or find a different one, because they use its current values for the lookup, but its dictionary entry is still filed under its old values. That needs a design choice, so I've described the options in #587.dnlib has no test project, so this PR adds no test. Here is the repro.
args[0]is an assembly with its Portable PDB beside it: