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
470 lines
15 KiB
Python
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
|