t8y2/dbx · error · PeFormatError

unterminated PE string

Error message

unterminated PE string

What it means

PeFormatError raised by _read_c_string when data.find(b"\0", offset) returns -1, meaning there is no NUL terminator between the given offset and end-of-file. The library throws it because an import DLL name is required to be a NUL-terminated ASCII string; running off the end indicates a corrupt import table or a bad RVA-to-offset mapping.

Source

Thrown at agents/scripts/validate_windows_pe_dependencies.py:29

def _read_u16(data: bytes, offset: int) -> int:
    if offset < 0 or offset + 2 > len(data):
        raise PeFormatError("unexpected end of PE file")
    return struct.unpack_from("<H", data, offset)[0]


def _read_u32(data: bytes, offset: int) -> int:
    if offset < 0 or offset + 4 > len(data):
        raise PeFormatError("unexpected end of PE file")
    return struct.unpack_from("<I", data, offset)[0]


def _read_c_string(data: bytes, offset: int) -> str:
    if offset < 0 or offset >= len(data):
        raise PeFormatError("PE string offset is outside the file")
    end = data.find(b"\0", offset)
    if end < 0:
        raise PeFormatError("unterminated PE string")
    try:
        return data[offset:end].decode("ascii")
    except UnicodeDecodeError as error:
        raise PeFormatError("PE import name is not ASCII") from error


def imported_dlls(path: Path) -> list[str]:
    data = path.read_bytes()
    if len(data) < 64 or data[:2] != b"MZ":
        raise PeFormatError("missing DOS header")

    pe_offset = _read_u32(data, 0x3C)
    if data[pe_offset : pe_offset + 4] != b"PE\0\0":
        raise PeFormatError("missing PE signature")

    section_count = _read_u16(data, pe_offset + 6)
    optional_header_size = _read_u16(data, pe_offset + 20)
    optional_header_offset = pe_offset + 24

View on GitHub (pinned to c0390bff16)

Solutions

  1. Re-download / rebuild the binary; a missing terminator usually means the file is truncated at the end.
  2. Check the file size against the size implied by the last section's PointerToRawData + SizeOfRawData; if smaller, the copy is incomplete.
  3. Catch PeFormatError and skip/fail the file in batch-validation pipelines instead of crashing.
  4. If you produce PE files yourself, ensure every import-name string is NUL-terminated and covered by a mapped section.

Example fix

// before
for path in paths:
    imports[path] = imported_dlls(path)  # aborts whole batch
// after
for path in paths:
    try:
        imports[path] = imported_dlls(path)
    except PeFormatError as error:
        print(f"skipping {path}: {error}")
Defensive patterns

Strategy: try-catch

Validate before calling

data = path.read_bytes()
if len(data) < 64 or data[:2] != b"MZ":
    raise SystemExit("not a PE")
if data.rstrip(b"\0")[-1:] not in (b"\0", b"") and len(data) < 0x400:
    print("warning: file looks truncated")

Type guard

def looks_truncated(path: Path, expected_min: int) -> bool:
    return path.stat().st_size < expected_min

Try / catch

try:
    imports = imported_dlls(path)
except PeFormatError as error:
    if "unterminated" in str(error) or "outside the file" in str(error):
        print(f"{path}: corrupt/truncated PE: {error}")
    else:
        raise

Prevention

When it happens

Trigger: imported_dlls() calls _read_c_string(data, rva_to_offset(name_rva)) and the resolved offset points at bytes that continue to EOF without a \0 — e.g. name_rva maps into the very tail of the file, or the last section's raw data was truncated mid-string.

Common situations: Parsing truncated downloads or incomplete writes of PE files, files whose final section was cut during packing/unpacking, deliberately corrupted malware samples, or concatenation errors where a PE was appended to another file and the tail is missing.

Related errors


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