Files
TheRock/rocm-systems/shared/kpack/python/rocm_kpack/elf/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

470 lines
15 KiB
Python

"""
ELF verification utilities.
The ElfVerifier class provides structural validation for ELF 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
import tempfile
from pathlib import Path
from typing import Callable
from ..verify import VerificationResult, BinaryVerifier
from .types import (
PT_LOAD,
PT_PHDR,
SHT_NOBITS,
PAGE_SIZE,
)
from .surgery import ElfSurgery, ProgramHeader
class ElfVerifier(BinaryVerifier):
"""ELF binary verification.
Checks for common structural issues that cause problems at runtime
or with tools like strip, gdb, objcopy.
Usage:
result = ElfVerifier.verify(Path("libfoo.so"))
if not result.passed:
print(result)
"""
def __init__(self, surgery: ElfSurgery):
"""Initialize with an ElfSurgery instance.
Args:
surgery: Parsed ELF binary
"""
self._surgery = surgery
@classmethod
def verify(cls, path: Path) -> VerificationResult:
"""Verify an ELF file on disk.
Args:
path: Path to ELF binary
Returns:
VerificationResult with any errors/warnings
"""
surgery = ElfSurgery.load(path)
verifier = cls(surgery)
return verifier.run_all_checks()
@classmethod
def verify_data(cls, data: bytes | bytearray) -> VerificationResult:
"""Verify ELF data in memory.
Args:
data: ELF binary data
Returns:
VerificationResult with any errors/warnings
"""
surgery = ElfSurgery(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_load_segments,
self.check_phdr_alignment,
self.check_section_alignment,
self.check_string_table_bounds,
self.check_section_header_offsets,
self.check_phdr_file_offset_consistency,
]
for check in checks:
result.merge(check())
return result
# =========================================================================
# Structural Checks
# =========================================================================
def check_no_overlapping_load_segments(self) -> VerificationResult:
"""Check that no two PT_LOAD segments share file offsets.
This catches the NOBITS segment offset collision bug where two
segments had the same p_offset value.
"""
result = VerificationResult()
# Collect all PT_LOAD segments with non-zero filesz
load_segments: list[tuple[int, ProgramHeader]] = []
for idx, phdr in self._surgery.iter_load_segments():
load_segments.append((idx, phdr))
# Check for overlaps
for i, (idx1, phdr1) in enumerate(load_segments):
for idx2, phdr2 in load_segments[i + 1 :]:
# Check file offset overlap (only for segments with file content)
if phdr1.p_filesz > 0 and phdr2.p_filesz > 0:
# Two segments overlap if their file ranges intersect
if (
phdr1.p_offset < phdr2.p_offset + phdr2.p_filesz
and phdr2.p_offset < phdr1.p_offset + phdr1.p_filesz
):
result.add_error(
f"PT_LOAD segments {idx1} and {idx2} have overlapping "
f"file ranges: [{phdr1.p_offset:#x}, {phdr1.end_offset:#x}) "
f"and [{phdr2.p_offset:#x}, {phdr2.end_offset:#x})"
)
# Special case: two segments with same offset and at least one has filesz=0
# This is the NOBITS bug - dynamic linker gets confused
if phdr1.p_offset == phdr2.p_offset:
if phdr1.p_filesz == 0 or phdr2.p_filesz == 0:
result.add_error(
f"PT_LOAD segments {idx1} and {idx2} share file offset "
f"{phdr1.p_offset:#x} (NOBITS collision)"
)
return result
def check_phdr_alignment(self) -> VerificationResult:
"""Check PT_LOAD segments satisfy mmap alignment requirements.
For mmap to work: (p_offset % PAGE_SIZE) == (p_vaddr % PAGE_SIZE)
"""
result = VerificationResult()
page_size = self._surgery.page_size
for idx, phdr in self._surgery.iter_load_segments():
offset_mod = phdr.p_offset % page_size
vaddr_mod = phdr.p_vaddr % page_size
if offset_mod != vaddr_mod:
result.add_error(
f"PT_LOAD segment {idx} has misaligned offset/vaddr: "
f"offset=0x{phdr.p_offset:x} ({offset_mod:#x} mod PAGE_SIZE), "
f"vaddr=0x{phdr.p_vaddr:x} ({vaddr_mod:#x} mod PAGE_SIZE)"
)
return result
def check_section_alignment(self) -> VerificationResult:
"""Check section alignment constraints."""
result = VerificationResult()
for section in self._surgery.iter_sections():
shdr = section.header
if shdr.sh_addralign > 1:
# Check offset alignment
if shdr.sh_offset % shdr.sh_addralign != 0:
result.add_warning(
f"Section {section.name} offset 0x{shdr.sh_offset:x} "
f"not aligned to {shdr.sh_addralign}"
)
# Check address alignment for ALLOC sections
if shdr.is_alloc and shdr.sh_addr % shdr.sh_addralign != 0:
result.add_warning(
f"Section {section.name} address 0x{shdr.sh_addr:x} "
f"not aligned to {shdr.sh_addralign}"
)
return result
def check_string_table_bounds(self) -> VerificationResult:
"""Check that string table references are within bounds.
This catches the .dynstr corruption issue where string offsets
pointed outside the string table.
"""
result = VerificationResult()
# Check section header string table
ehdr = self._surgery.ehdr
if ehdr.e_shstrndx < len(self._surgery._shdrs):
shstrtab = self._surgery._shdrs[ehdr.e_shstrndx]
for section in self._surgery.iter_sections():
name_offset = section.header.sh_name
if name_offset >= shstrtab.sh_size:
result.add_error(
f"Section header {section.index} name offset "
f"{name_offset} exceeds .shstrtab size {shstrtab.sh_size}"
)
# Find .dynstr and check it exists for dynamic binaries
dynstr = self._surgery.find_section(".dynstr")
if dynstr is not None:
# Verify .dynstr content ends with null
try:
content = self._surgery.get_section_content(dynstr)
if content and content[-1] != 0:
result.add_warning(".dynstr does not end with null terminator")
except ValueError:
pass # NOBITS section, can't check
return result
def check_section_header_offsets(self) -> VerificationResult:
"""Check that section header table is at a valid offset.
The section header table must not overlap with content.
"""
result = VerificationResult()
ehdr = self._surgery.ehdr
shdr_start = ehdr.e_shoff
shdr_end = ehdr.e_shoff + ehdr.e_shnum * ehdr.e_shentsize
# Check against PHDR table
phdr_start = ehdr.e_phoff
phdr_end = ehdr.e_phoff + ehdr.e_phnum * ehdr.e_phentsize
if shdr_start < phdr_end and phdr_start < shdr_end:
result.add_error(
f"Section header table [{shdr_start:#x}, {shdr_end:#x}) "
f"overlaps with program header table [{phdr_start:#x}, {phdr_end:#x})"
)
# Check against sections
for section in self._surgery.iter_sections():
if section.header.sh_type == SHT_NOBITS:
continue
sect_start = section.header.sh_offset
sect_end = section.header.end_offset
if shdr_start < sect_end and sect_start < shdr_end:
if section.name != ".shstrtab": # shstrtab often abuts section headers
result.add_warning(
f"Section header table [{shdr_start:#x}, {shdr_end:#x}) "
f"may overlap with section {section.name} "
f"[{sect_start:#x}, {sect_end:#x})"
)
return result
def check_phdr_file_offset_consistency(self) -> VerificationResult:
"""Check that PHDR table location matches PT_PHDR segment.
If a PT_PHDR segment exists, its offset/vaddr should match
the actual program header table location.
"""
result = VerificationResult()
ehdr = self._surgery.ehdr
for idx, phdr in self._surgery.iter_program_headers():
if phdr.p_type == PT_PHDR:
if phdr.p_offset != ehdr.e_phoff:
result.add_error(
f"PT_PHDR offset {phdr.p_offset:#x} doesn't match "
f"e_phoff {ehdr.e_phoff:#x}"
)
expected_filesz = ehdr.e_phnum * ehdr.e_phentsize
if phdr.p_filesz != expected_filesz:
result.add_warning(
f"PT_PHDR filesz {phdr.p_filesz} doesn't match "
f"expected {expected_filesz} (e_phnum * e_phentsize)"
)
break
return result
# =============================================================================
# External Tool Verification
# =============================================================================
def verify_with_readelf(path: Path) -> VerificationResult:
"""Run readelf -a and check for warnings/errors.
Args:
path: Path to ELF binary
Returns:
VerificationResult
"""
result = VerificationResult()
try:
proc = subprocess.run(
["readelf", "-a", str(path)],
capture_output=True,
text=True,
)
except FileNotFoundError:
result.add_error("readelf not found")
return result
# Check for common warning patterns
output = proc.stdout + proc.stderr
warning_patterns = [
"corrupt",
"invalid",
"warning:",
"Warning:",
"ERROR:",
"truncated",
"out of range",
"overlaps",
]
for line in output.splitlines():
for pattern in warning_patterns:
if pattern.lower() in line.lower():
result.add_error(f"readelf: {line.strip()}")
break
if proc.returncode != 0:
result.add_warning(f"readelf exited with code {proc.returncode}")
return result
def verify_with_strip(path: Path, tmp_dir: Path | None = None) -> VerificationResult:
"""Test that strip doesn't corrupt the binary.
Args:
path: Path to ELF binary
tmp_dir: Directory for temporary output (uses system temp if None)
Returns:
VerificationResult
"""
result = VerificationResult()
# Create temp file for stripped output
if tmp_dir is None:
tmp_dir = Path(tempfile.gettempdir())
stripped_path = tmp_dir / f"{path.name}.stripped"
try:
proc = subprocess.run(
["strip", "-o", str(stripped_path), str(path)],
capture_output=True,
text=True,
)
except FileNotFoundError:
result.add_error("strip not found")
return result
finally:
# Clean up temp file
if stripped_path.exists():
stripped_path.unlink()
if proc.returncode != 0:
result.add_error(f"strip failed: {proc.stderr.strip()}")
if proc.stderr:
for line in proc.stderr.splitlines():
if "warning" in line.lower() or "error" in line.lower():
result.add_warning(f"strip: {line.strip()}")
return result
def verify_with_gdb(path: Path) -> VerificationResult:
"""Check for GDB warnings about the binary.
Args:
path: Path to ELF binary
Returns:
VerificationResult
"""
result = VerificationResult()
try:
proc = subprocess.run(
["gdb", "-batch", "-ex", "info files", str(path)],
capture_output=True,
text=True,
)
except FileNotFoundError:
result.add_error("gdb not found")
return result
# Check for warning patterns
output = proc.stdout + proc.stderr
warning_patterns = [
"corrupt",
"invalid",
"warning:",
"Warning:",
]
for line in output.splitlines():
for pattern in warning_patterns:
if pattern in line:
result.add_error(f"gdb: {line.strip()}")
break
return result
def verify_with_ldd(path: Path) -> VerificationResult:
"""Check that ldd can process the binary.
Args:
path: Path to ELF binary (should be shared library or executable)
Returns:
VerificationResult
"""
result = VerificationResult()
try:
proc = subprocess.run(
["ldd", str(path)],
capture_output=True,
text=True,
)
except FileNotFoundError:
result.add_error("ldd not found")
return result
# ldd failures are not necessarily errors (static binaries, etc)
if "not a dynamic executable" in proc.stdout + proc.stderr:
result.add_warning("ldd: not a dynamic executable")
elif proc.returncode != 0:
result.add_warning(f"ldd exited with code {proc.returncode}")
return result
def verify_all(path: Path, tmp_dir: Path | None = None) -> VerificationResult:
"""Run all verification checks on an ELF binary.
Args:
path: Path to ELF binary
tmp_dir: Directory for temporary files
Returns:
Combined VerificationResult
"""
result = VerificationResult()
# Internal structural checks
internal = ElfVerifier.verify(path)
result.merge(internal)
# External tool checks
result.merge(verify_with_readelf(path))
result.merge(verify_with_strip(path, tmp_dir))
# gdb and ldd are Linux-specific and won't work correctly on Windows
# (even if available via MSYS2, they expect Windows binaries)
if sys.platform != "win32":
result.merge(verify_with_gdb(path))
result.merge(verify_with_ldd(path))
return result