mirror of
https://github.com/flectar/mail.git
synced 2026-11-11 17:57:54 +00:00
137 lines
5.3 KiB
Python
137 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Name and check the ELF files of a Linux package.
|
|
|
|
`arch EXECUTABLE` prints the package architecture names of the executable
|
|
being packaged: its machine name (AppImage and RPM), Debian architecture and
|
|
PDFium archive, for example `aarch64 arm64 linux-arm64`. Packages describe the
|
|
executable they contain rather than the build machine, because a 64-bit
|
|
kernel can run a 32-bit userland.
|
|
|
|
`verify PATH...` checks every ELF file at or below the paths. They must all be
|
|
built for one machine, and every load segment must be aligned for the largest
|
|
page size that machine's kernels use. Arm64 kernels use 4, 16 or 64 KiB pages
|
|
(the Raspberry Pi 5 kernel uses 16 KiB), and they cannot load a segment that
|
|
is aligned only for 4 KiB pages.
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import struct
|
|
from pathlib import Path
|
|
|
|
PT_LOAD = 1
|
|
|
|
# ELF e_machine: machine name, Debian architecture, PDFium archive and the
|
|
# largest page size its Linux kernels use.
|
|
MACHINES = {
|
|
62: ("x86_64", "amd64", "linux-x64", 4096),
|
|
183: ("aarch64", "arm64", "linux-arm64", 65536),
|
|
}
|
|
|
|
|
|
def read_elf(path):
|
|
"""Return the machine and load segments of an ELF file, or None for other files."""
|
|
with open(path, "rb") as stream:
|
|
header = stream.read(64)
|
|
if header[:4] != b"\x7fELF":
|
|
return None
|
|
if len(header) < 64 or header[4:6] != b"\x02\x01":
|
|
raise ValueError(f"{path}: expected a little-endian 64-bit ELF file")
|
|
machine = struct.unpack_from("<H", header, 18)[0]
|
|
table_offset = struct.unpack_from("<Q", header, 32)[0]
|
|
entry_size, count = struct.unpack_from("<HH", header, 54)
|
|
if entry_size < 56:
|
|
raise ValueError(f"{path}: invalid ELF program headers")
|
|
stream.seek(table_offset)
|
|
table = stream.read(entry_size * count)
|
|
if len(table) != entry_size * count:
|
|
raise ValueError(f"{path}: truncated ELF program headers")
|
|
loads = []
|
|
for entry in range(0, len(table), entry_size):
|
|
if struct.unpack_from("<I", table, entry)[0] == PT_LOAD:
|
|
offset, address = struct.unpack_from("<QQ", table, entry + 8)
|
|
alignment = struct.unpack_from("<Q", table, entry + 48)[0]
|
|
loads.append((offset, address, alignment))
|
|
return machine, loads
|
|
|
|
|
|
def machine_names(machine, path):
|
|
if machine not in MACHINES:
|
|
raise ValueError(f"{path}: unsupported ELF machine {machine}")
|
|
return MACHINES[machine]
|
|
|
|
|
|
def architecture(path):
|
|
elf = read_elf(path)
|
|
if elf is None:
|
|
raise ValueError(f"{path} is not an ELF file")
|
|
return machine_names(elf[0], path)[:3]
|
|
|
|
|
|
def elf_files(paths):
|
|
for path in map(Path, paths):
|
|
if not path.is_dir():
|
|
# Name the problem when an expected executable is missing or broken.
|
|
elf = read_elf(path)
|
|
if elf is None:
|
|
raise ValueError(f"{path} is not an ELF file")
|
|
yield path, elf
|
|
continue
|
|
for directory, _, names in sorted(os.walk(path)):
|
|
for name in sorted(names):
|
|
candidate = Path(directory, name)
|
|
# Symlinks name files that the walk already visits.
|
|
if candidate.is_symlink() or not candidate.is_file():
|
|
continue
|
|
if (elf := read_elf(candidate)) is not None:
|
|
yield candidate, elf
|
|
|
|
|
|
def verify(paths):
|
|
"""Check every ELF file below the paths; return the machine names and file count."""
|
|
first = None
|
|
count = 0
|
|
for path, (machine, loads) in elf_files(paths):
|
|
name, _, _, page_size = machine_names(machine, path)
|
|
if first is None:
|
|
first = (machine, path)
|
|
elif machine != first[0]:
|
|
raise ValueError(f"{path} is built for {name}, but {first[1]} for {MACHINES[first[0]][0]}")
|
|
if not loads:
|
|
raise ValueError(f"{path}: missing ELF load segments")
|
|
for offset, address, alignment in loads:
|
|
if alignment < page_size or offset % page_size != address % page_size:
|
|
raise ValueError(
|
|
f"{path}: a load segment cannot be mapped with the "
|
|
f"{page_size // 1024} KiB pages that {name} kernels can use"
|
|
)
|
|
count += 1
|
|
if first is None:
|
|
raise ValueError("no ELF files found in " + ", ".join(map(str, paths)))
|
|
return MACHINES[first[0]], count
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
|
)
|
|
commands = parser.add_subparsers(dest="command", required=True)
|
|
names = commands.add_parser("arch", help="print the package architecture names")
|
|
names.add_argument("executable", type=Path)
|
|
check = commands.add_parser("verify", help="check machines and page alignment")
|
|
check.add_argument("paths", nargs="+", type=Path)
|
|
args = parser.parse_args()
|
|
try:
|
|
if args.command == "arch":
|
|
print(" ".join(architecture(args.executable)))
|
|
else:
|
|
(machine, _, _, page_size), count = verify(args.paths)
|
|
files = "file" if count == 1 else "files"
|
|
print(f"Checked {count} {machine} ELF {files}: load segments fit {page_size // 1024} KiB pages")
|
|
except (OSError, ValueError) as error:
|
|
raise SystemExit(str(error)) from error
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|