Internal error during DumpSymbolMap

DUMPBIN has an undocumented command-line option /map that aims to dump a Section Contribution Map from a PDB but which settles for dumping a so-called Symbol Map that is confected from the COFF Symbol Table. However, the code that would build and dump this Symbol Map has perhaps not been treated to the best care that Microsoft’s programmers are surely capable of. One coding error is bad enough to cause a CPU exception, specifically from dereferencing a null pointer. The ordinary consequence is that the program’s top-level exception handler terminates the program after displaying the message

DUMPBIN : fatal error LNK1000: Internal error during DumpSymbolMap

followed by a version number, some exception information and a dump of CPU registers.

This internal error is reliably reproducible with input files that Microsoft has sold by the hundreds of millions and presumably regards as well-formed. A traditional exposition would begin with the details for reproducing the problem and proceed to what was eventually discovered as the explanation. At this site however, the process of discovery mostly works in the other direction: to study the code, see an error, and then deduce how to trigger it.

The DumpSymbolMap Function

As noted above, the coding error is found on investigating what /map does. Of particular interest is the case where

Barring early exits due to unrelated errors, these are necessary and sufficient conditions to have DUMPBIN fall back on dumping a Symbol Map, which is done in a function that is plausibly called DumpSymbolMap in Microsoft’s source code.

The Symbol Map is extracted from the COFF Symbol Table. This is an array of 18-byte structures of two types. An ordinary entry in the Symbol Table has the form of an IMAGE_SYMBOL structure. It may be followed by some number, including zero, of auxiliary entries. The NumberOfAuxSymbols member in the ordinary entry tells how many auxiliary entries follow. Interpretation of the auxiliary entries that follow an ordinary entry depends on other members of the ordinary entry. The DumpSymbolMap function searches for ordinary entries such that

For each such qualifying entry, the Value member is taken to be the RVA of a new block in the Symbol Map. If the entry is not the last in the Symbol Table and the next entry has the same SectionNumber, then the block extends up to but not including the RVA given by the Value member of this next entry. Otherwise, the block finishes with the section.

Note that DUMPBIN takes for granted that the next entry is an ordinary entry, and that if it has the same SectionNumber then it has a higher Value. By extension, it seems assumed that all qualifying entries with the same SectionNumber are arranged in increasing order of Value. Perhaps this is true in all well-formed COFF Symbol Tables, but let’s put aside the interpretation of entries in the COFF Symbol Table and look instead at how the DumpSymbolMap function even gets these entries.

There are two cases. In one, the whole of the COFF Symbol Table has already been read and DumpSymbolMap is given the address. In the other, DumpSymbolMap is left to read the entries by itself. It does this one at a time. The coding for this second case has potential for trouble on three points.

First, the function does nothing about setting the file pointer. It just assumes that the current file position for reading from the input file is at the first entry in the COFF Symbol Table.

Second, each entry is read into the one 18-byte buffer. If a qualifying entry happens to be preceded by an auxiliary entry, then the function does not actually test the SectionNumber and StorageClass members of an ordinary entry but instead tests whatever the auxiliary entry happens to have in the same places as these members.

Third, for the computation of size by subtracting the Value member of the qualifying entry from the same member in the next entry, there is yet no next entry that has been read. What would be the pointer to the next entry is NULL. The attempt to use it will produce the CPU exception and thence the LNK1000 internal error. The question is: can this fault be triggered in practice or is it just an error in theory, e.g., because the code is vestigial, with no way to execute? To decide, we work backwards.

Finding the COFF Symbol Table

A necessary condition for the fault is that DumpSymbolMap believes that the input file has a non-zero number of symbols in a COFF Symbol Table that has not yet been read. How might DUMPBIN have determined that there are symbols but not read them?

Given a DBG file as input, DUMPBIN works from the debug directory, specifically from an entry that has IMAGE_DEBUG_TYPE_COFF (1) as the Type. In this entry, the PointerToRawData and SizeOfData members locate an IMAGE_COFF_SYMBOLS_HEADER, which in turn has members LvaToFirstSymbol and NumberOfSymbols that locate the COFF Symbol Table. DUMPBIN assumes that the necessary members are all meaningful, and loads the table. This may be not very defensive, but it is not unreasonable: assuming the DBG file is well-formed, the way to indicate the absence of a COFF Symbol Table is simply to omit the corresponding entry in the debug directory.

Rather than pursue some possibility that depends on the input file being corrupt, let’s look instead at executables. For these, DUMPBIN works from the file header, specifically from the PointerToSymbolTable and NumberOfSymbols members. If both are non-zero, DUMPBIN loads the table from the executable. Note however that Microsoft’s documentation of the IMAGE_FILE_HEADER structure provides that if the COFF Symbol Table has been separated from the executable into a DBG file, then PointerToSymbolTable is zero. It does not insist that NumberOfSymbols also be zero. This is what gives us the steps to trigger the fault.

Reproduction

We need as an input file an executable that was built to have COFF symbols, which have then been separated to a DBG file, by a tool that cleared the PointerToSymbolTable member of the file header but left the NumberOfSymbols member alone. We also need, as above, that DUMPBIN have no access to any matching PDB file that can supply a Section Contribution Map.

Running dumpbin /map with such an executable as input is enough to induce the DumpSymbolMap function to try reading the COFF Symbol Table itself, one entry at a time. Note however that although the COFF Symbol Table was removed to a DBG file, DUMPBIN does not try to read from the there, but still from the executable. Moreover, it reads from wherever the file pointer was left before the DumpSymbolMap function was called. In effect, when DumpSymbolMap searches for a qualifying entry in what it thinks is the COFF Symbol Table, what it searches is not quite random data. We therefore need that the NumberOfSymbols be fairly large, to give a good chance that DumpSymbolMap will happen to hit on data that looks like a qualifying entry in what DumpSymbolMap thinks is a COFF Symbol Table.

Suitable executables are actually numerous, but old. Recent versions of Microsoft’s programming tools do not emit COFF symbols. We have to go back further than Windows 2000 in the NT stream of operating systems that Microsoft calls Windows. For the Windows systems based on MS-DOS, Windows 95 is not ideal since the retail versions of most executables appear to have been built without symbols. I have chosen SHELL32.DLL as a “fairly large” executable. For the versions from NT 4.0 (including Service Packs 4, 5 and 6), Windows 98 (including Second Edition), Internet Explorer 4.0 and 4.01 (including Service Packs 1 and 2), running

dumpbin /map shell32.dll

produces the LNK1000 internal error cited above.