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

897 lines
30 KiB
Python

"""
High-level PE/COFF surgery interface.
The CoffSurgery class provides a clean abstraction for PE/COFF binary modifications.
It handles the complexity of maintaining PE invariants while exposing
simple high-level operations.
Design principles:
- Parse once, modify in memory, write once
- Track all modifications for verification
- Fail fast with clear error messages
- Compose well with other utilities
Key differences from ELF:
- No program headers (sections define memory layout via characteristics)
- Uses ImageBase + RVA for virtual addresses
- Base relocations instead of RELA sections
- Section names limited to 8 characters
"""
import os
import struct
from dataclasses import dataclass, field
from pathlib import Path
from typing import Iterator
from .types import (
DosHeader,
CoffHeader,
OptionalHeader64,
SectionHeader,
DataDirectory,
BaseRelocationBlock,
BaseRelocationEntry,
PE_SIGNATURE,
DOS_HEADER_SIZE,
COFF_HEADER_SIZE,
SECTION_HEADER_SIZE,
DATA_DIRECTORY_SIZE,
IMAGE_DIRECTORY_ENTRY_BASERELOC,
IMAGE_REL_BASED_DIR64,
IMAGE_REL_BASED_ABSOLUTE,
IMAGE_SCN_MEM_READ,
IMAGE_SCN_CNT_INITIALIZED_DATA,
round_up_to_alignment,
section_name_to_bytes,
)
@dataclass
class SectionInfo:
"""Information about a section, combining header with derived data."""
index: int
name: str
header: SectionHeader
@property
def rva(self) -> int:
"""Relative virtual address."""
return self.header.VirtualAddress
@property
def virtual_size(self) -> int:
"""Size in memory."""
return self.header.VirtualSize
@property
def file_offset(self) -> int:
"""File offset of raw data."""
return self.header.PointerToRawData
@property
def raw_size(self) -> int:
"""Size in file."""
return self.header.SizeOfRawData
@dataclass
class Modification:
"""Record of a modification made to the PE binary."""
operation: str # e.g., "write_bytes", "update_section"
file_offset: int
size: int
description: str
@dataclass
class RelocationInfo:
"""Information about a found base relocation entry."""
block_rva: int # Page RVA for the block
entry_offset: int # File offset of the entry
target_rva: int # Full RVA being relocated
entry: BaseRelocationEntry
class CoffSurgery:
"""High-level interface for PE/COFF binary modifications.
Usage:
surgery = CoffSurgery.load(Path("foo.dll"))
# Query operations
section = surgery.find_section(".hip_fat")
offset = surgery.rva_to_file_offset(0x1000)
# Modification operations
surgery.write_bytes_at_offset(0x1000, b"\\x00" * 100)
# Save and verify
surgery.save(Path("foo_modified.dll"))
"""
# Reasonable limits for PE structures to prevent DoS from malformed files
MAX_NUMBER_OF_SECTIONS = 256
MAX_NUMBER_OF_DATA_DIRECTORIES = 64
def __init__(
self,
data: bytearray,
path: Path | None = None,
):
"""Initialize with binary data.
Prefer using CoffSurgery.load() for most use cases.
Args:
data: Mutable PE binary data
path: Original file path (for error messages)
"""
self._data = data
self._path = path
self._modifications: list[Modification] = []
# Parse structures
self._dos_hdr = DosHeader.from_bytes(data)
self._pe_offset = self._dos_hdr.e_lfanew
# Validate PE offset is within bounds
if self._pe_offset < 0 or self._pe_offset + 4 > len(data):
raise ValueError(
f"Invalid PE header offset {self._pe_offset:#x}: "
f"must be within file bounds (0 to {len(data) - 4})"
)
# Verify PE signature
pe_sig = bytes(data[self._pe_offset : self._pe_offset + 4])
if pe_sig != PE_SIGNATURE:
raise ValueError(f"Invalid PE signature: {pe_sig!r}")
# Parse COFF header (after PE signature)
coff_offset = self._pe_offset + 4
self._coff_hdr = CoffHeader.from_bytes(data, coff_offset)
# Parse optional header
opt_offset = coff_offset + COFF_HEADER_SIZE
self._opt_hdr = OptionalHeader64.from_bytes(data, opt_offset)
# Parse data directories
data_dir_offset = opt_offset + self._opt_hdr.SIZE
self._data_dirs = self._parse_data_directories(data_dir_offset)
# Parse section headers
section_offset = opt_offset + self._coff_hdr.SizeOfOptionalHeader
self._sections = self._parse_sections(section_offset)
@classmethod
def load(cls, path: Path) -> "CoffSurgery":
"""Load a PE binary from file.
Args:
path: Path to PE binary
Returns:
CoffSurgery instance ready for modifications
"""
data = bytearray(path.read_bytes())
return cls(data, path)
@staticmethod
def has_fatbin_section(file_path: Path) -> bool:
"""Fast check for .hip_fat section without loading the full binary.
Reads only the DOS header, PE signature, COFF header, and section
headers — a few KB total even for large binaries.
Returns False for non-PE files (wrong magic).
Raises on I/O errors or corrupt PE headers.
"""
with open(file_path, "rb") as f:
dos = f.read(DOS_HEADER_SIZE)
if len(dos) < DOS_HEADER_SIZE:
return False
if dos[:2] != b"MZ":
return False
pe_offset = struct.unpack_from("<I", dos, 0x3C)[0]
f.seek(pe_offset)
pe_sig = f.read(4)
if pe_sig != PE_SIGNATURE:
return False
# COFF header: NumberOfSections at offset 2
coff = f.read(COFF_HEADER_SIZE)
if len(coff) < COFF_HEADER_SIZE:
return False
num_sections = struct.unpack_from("<H", coff, 2)[0]
opt_hdr_size = struct.unpack_from("<H", coff, 16)[0]
# Section headers start after optional header
section_offset = pe_offset + 4 + COFF_HEADER_SIZE + opt_hdr_size
f.seek(section_offset)
section_data = f.read(num_sections * SECTION_HEADER_SIZE)
if len(section_data) < num_sections * SECTION_HEADER_SIZE:
return False
# Check each section name for ".hip_fat"
target = b".hip_fat"
for i in range(num_sections):
name = section_data[
i * SECTION_HEADER_SIZE : i * SECTION_HEADER_SIZE + 8
]
# Section names are 8 bytes, null-padded
if name == target or name.rstrip(b"\x00") == target:
return True
return False
# =========================================================================
# Properties
# =========================================================================
@property
def data(self) -> bytearray:
"""Access to raw binary data (mutable)."""
return self._data
@property
def dos_header(self) -> DosHeader:
"""DOS header."""
return self._dos_hdr
@property
def coff_header(self) -> CoffHeader:
"""COFF file header."""
return self._coff_hdr
@property
def optional_header(self) -> OptionalHeader64:
"""PE32+ optional header."""
return self._opt_hdr
@property
def modifications(self) -> list[Modification]:
"""List of modifications made."""
return self._modifications
@property
def image_base(self) -> int:
"""Preferred load address."""
return self._opt_hdr.ImageBase
@property
def file_alignment(self) -> int:
"""File alignment for raw data."""
return self._opt_hdr.FileAlignment
@property
def section_alignment(self) -> int:
"""Section alignment in memory."""
return self._opt_hdr.SectionAlignment
@property
def is_dll(self) -> bool:
"""Check if this is a DLL."""
return self._coff_hdr.is_dll
@property
def has_aslr(self) -> bool:
"""Check if ASLR is enabled."""
return self._opt_hdr.has_aslr
# =========================================================================
# Parsing (internal)
# =========================================================================
def _parse_data_directories(self, offset: int) -> list[DataDirectory]:
"""Parse all data directories."""
num_dirs = self._opt_hdr.NumberOfRvaAndSizes
if num_dirs > self.MAX_NUMBER_OF_DATA_DIRECTORIES:
raise ValueError(
f"NumberOfRvaAndSizes ({num_dirs}) exceeds maximum "
f"({self.MAX_NUMBER_OF_DATA_DIRECTORIES})"
)
dirs = []
for i in range(num_dirs):
dir_offset = offset + i * DATA_DIRECTORY_SIZE
dirs.append(DataDirectory.from_bytes(self._data, dir_offset))
return dirs
def _parse_sections(self, offset: int) -> list[SectionHeader]:
"""Parse all section headers."""
num_sections = self._coff_hdr.NumberOfSections
if num_sections > self.MAX_NUMBER_OF_SECTIONS:
raise ValueError(
f"NumberOfSections ({num_sections}) exceeds maximum "
f"({self.MAX_NUMBER_OF_SECTIONS})"
)
sections = []
for i in range(num_sections):
sect_offset = offset + i * SECTION_HEADER_SIZE
sections.append(SectionHeader.from_bytes(self._data, sect_offset))
return sections
# =========================================================================
# Query Operations
# =========================================================================
def find_section(self, name: str) -> SectionInfo | None:
"""Find a section by name.
Args:
name: Section name (e.g., ".hip_fat", ".hipFatB")
Can be the full name or truncated 8-char version.
Returns:
SectionInfo if found, None otherwise
"""
# Normalize name for comparison
search_name = name[:8] if len(name) > 8 else name
for idx, shdr in enumerate(self._sections):
if shdr.name_str == search_name:
return SectionInfo(
index=idx,
name=shdr.name_str,
header=shdr,
)
return None
def get_section_by_index(self, index: int) -> SectionInfo | None:
"""Get section by index."""
if 0 <= index < len(self._sections):
shdr = self._sections[index]
return SectionInfo(
index=index,
name=shdr.name_str,
header=shdr,
)
return None
def iter_sections(self) -> Iterator[SectionInfo]:
"""Iterate over all sections."""
for idx, shdr in enumerate(self._sections):
yield SectionInfo(
index=idx,
name=shdr.name_str,
header=shdr,
)
def get_section_content(self, section: SectionInfo | str) -> bytes:
"""Get content of a section.
Args:
section: SectionInfo or section name
Returns:
Section content bytes
Raises:
ValueError: If section not found or has no raw data
"""
if isinstance(section, str):
info = self.find_section(section)
if info is None:
raise ValueError(f"Section not found: {section}")
section = info
if section.header.SizeOfRawData == 0:
return b""
start = section.header.PointerToRawData
end = start + section.header.SizeOfRawData
return bytes(self._data[start:end])
def get_data_directory(self, index: int) -> DataDirectory | None:
"""Get a data directory by index."""
if 0 <= index < len(self._data_dirs):
return self._data_dirs[index]
return None
# =========================================================================
# Address Conversion
# =========================================================================
def rva_to_file_offset(self, rva: int) -> int | None:
"""Convert RVA to file offset using section table.
Args:
rva: Relative virtual address
Returns:
File offset if RVA is in a section with raw data, None otherwise.
Returns None for BSS/uninitialized sections (SizeOfRawData == 0).
"""
for shdr in self._sections:
if shdr.contains_rva(rva):
# BSS sections have no file data
if shdr.SizeOfRawData == 0:
return None
# Calculate offset within section
section_offset = rva - shdr.VirtualAddress
# Ensure we don't read beyond the raw data
if section_offset >= shdr.SizeOfRawData:
return None
return shdr.PointerToRawData + section_offset
return None
def file_offset_to_rva(self, offset: int) -> int | None:
"""Convert file offset to RVA.
Args:
offset: File offset
Returns:
RVA if offset is in a section, None otherwise
"""
for shdr in self._sections:
if shdr.contains_file_offset(offset):
# Calculate offset within section
section_offset = offset - shdr.PointerToRawData
return shdr.VirtualAddress + section_offset
return None
def rva_to_va(self, rva: int) -> int:
"""Convert RVA to virtual address (ImageBase + RVA)."""
return self.image_base + rva
def va_to_rva(self, va: int) -> int:
"""Convert virtual address to RVA (VA - ImageBase)."""
return va - self.image_base
def va_to_file_offset(self, va: int) -> int | None:
"""Convert virtual address to file offset."""
return self.rva_to_file_offset(self.va_to_rva(va))
# =========================================================================
# Write Operations (Low-Level)
# =========================================================================
def write_bytes_at_offset(
self, offset: int, data: bytes, description: str = ""
) -> None:
"""Write bytes at a file offset.
Args:
offset: File offset
data: Bytes to write
description: Human-readable description for tracking
"""
if offset + len(data) > len(self._data):
raise ValueError(
f"Write would exceed file bounds: offset={offset}, "
f"len={len(data)}, file_size={len(self._data)}"
)
self._data[offset : offset + len(data)] = data
self._modifications.append(
Modification(
operation="write_bytes",
file_offset=offset,
size=len(data),
description=description or f"write {len(data)} bytes at 0x{offset:x}",
)
)
def write_bytes_at_rva(self, rva: int, data: bytes, description: str = "") -> None:
"""Write bytes at an RVA.
Args:
rva: Relative virtual address
data: Bytes to write
description: Human-readable description for tracking
Raises:
ValueError: If RVA is not in any section
"""
offset = self.rva_to_file_offset(rva)
if offset is None:
raise ValueError(f"RVA 0x{rva:x} not in any section")
self.write_bytes_at_offset(offset, data, description)
def zero_range(self, offset: int, size: int, description: str = "") -> None:
"""Zero out a range of bytes.
Args:
offset: Start file offset
size: Number of bytes to zero
description: Human-readable description
"""
self.write_bytes_at_offset(
offset,
b"\x00" * size,
description or f"zero {size} bytes at 0x{offset:x}",
)
# =========================================================================
# Pointer Operations
# =========================================================================
def read_pointer_at_rva(self, rva: int) -> int:
"""Read an 8-byte pointer at an RVA.
Args:
rva: Relative virtual address
Returns:
Pointer value (little-endian uint64)
"""
offset = self.rva_to_file_offset(rva)
if offset is None:
raise ValueError(f"RVA 0x{rva:x} not in any section")
return struct.unpack_from("<Q", self._data, offset)[0]
def write_pointer_at_rva(self, rva: int, value: int, description: str = "") -> None:
"""Write an 8-byte pointer at an RVA.
Args:
rva: Relative virtual address
value: Pointer value to write (as VA, not RVA)
description: Human-readable description
"""
data = struct.pack("<Q", value)
self.write_bytes_at_rva(
rva,
data,
description or f"write pointer 0x{value:x} at RVA 0x{rva:x}",
)
def read_u32_at_rva(self, rva: int) -> int:
"""Read a 4-byte unsigned integer at an RVA."""
offset = self.rva_to_file_offset(rva)
if offset is None:
raise ValueError(f"RVA 0x{rva:x} not in any section")
return struct.unpack_from("<I", self._data, offset)[0]
def write_u32_at_rva(self, rva: int, value: int, description: str = "") -> None:
"""Write a 4-byte unsigned integer at an RVA."""
data = struct.pack("<I", value)
self.write_bytes_at_rva(
rva,
data,
description or f"write u32 0x{value:x} at RVA 0x{rva:x}",
)
# =========================================================================
# Header Update Operations
# =========================================================================
def _get_optional_header_offset(self) -> int:
"""Get file offset of optional header."""
return self._pe_offset + 4 + COFF_HEADER_SIZE
def _get_section_headers_offset(self) -> int:
"""Get file offset of first section header."""
return self._get_optional_header_offset() + self._coff_hdr.SizeOfOptionalHeader
def update_optional_header(self) -> None:
"""Write current optional header to binary."""
offset = self._get_optional_header_offset()
self._opt_hdr.write_to(self._data, offset)
# Also write data directories
data_dir_offset = offset + self._opt_hdr.SIZE
for i, dd in enumerate(self._data_dirs):
dd.write_to(self._data, data_dir_offset + i * DATA_DIRECTORY_SIZE)
self._modifications.append(
Modification(
operation="update_optional_header",
file_offset=offset,
size=self._coff_hdr.SizeOfOptionalHeader,
description="update optional header",
)
)
def update_coff_header(self) -> None:
"""Write current COFF header to binary."""
offset = self._pe_offset + 4
self._coff_hdr.write_to(self._data, offset)
self._modifications.append(
Modification(
operation="update_coff_header",
file_offset=offset,
size=COFF_HEADER_SIZE,
description="update COFF header",
)
)
def update_section_header(self, index: int, shdr: SectionHeader) -> None:
"""Update a section header in the binary.
Args:
index: Section header index
shdr: New section header value
"""
if index < 0 or index >= self._coff_hdr.NumberOfSections:
raise ValueError(f"Invalid section header index: {index}")
offset = self._get_section_headers_offset() + index * SECTION_HEADER_SIZE
shdr.write_to(self._data, offset)
self._sections[index] = shdr
self._modifications.append(
Modification(
operation="update_section_header",
file_offset=offset,
size=SECTION_HEADER_SIZE,
description=f"update section header {index} ({shdr.name_str})",
)
)
# =========================================================================
# Base Relocation Operations
# =========================================================================
def iter_base_relocations(self) -> Iterator[RelocationInfo]:
"""Iterate over all base relocation entries.
Yields:
RelocationInfo for each non-ABSOLUTE entry
"""
reloc_dir = self.get_data_directory(IMAGE_DIRECTORY_ENTRY_BASERELOC)
if reloc_dir is None or not reloc_dir.is_present:
return
reloc_offset = self.rva_to_file_offset(reloc_dir.VirtualAddress)
if reloc_offset is None:
return
end_offset = reloc_offset + reloc_dir.Size
current = reloc_offset
while current < end_offset:
block = BaseRelocationBlock.from_bytes(self._data, current)
if block.BlockSize == 0:
break
# Validate BlockSize to prevent infinite loop or DoS
if block.BlockSize < BaseRelocationBlock.SIZE:
break # Invalid block, stop iteration
# Iterate entries in this block
entry_offset = current + BaseRelocationBlock.SIZE
for i in range(block.num_entries):
entry = BaseRelocationEntry.from_bytes(self._data, entry_offset)
if not entry.is_absolute: # Skip padding entries
target_rva = block.PageRVA + entry.offset
yield RelocationInfo(
block_rva=block.PageRVA,
entry_offset=entry_offset,
target_rva=target_rva,
entry=entry,
)
entry_offset += BaseRelocationEntry.SIZE
current += block.BlockSize
def find_relocation_at_rva(self, target_rva: int) -> RelocationInfo | None:
"""Find a base relocation targeting a specific RVA.
Args:
target_rva: RVA the relocation targets
Returns:
RelocationInfo if found, None otherwise
"""
for reloc in self.iter_base_relocations():
if reloc.target_rva == target_rva:
return reloc
return None
# =========================================================================
# File Operations
# =========================================================================
def resize(self, new_size: int) -> None:
"""Resize the binary data.
Args:
new_size: New size in bytes
"""
current_size = len(self._data)
if new_size > current_size:
self._data.extend(b"\x00" * (new_size - current_size))
elif new_size < current_size:
del self._data[new_size:]
def append_bytes(self, data: bytes, description: str = "") -> int:
"""Append bytes to end of file.
Args:
data: Bytes to append
description: Human-readable description
Returns:
File offset where data was appended
"""
offset = len(self._data)
self._data.extend(data)
self._modifications.append(
Modification(
operation="append_bytes",
file_offset=offset,
size=len(data),
description=description or f"append {len(data)} bytes",
)
)
return offset
def save(self, path: Path) -> None:
"""Save modified binary to file.
Args:
path: Output file path
"""
path.write_bytes(self._data)
def save_preserving_mode(self, path: Path, mode: int | None = None) -> None:
"""Save modified binary, optionally setting file mode.
Args:
path: Output file path
mode: File mode to set. If None, does not set mode.
"""
path.write_bytes(self._data)
if mode is not None:
os.chmod(path, mode)
# =========================================================================
# Layout Information
# =========================================================================
def get_max_section_end_offset(self) -> int:
"""Get the maximum file offset used by any section."""
max_offset = 0
for shdr in self._sections:
if shdr.SizeOfRawData > 0:
end = shdr.PointerToRawData + shdr.SizeOfRawData
max_offset = max(max_offset, end)
return max_offset
def get_max_section_end_rva(self) -> int:
"""Get the maximum RVA used by any section (for SizeOfImage calculation)."""
max_rva = 0
for shdr in self._sections:
end = shdr.VirtualAddress + shdr.VirtualSize
max_rva = max(max_rva, end)
return max_rva
# =========================================================================
# Section Addition
# =========================================================================
def add_section(
self,
name: str,
content: bytes,
characteristics: int = IMAGE_SCN_MEM_READ | IMAGE_SCN_CNT_INITIALIZED_DATA,
) -> "AddSectionResult":
"""Add a new section to the PE binary.
This appends the section content to the end of the file and adds
a new section header. The new section gets the next available RVA.
Steps:
1. Check space exists for new section header
2. Append section content to end of file (aligned to FileAlignment)
3. Calculate RVA (next available, aligned to SectionAlignment)
4. Add new section header entry
5. Update NumberOfSections and SizeOfImage
Args:
name: Section name (max 8 chars, will be truncated)
content: Section content bytes
characteristics: IMAGE_SCN_* flags (default: readable initialized data)
Returns:
AddSectionResult with section details
Raises:
ValueError: If section already exists or no space for new header
"""
# Check if section already exists
truncated_name = name[:8]
if self.find_section(truncated_name) is not None:
raise ValueError(f"Section '{truncated_name}' already exists")
# Check space for new section header
headers_end = self._get_section_headers_offset() + (
self._coff_hdr.NumberOfSections * SECTION_HEADER_SIZE
)
new_header_end = headers_end + SECTION_HEADER_SIZE
if new_header_end > self._opt_hdr.SizeOfHeaders:
raise ValueError(
f"No space for new section header. "
f"Headers end at 0x{new_header_end:x}, "
f"SizeOfHeaders is 0x{self._opt_hdr.SizeOfHeaders:x}. "
f"Need {new_header_end - self._opt_hdr.SizeOfHeaders} more bytes."
)
# Step 1: Calculate file offset for new section (aligned to FileAlignment)
current_end = self.get_max_section_end_offset()
file_offset = round_up_to_alignment(current_end, self.file_alignment)
# Step 2: Calculate RVA for new section (aligned to SectionAlignment)
current_max_rva = self.get_max_section_end_rva()
rva = round_up_to_alignment(current_max_rva, self.section_alignment)
# Step 3: Prepare content (pad to FileAlignment)
raw_size = round_up_to_alignment(len(content), self.file_alignment)
padded_content = content + (b"\x00" * (raw_size - len(content)))
# Step 4: Extend file with padding + content
padding_needed = file_offset - len(self._data)
if padding_needed > 0:
self._data.extend(b"\x00" * padding_needed)
self._data.extend(padded_content)
# Step 5: Create section header
new_shdr = SectionHeader(
Name=section_name_to_bytes(truncated_name),
VirtualSize=len(content), # Actual size, not padded
VirtualAddress=rva,
SizeOfRawData=raw_size,
PointerToRawData=file_offset,
PointerToRelocations=0,
PointerToLinenumbers=0,
NumberOfRelocations=0,
NumberOfLinenumbers=0,
Characteristics=characteristics,
)
# Step 6: Write section header to file
new_shdr.write_to(self._data, headers_end)
self._sections.append(new_shdr)
# Step 7: Update COFF header (NumberOfSections)
self._coff_hdr = CoffHeader(
Machine=self._coff_hdr.Machine,
NumberOfSections=self._coff_hdr.NumberOfSections + 1,
TimeDateStamp=self._coff_hdr.TimeDateStamp,
PointerToSymbolTable=self._coff_hdr.PointerToSymbolTable,
NumberOfSymbols=self._coff_hdr.NumberOfSymbols,
SizeOfOptionalHeader=self._coff_hdr.SizeOfOptionalHeader,
Characteristics=self._coff_hdr.Characteristics,
)
self.update_coff_header()
# Step 8: Update SizeOfImage in optional header
# SizeOfImage must include the new section, aligned to SectionAlignment
new_max_rva = rva + round_up_to_alignment(len(content), self.section_alignment)
self._opt_hdr.SizeOfImage = new_max_rva
self.update_optional_header()
self._modifications.append(
Modification(
operation="add_section",
file_offset=file_offset,
size=raw_size,
description=f"add section '{truncated_name}' at RVA 0x{rva:x}",
)
)
return AddSectionResult(
success=True,
section_index=len(self._sections) - 1,
rva=rva,
file_offset=file_offset,
size=len(content),
)
@dataclass
class AddSectionResult:
"""Result of adding a section."""
success: bool
section_index: int
rva: int
file_offset: int
size: int
error: str | None = None