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.

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)

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.