t8y2/dbx · error · PeFormatError

PE import descriptor table is not terminated

Error message

PE import descriptor table is not terminated

What it means

A PE import descriptor array must end with an all-zero (20-byte) terminator. The parser walks at most 4096 descriptors; if it exhausts that bound without hitting the terminator, it raises this error. This guards against runaway or cyclic descriptor tables that would otherwise cause unbounded parsing.

Solutions

  1. Check the real import count with dumpbin /imports or pefile; if it exceeds 4096, raise the loop bound in the script.
  2. Verify the import directory RVA/size (data directory index 1) actually points at a descriptor array; a wrong RVA makes the terminator unreachable.
  3. Rebuild the binary with a standard linker; a missing terminator indicates a malformed or hand-patched import table.
  4. Treat the file as unverifiable: catch PeFormatError and fail the specific artifact rather than the whole batch.

Example fix

// before
for _ in range(4096):
    ...
raise PeFormatError("PE import descriptor table is not terminated")
// after
max_descriptors = import_directory_size // 20
for _ in range(max_descriptors):
    ...
Defensive patterns

Strategy: validation

Validate before calling

import pefile

def import_count_within_limits(path, max_descriptors: int = 4096) -> bool:
    pe = pefile.PE(path)
    return len(getattr(pe, "DIRECTORY_ENTRY_IMPORT", [])) <= max_descriptors

Try / catch

try:
    imports = imported_dlls(path)
except PeFormatError as err:
    if "not terminated" in str(err):
        fail_artifact(path, "unterminated or oversized import table")
    else:
        raise

Prevention

When it happens

Trigger: imported_dlls(path) on a PE with more than 4096 import descriptors, or with a descriptor array that never contains the all-zero terminator (corrupt/forged import directory RVA pointing at non-descriptor data, or a crafted cyclic table).

Common situations: Extremely large binaries linking thousands of DLLs (rare but possible in monolithic builds); deliberately malformed PE samples; import directory RVA pointing into unrelated data so terminator bytes never appear; automated fuzzing corpora.

Related errors


AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05). Data as JSON: /api/errors/2ddb7175ad4cdce3. Report an issue: GitHub.

Appendix: source

Thrown at agents/scripts/validate_windows_pe_dependencies.py:101

                    break
                return raw_offset + delta
        raise PeFormatError(f"PE RVA 0x{rva:x} is not backed by file data")

    descriptor_offset = rva_to_offset(import_directory_rva)
    imports = []
    for _ in range(4096):
        if descriptor_offset + 20 > len(data):
            raise PeFormatError("truncated PE import descriptor")
        descriptor = data[descriptor_offset : descriptor_offset + 20]
        if descriptor == b"\0" * 20:
            return sorted(set(imports), key=str.casefold)
        name_rva = _read_u32(data, descriptor_offset + 12)
        if name_rva == 0:
            raise PeFormatError("PE import descriptor has no DLL name")
        imports.append(_read_c_string(data, rva_to_offset(name_rva)))
        descriptor_offset += 20

    raise PeFormatError("PE import descriptor table is not terminated")


def forbidden_msvc_runtime_dlls(imports: list[str]) -> list[str]:
    return sorted(
        {name for name in imports if name.casefold().startswith(("msvcp", "vcruntime"))},
        key=str.casefold,
    )


def main() -> int:
    parser = argparse.ArgumentParser(description="Reject Windows PE files that require the Visual C++ runtime")
    parser.add_argument("binary", type=Path)
    args = parser.parse_args()

    try:
        imports = imported_dlls(args.binary)
    except (OSError, PeFormatError) as error:
        parser.error(str(error))

View on GitHub (pinned to c0390bff16)