Skip to content

Delay-load imports: enumeration from IAT vs INT when lengths differ - intended behaviour? #9375

Description

@malx-labs

When a delay-load descriptor's Import Name Table (INT) and Import Address Table (IAT) have different lengths, Ghidra appears to enumerate imports from the IAT, where several other PE tools enumerate from the INT. I'd like to understand whether this is a deliberate design choice, and how the loader is intended to handle an INT/IAT length mismatch.

Observed behaviour

Testing with a minimal PE32+ binary whose single delay-load descriptor has:

  • INT: three entries (Foo, Bar, Baz) + null terminator
  • IAT: two entries (Foo, Bar) + null terminator

Ghidra 12.1 (headless, default analysis) enumerates two delay-load imports:

DDI: delay_dir_parsed=true datatype=ImgDelayDescr
DDI: external_library=TEST.DLL
DDI: external_symbol=TEST.DLL!Foo
DDI: external_symbol=TEST.DLL!Bar
DDI: external_symbol_count=2

(Output from a small postscript walking getSymbolTable().getExternalSymbols().)

Note: datatype=ImgDelayDescr - Ghidra parsed the descriptor structure cleanly; the divergence is purely in which array it uses for the import count, not a parse failure.

For comparison, on the same binary:

  • dumpbin 14.44 enumerates 3 (from INT; the third shown with a null IAT address)
  • pefile 2024.8.26 enumerates 3 (from INT)

So Ghidra reports two imports where dumpbin and pefile report three. The difference appears to be that Ghidra walks the IAT to determine the import count, while the others walk the INT.

Why I'm asking

I'm studying how PE parsers handle malformed/edge-case delay-load structures, using single-anomaly test binaries (this one differs from a well-formed binary only in the INT/IAT length mismatch; a well-formed control with matching INT/IAT is enumerated identically by all tools, Ghidra included all 3 imports).

The PE specification doesn't state which array is authoritative when the two disagree, so I don't think any tool is strictly wrong here. I'm trying to understand the different implementation choices. Ghidra's choice to key off the IAT is defensible; I'd just like to know:

  1. Is IAT-based enumeration intentional for delay-load imports, or a side effect of how the loader walks the descriptor?
  2. How does Ghidra intend to handle an INT/IAT length mismatch; is it treated as malformed, or is one array simply taken as authoritative by design?
  3. Would it be useful for Ghidra to surface the mismatch (e.g., a bookmark or log warning) when the arrays disagree, given that the divergence is silent today?

Not a bug report

To be clear, I'm not reporting this as a defect. Ghidra parses the descriptor cleanly and produces a reasonable result. On a well-formed binary Ghidra's output matches the other tools exactly. This is a question about intended behaviour on a specifically malformed edge case, which the PE spec underspecifies. I've also raised the underlying spec ambiguity with the PE format documentation: MicrosoftDocs/win32#2202

Reproduction

Minimal PE32+ test binary and the headless dump postscript are available at https://github.com/malx-labs/pe-delayload-divergence. Happy to provide the fixture directly if useful. Ghidra version 12.1 PUBLIC, default headless analysis options.

Thanks, genuinely curious how the loader team thinks about this case.

Metadata

Metadata

Assignees

Labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions