Files
TheRock/rocm-systems/shared/kpack/python/rocm_kpack/coff/verify.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

368 lines
11 KiB
Python

"""
PE/COFF verification utilities.
The CoffVerifier class provides structural validation for PE binaries,
catching common issues that would cause problems at runtime or with tools.
This includes both internal structural checks and external tool invocation.
"""
import subprocess
import sys
from pathlib import Path
from typing import Callable
from ..verify import VerificationResult, BinaryVerifier
from .types import (
IMAGE_DIRECTORY_ENTRY_BASERELOC,
IMAGE_DIRECTORY_ENTRY_IMPORT,
IMAGE_DIRECTORY_ENTRY_EXPORT,
)
from .surgery import CoffSurgery
class CoffVerifier(BinaryVerifier):
"""PE/COFF binary verification.
Checks for common structural issues that cause problems at runtime
or with tools like llvm-objdump, dumpbin, etc.
Usage:
result = CoffVerifier.verify(Path("foo.dll"))
if not result.passed:
print(result)
"""
def __init__(self, surgery: CoffSurgery):
"""Initialize with a CoffSurgery instance.
Args:
surgery: Parsed PE binary
"""
self._surgery = surgery
@classmethod
def verify(cls, path: Path) -> VerificationResult:
"""Verify a PE file on disk.
Args:
path: Path to PE binary
Returns:
VerificationResult with any errors/warnings
"""
surgery = CoffSurgery.load(path)
verifier = cls(surgery)
return verifier.run_all_checks()
@classmethod
def verify_data(cls, data: bytes | bytearray) -> VerificationResult:
"""Verify PE data in memory.
Args:
data: PE binary data
Returns:
VerificationResult with any errors/warnings
"""
surgery = CoffSurgery(bytearray(data))
verifier = cls(surgery)
return verifier.run_all_checks()
def run_all_checks(self) -> VerificationResult:
"""Run all internal structural checks."""
result = VerificationResult()
# Run each check and merge results
checks: list[Callable[[], VerificationResult]] = [
self.check_no_overlapping_sections,
self.check_section_alignment,
self.check_section_offsets_in_bounds,
self.check_data_directory_bounds,
self.check_size_of_image,
self.check_file_alignment,
]
for check in checks:
result.merge(check())
return result
# =========================================================================
# Structural Checks
# =========================================================================
def check_no_overlapping_sections(self) -> VerificationResult:
"""Check that no two sections have overlapping file regions.
This catches corruption where section raw data overlaps.
"""
result = VerificationResult()
sections = list(self._surgery.iter_sections())
for i, sect1 in enumerate(sections):
if sect1.raw_size == 0:
continue
start1 = sect1.file_offset
end1 = start1 + sect1.raw_size
for sect2 in sections[i + 1 :]:
if sect2.raw_size == 0:
continue
start2 = sect2.file_offset
end2 = start2 + sect2.raw_size
# Check for overlap
if start1 < end2 and start2 < end1:
result.add_error(
f"Sections {sect1.name} and {sect2.name} have overlapping "
f"file ranges: [{start1:#x}, {end1:#x}) and [{start2:#x}, {end2:#x})"
)
return result
def check_section_alignment(self) -> VerificationResult:
"""Check section alignment constraints.
- PointerToRawData must be aligned to FileAlignment
- VirtualAddress must be aligned to SectionAlignment
"""
result = VerificationResult()
file_align = self._surgery.file_alignment
sect_align = self._surgery.section_alignment
for section in self._surgery.iter_sections():
# Check file offset alignment (if section has raw data)
if section.raw_size > 0:
if section.file_offset % file_align != 0:
result.add_error(
f"Section {section.name} PointerToRawData 0x{section.file_offset:x} "
f"not aligned to FileAlignment 0x{file_align:x}"
)
# Check RVA alignment
if section.rva % sect_align != 0:
result.add_warning(
f"Section {section.name} VirtualAddress 0x{section.rva:x} "
f"not aligned to SectionAlignment 0x{sect_align:x}"
)
return result
def check_section_offsets_in_bounds(self) -> VerificationResult:
"""Check that section raw data is within file bounds."""
result = VerificationResult()
file_size = len(self._surgery.data)
for section in self._surgery.iter_sections():
if section.raw_size == 0:
continue
end_offset = section.file_offset + section.raw_size
if end_offset > file_size:
result.add_error(
f"Section {section.name} raw data extends beyond file: "
f"ends at 0x{end_offset:x}, file size is 0x{file_size:x}"
)
return result
def check_data_directory_bounds(self) -> VerificationResult:
"""Check that data directories point to valid locations."""
result = VerificationResult()
# Check key data directories
for idx, name in [
(IMAGE_DIRECTORY_ENTRY_EXPORT, "Export"),
(IMAGE_DIRECTORY_ENTRY_IMPORT, "Import"),
(IMAGE_DIRECTORY_ENTRY_BASERELOC, "BaseReloc"),
]:
dd = self._surgery.get_data_directory(idx)
if dd is None or not dd.is_present:
continue
# Check RVA is valid (can be converted to file offset)
offset = self._surgery.rva_to_file_offset(dd.VirtualAddress)
if offset is None:
result.add_error(
f"{name} directory RVA 0x{dd.VirtualAddress:x} "
"not in any section"
)
continue
# Check size is within section
end_rva = dd.VirtualAddress + dd.Size
end_offset = self._surgery.rva_to_file_offset(end_rva - 1)
if end_offset is None and dd.Size > 0:
result.add_warning(
f"{name} directory end RVA 0x{end_rva:x} " "extends beyond section"
)
return result
def check_size_of_image(self) -> VerificationResult:
"""Check SizeOfImage covers all sections."""
result = VerificationResult()
size_of_image = self._surgery.optional_header.SizeOfImage
sect_align = self._surgery.section_alignment
# Find max section end RVA
max_rva = 0
for section in self._surgery.iter_sections():
end_rva = section.rva + section.virtual_size
max_rva = max(max_rva, end_rva)
# SizeOfImage should be >= max_rva, aligned to SectionAlignment
expected_min = max_rva
if expected_min % sect_align != 0:
expected_min = (expected_min // sect_align + 1) * sect_align
if size_of_image < expected_min:
result.add_error(
f"SizeOfImage 0x{size_of_image:x} is smaller than "
f"required 0x{expected_min:x} to cover all sections"
)
return result
def check_file_alignment(self) -> VerificationResult:
"""Check FileAlignment is a power of 2 between 512 and 64K."""
result = VerificationResult()
file_align = self._surgery.file_alignment
# Must be power of 2
if file_align == 0 or (file_align & (file_align - 1)) != 0:
result.add_error(f"FileAlignment 0x{file_align:x} is not a power of 2")
elif file_align < 512:
result.add_warning(
f"FileAlignment 0x{file_align:x} is smaller than standard 512"
)
elif file_align > 65536:
result.add_warning(
f"FileAlignment 0x{file_align:x} is larger than standard 64K"
)
return result
# =============================================================================
# External Tool Verification
# =============================================================================
def verify_with_llvm_objdump(
path: Path,
llvm_objdump: Path | str = "llvm-objdump",
) -> VerificationResult:
"""Run llvm-objdump and check for warnings/errors.
Args:
path: Path to PE binary
llvm_objdump: Path to llvm-objdump executable
Returns:
VerificationResult
"""
result = VerificationResult()
try:
proc = subprocess.run(
[str(llvm_objdump), "-h", str(path)],
capture_output=True,
text=True,
)
except FileNotFoundError:
result.add_warning("llvm-objdump not found")
return result
# Check for warning patterns
output = proc.stdout + proc.stderr
warning_patterns = [
"corrupt",
"invalid",
"warning:",
"Warning:",
"error:",
"Error:",
"truncated",
]
for line in output.splitlines():
for pattern in warning_patterns:
if pattern.lower() in line.lower():
result.add_error(f"llvm-objdump: {line.strip()}")
break
if proc.returncode != 0:
result.add_error(f"llvm-objdump exited with code {proc.returncode}")
return result
def verify_with_dumpbin(path: Path) -> VerificationResult:
"""Run dumpbin (Windows SDK) and check for warnings/errors.
Args:
path: Path to PE binary
Returns:
VerificationResult
"""
result = VerificationResult()
try:
proc = subprocess.run(
["dumpbin", "/headers", str(path)],
capture_output=True,
text=True,
)
except FileNotFoundError:
result.add_warning("dumpbin not found (Windows SDK tool)")
return result
if proc.returncode != 0:
result.add_error(f"dumpbin exited with code {proc.returncode}")
# Check for error patterns
output = proc.stdout + proc.stderr
if "fatal error" in output.lower():
result.add_error("dumpbin reported fatal error")
return result
def verify_all(
path: Path,
llvm_objdump: Path | str = "llvm-objdump",
) -> VerificationResult:
"""Run all verification checks on a PE binary.
Args:
path: Path to PE binary
llvm_objdump: Path to llvm-objdump executable
Returns:
Combined VerificationResult
"""
result = VerificationResult()
# Internal structural checks
internal = CoffVerifier.verify(path)
result.merge(internal)
# External tool checks
result.merge(verify_with_llvm_objdump(path, llvm_objdump))
# dumpbin is Windows-only (part of MSVC/Windows SDK)
if sys.platform == "win32":
result.merge(verify_with_dumpbin(path))
return result