mirror of
https://github.com/YusufOzmen01/quest-kexec.git
synced 2026-10-26 19:37:39 +00:00
144 lines
6.9 KiB
Python
Executable File
144 lines
6.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Prepare Image + initramfs + DTB for the Quest Pro kexec staging layout.
|
|
|
|
The Android boot image is optional; it is only needed when the kernel or
|
|
initramfs is taken from it.
|
|
|
|
Only writes host files. Does not contact the headset or execute a boot.
|
|
"""
|
|
import argparse
|
|
import gzip
|
|
import hashlib
|
|
import json
|
|
import struct
|
|
import subprocess
|
|
import zlib
|
|
from pathlib import Path
|
|
|
|
|
|
def align(n, a):
|
|
return (n + a - 1) & -a
|
|
|
|
|
|
def unpack(data):
|
|
if len(data) < 1660 or data[:8] != b"ANDROID!":
|
|
raise ValueError("not an Android boot image")
|
|
ksize, _, rsize, _, ssize, _, _, page, version, _ = struct.unpack_from("<10I", data, 8)
|
|
if version not in (0, 1, 2):
|
|
raise ValueError("only boot header versions 0, 1 and 2 are supported")
|
|
if page not in (2048, 4096, 8192, 16384) or not ksize:
|
|
raise ValueError("invalid page or kernel size")
|
|
if ssize:
|
|
raise ValueError("second-stage bootloader payloads are not supported")
|
|
roff = align(page + ksize, page)
|
|
soff = align(roff + rsize, page)
|
|
if soff + ssize > len(data) or roff + rsize > len(data):
|
|
raise ValueError("truncated boot image")
|
|
kernel = data[page:page + ksize]
|
|
if kernel.startswith(b"\x1f\x8b"):
|
|
# Gzip streams with appended DTBs need separate handling, not guesses.
|
|
kernel = gzip.decompress(kernel)
|
|
if len(kernel) < 64 or kernel[56:60] != b"ARM\x64":
|
|
raise ValueError("kernel is not an uncompressed/gzip ARM64 Image")
|
|
text_offset, image_size, flags = struct.unpack_from("<QQQ", kernel, 8)
|
|
if not image_size or image_size < len(kernel) or flags & 1:
|
|
raise ValueError("unsupported ARM64 image size/endianness (appended data must be separated)")
|
|
if flags & 6 not in (0, 2):
|
|
raise ValueError("this loader requires a 4K-page target")
|
|
if not flags & 8:
|
|
raise ValueError("target does not permit placement away from the base of RAM")
|
|
cmdline = (data[64:576].split(b"\0", 1)[0] +
|
|
data[608:1632].split(b"\0", 1)[0]).decode("ascii")
|
|
return kernel, data[roff:roff + rsize], dict(
|
|
boot_header_version=version, text_offset=text_offset,
|
|
image_size=image_size, image_file_size=len(kernel), flags=flags,
|
|
cmdline=cmdline, boot_sha256=hashlib.sha256(data).hexdigest())
|
|
|
|
|
|
def check_ram(iomem, start, size):
|
|
"""Require one System RAM extent and reject every nested reserved extent."""
|
|
end = start + size
|
|
ranges = []
|
|
for line in iomem.splitlines():
|
|
span, sep, label = line.strip().partition(" : ")
|
|
if not sep:
|
|
continue
|
|
lo, hi = (int(x, 16) for x in span.split("-"))
|
|
ranges.append((lo, hi + 1, label))
|
|
if not any(lo <= start < end <= hi and label == "System RAM" for lo, hi, label in ranges):
|
|
raise ValueError("destination does not fit in a captured System RAM extent")
|
|
for lo, hi, label in ranges:
|
|
if label != "System RAM" and start < hi and lo < end:
|
|
raise ValueError(f"destination overlaps {label}: {lo:#x}-{hi:#x}")
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
ap.add_argument("--boot", type=Path, help="Android boot.img (v0-v2) to take missing parts from")
|
|
ap.add_argument("--live-dtb", type=Path, required=True)
|
|
ap.add_argument("--iomem", type=Path, required=True)
|
|
ap.add_argument("--output", type=Path, required=True)
|
|
ap.add_argument("--initramfs", type=Path, help="optional replacement; original is retained separately")
|
|
ap.add_argument("--cmdline", help="explicit replacement command line")
|
|
ap.add_argument("--kernel-image", type=Path, help="replacement raw ARM64 Image")
|
|
ap.add_argument("--keep-watchdog", action="store_true",
|
|
help="leave /soc/qcom,wdt@17c10000 enabled in the DTB")
|
|
a = ap.parse_args()
|
|
if a.boot:
|
|
kernel, original, report = unpack(a.boot.read_bytes())
|
|
elif a.kernel_image and a.initramfs:
|
|
kernel, original, report = b"", b"", dict(cmdline="")
|
|
else:
|
|
ap.error("--boot is required unless --kernel-image and --initramfs are both given")
|
|
if a.kernel_image:
|
|
kernel = a.kernel_image.read_bytes()
|
|
if len(kernel) < 64 or kernel[56:60] != b"ARM\x64":
|
|
raise ValueError("replacement is not a raw ARM64 Image")
|
|
offset, size, flags = struct.unpack_from("<QQQ", kernel, 8)
|
|
if size < len(kernel) or flags & 1 or flags & 6 not in (0, 2) or not flags & 8:
|
|
raise ValueError("unsupported replacement Image")
|
|
report.update(text_offset=offset, image_size=size, flags=flags,
|
|
image_file_size=len(kernel), replacement_kernel=str(a.kernel_image),
|
|
kernel_sha256=hashlib.sha256(kernel).hexdigest())
|
|
if report["text_offset"] != 0x80000 or report["image_size"] > 0x2F80000:
|
|
raise ValueError("image does not fit this Quest Pro staging layout")
|
|
initrd = a.initramfs.read_bytes() if a.initramfs else original
|
|
dtb = a.live_dtb.read_bytes()
|
|
if len(dtb) < 40 or struct.unpack_from(">I", dtb)[0] != 0xD00DFEED:
|
|
raise ValueError("invalid live DTB")
|
|
if len(initrd) > 0x200000 or len(dtb) > 0x200000:
|
|
raise ValueError("initramfs/DTB exceeds staging limits")
|
|
# Leave a single exclusion window for source, descriptor and control pages.
|
|
check_ram(a.iomem.read_text(), 0x90000000, 0x3400000)
|
|
a.output.mkdir(parents=True, exist_ok=True)
|
|
(a.output / "Image").write_bytes(kernel)
|
|
if original:
|
|
(a.output / "original-ramdisk.gz").write_bytes(original)
|
|
(a.output / "initramfs").write_bytes(initrd)
|
|
dtpath = a.output / "boot.dtb"
|
|
dtpath.write_bytes(dtb)
|
|
args = a.cmdline if a.cmdline is not None else report["cmdline"]
|
|
subprocess.run(["fdtput", "-t", "s", str(dtpath), "/chosen", "bootargs", args], check=True)
|
|
# The target kernel takes over the apps watchdog from the loader instead of
|
|
# probing it again; this matches every validated boot.
|
|
if not a.keep_watchdog:
|
|
subprocess.run(["fdtput", "-t", "s", str(dtpath), "/soc/qcom,wdt@17c10000",
|
|
"status", "disabled"], check=True)
|
|
for name, address in (("linux,initrd-start", 0x93000000),
|
|
("linux,initrd-end", 0x93000000 + len(initrd))):
|
|
subprocess.run(["fdtput", "-t", "x", str(dtpath), "/chosen", name,
|
|
"0", f"{address:x}"], check=True)
|
|
report.update(kernel_address=0x90080000, initrd_address=0x93000000,
|
|
dtb_address=0x93200000, initrd_size=len(initrd),
|
|
dtb_size=dtpath.stat().st_size, effective_cmdline=args,
|
|
replacement_initramfs=a.initramfs is not None,
|
|
crc32={name: f"{zlib.crc32(content):08x}" for name, content in
|
|
(("Image", kernel), ("initramfs", initrd), ("boot.dtb", dtpath.read_bytes()))},
|
|
status="prepared only; no boot attempted")
|
|
(a.output / "manifest.json").write_text(json.dumps(report, indent=2) + "\n")
|
|
print(json.dumps(report, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|