summaryrefslogtreecommitdiffstats
path: root/tools/gen_gsp_bindata.py
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-01 10:07:14 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-01 10:07:14 -0400
commit5f372957a5fd0dd7ca7d4b5f06e959cce92dd49e (patch)
tree03000df095fa084c733f87634d92d1b8e8be1e0f /tools/gen_gsp_bindata.py
parent4805f88b6d1449d23bf8af2e80a9b2b1984f7189 (diff)
downloadopen-iov-5f372957a5fd0dd7ca7d4b5f06e959cce92dd49e.tar.gz
open-iov-5f372957a5fd0dd7ca7d4b5f06e959cce92dd49e.zip
gsp-phys: real GSP boot stages 3d/3e and the GSP-RM run-sequencer event
Stage 3d stages the GSP-RM boot from host sysmem (FMC, radix3 over the GSP-RM ELF, signature, message queues, libos LOGINIT/LOGRM/RMARGS and GSP arguments) and runs Booter Load on SEC2: GSP RISC-V reset, libos mailbox, IMEM/DMEM direct load, BOOTVEC=0, M0==0, FALCON_OS, RISC-V active. Stage 3e links the status queue and polls for GSP-RM events; it now also executes the inline run-sequencer command buffer (event 0x1002) against BAR0 (REG_WRITE/MODIFY/POLL, DELAY, STORE, CORE_* including CORE_RESUME) and then waits for GSP_INIT_DONE 0x1001. Gated by gsp_phys_boot / gsp_phys_boot_dryrun (default off; the normal load path writes nothing). gsp_phys_fwsec=1 without gsp_phys_boot=1 is refused: a FWSEC-only carve cannot pass the booter-unload handoff gate. Release quiesces the GSP, runs the existing FWSEC-SB/booter-unload teardown and frees all staging. tools/gen_gsp_bindata.py decodes the public generated archives into the Booter Load image (prod signature patched at 0x8700) and the GSP-RM boot (FMC) image; both are self-checked at build time.
Diffstat (limited to 'tools/gen_gsp_bindata.py')
-rw-r--r--tools/gen_gsp_bindata.py273
1 files changed, 273 insertions, 0 deletions
diff --git a/tools/gen_gsp_bindata.py b/tools/gen_gsp_bindata.py
new file mode 100644
index 0000000..c59b9d1
--- /dev/null
+++ b/tools/gen_gsp_bindata.py
@@ -0,0 +1,273 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright (c) 2026 Open-IOV contributors
+#
+# Generate the kernel bindata headers for the real-GSP boot stages from the
+# public open kernel-modules generated bindata archives.
+#
+# The archives are C files holding raw-deflate compressed byte arrays. This
+# tool extracts the production booter-load image (runs on SEC2 to load,
+# verify and start the GSP-RM RISC-V image) and the GSP-RM boot image (the
+# "FMC" 4 KB bootloader), applies the production signature patch the driver
+# applies at runtime, verifies sizes/headers, and cross-checks the booter
+# image against the distribution firmware blob when present.
+#
+# Usage: python3 tools/gen_gsp_bindata.py [--check]
+
+import argparse
+import hashlib
+import re
+import struct
+import sys
+import zlib
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+GENERATED = ROOT / "nvidia-open-gpu-kernel-modules" / "src" / "nvidia" / "generated"
+KERNEL = ROOT / "kernel"
+
+BOOTER_ARCHIVE = GENERATED / "g_bindata_kgspGetBinArchiveBooterLoadUcode_TU102.c"
+FMC_ARCHIVE = GENERATED / "g_bindata_kgspGetBinArchiveGspRmBoot_TU102.c"
+DEFAULT_FW_BOOTER = Path("/lib/firmware/nvidia/tu102/gsp/booter_load-570.144.bin")
+
+# Booter-load production image expectations (reference ABI, Turing).
+BOOTER_IMAGE_SIZE = 59136
+BOOTER_SIG_OFFSET = 0x8700
+BOOTER_SIG = bytes.fromhex("3260436798897b06e85f1aa871435dcb")
+BOOTER_HEADER = (0x0, 0x100, 0x8500, 0x6200, 0x1, 0x100, 0x8400, 0x100, 0x0)
+BOOTER_PATCH_META = (0x0, 0x1, 0xd)
+
+# GSP-RM boot image expectations (FMC).
+FMC_IMAGE_SIZE = 4096
+FMC_DESC_SIZE = 76
+FMC_DESC_VERSION = 4
+FMC_DESC_WORDS = [FMC_DESC_VERSION, 0, 0x488, 0x488, 0x10] + [0] * 14
+
+EXPECTED_BOOTER_SHA256 = "1de31591beae01d3a59778d9066532c6e808029258927d270c31193265c3305f"
+EXPECTED_FMC_SHA256 = "8765694cd38beaa4e2b8b1b83a31fb7aee6cb9d8b7c6bc7a86e6d931a3a056c2"
+
+FIELD_RE = re.compile(r'// FUNCTION: .*\("([A-Z0-9_]+)"\)')
+COMPRESSION_RE = re.compile(r"// COMPRESSION: (YES|NO)")
+
+
+def parse_archive(path):
+ """Return {label: (compressed, bytes)} from a generated bindata C file."""
+ entries = {}
+ label = None
+ compressed = None
+ data = bytearray()
+ with open(path) as fh:
+ for line in fh:
+ stripped = line.strip()
+ m = FIELD_RE.match(stripped)
+ if m:
+ if label is not None:
+ entries[label] = (compressed, bytes(data))
+ label, compressed, data = m.group(1), None, bytearray()
+ continue
+ if label is None:
+ continue
+ m = COMPRESSION_RE.match(stripped)
+ if m:
+ compressed = m.group(1) == "YES"
+ continue
+ if stripped.startswith("//") or stripped == "":
+ continue
+ if stripped == "};":
+ entries[label] = (compressed, bytes(data))
+ label = None
+ continue
+ if stripped.startswith("{") or stripped.startswith("static") or \
+ stripped.startswith("BINDATA") or stripped.startswith("#"):
+ continue
+ for token in re.findall(r"0x[0-9a-fA-F]{1,2}", line):
+ data.append(int(token, 16))
+ if label is not None:
+ entries[label] = (compressed, bytes(data))
+ return entries
+
+
+def extract(entries, label, want_compressed=None):
+ if label not in entries:
+ raise SystemExit(f"missing bindata label {label}")
+ compressed, raw = entries[label]
+ if want_compressed is not None and compressed != want_compressed:
+ raise SystemExit(
+ f"{label}: compression flag {compressed} != expected {want_compressed}"
+ )
+ if compressed:
+ return zlib.decompress(raw, -15)
+ return raw
+
+
+def emit_header(path, guard, name, image, note_lines):
+ sha = hashlib.sha256(image).hexdigest()
+ out = []
+ out.append("/* SPDX-License-Identifier: GPL-2.0 */")
+ out.append("/*")
+ for line in note_lines:
+ out.append(f" * {line}" if line else " *")
+ out.append(" *")
+ out.append(" * Generated from the open kernel-modules bindata archives; do")
+ out.append(" * not edit by hand.")
+ out.append(" *")
+ out.append(f" * image size: {len(image)} bytes")
+ out.append(f" * sha256: {sha}")
+ out.append(" */")
+ out.append("")
+ out.append(f"#ifndef {guard}")
+ out.append(f"#define {guard}")
+ out.append("")
+ out.append("#include <linux/types.h>")
+ out.append("")
+ out.append(f"static const u8 {name}[{len(image)}] = {{")
+ for i in range(0, len(image), 12):
+ row = ", ".join(f"0x{b:02x}" for b in image[i : i + 12])
+ out.append(f"\t{row},")
+ out.append("};")
+ out.append("")
+ out.append(f"#endif /* {guard} */")
+ path.write_text("\n".join(out) + "\n")
+ return sha
+
+
+def cross_check_booter(fw_path, image):
+ if not fw_path.exists():
+ raise SystemExit(f"cross-check firmware not found: {fw_path}")
+ fw = fw_path.read_bytes()
+ if len(fw) < 0x88 + len(image):
+ raise SystemExit(f"cross-check firmware too small: {len(fw)} bytes")
+ if len(fw) != 0x88 + len(image):
+ raise SystemExit(
+ f"cross-check firmware size {len(fw)} != 0x88 + {len(image)}"
+ )
+ for i, byte in enumerate(image):
+ if BOOTER_SIG_OFFSET <= i < BOOTER_SIG_OFFSET + len(BOOTER_SIG):
+ continue
+ if fw[0x88 + i] != byte:
+ raise SystemExit(f"cross-check mismatch at image offset 0x{i:x}")
+ if fw[0x88 + BOOTER_SIG_OFFSET : 0x88 + BOOTER_SIG_OFFSET + 16] != bytes(16):
+ # The distribution blob carries a zeroed patch slot; anything else
+ # means this check is no longer comparing what we think it is.
+ raise SystemExit("cross-check firmware has a non-zero signature patch slot")
+ return True
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "--check",
+ action="store_true",
+ help="verify everything but do not rewrite the headers",
+ )
+ parser.add_argument(
+ "--firmware-booter",
+ type=Path,
+ default=DEFAULT_FW_BOOTER,
+ help=f"distribution booter blob for the cross-check (default {DEFAULT_FW_BOOTER})",
+ )
+ args = parser.parse_args()
+
+ if not BOOTER_ARCHIVE.exists() or not FMC_ARCHIVE.exists():
+ raise SystemExit(f"generated bindata sources not found under {GENERATED}")
+
+ booter_entries = parse_archive(BOOTER_ARCHIVE)
+ fmc_entries = parse_archive(FMC_ARCHIVE)
+
+ image = extract(booter_entries, "BINDATA_LABEL_IMAGE_PROD", want_compressed=True)
+ header = extract(booter_entries, "BINDATA_LABEL_HEADER_PROD", want_compressed=True)
+ sig = extract(booter_entries, "BINDATA_LABEL_SIG_PROD", want_compressed=False)
+ patch_loc = extract(booter_entries, "BINDATA_LABEL_PATCH_LOC", want_compressed=False)
+ patch_sig = extract(booter_entries, "BINDATA_LABEL_PATCH_SIG", want_compressed=False)
+ patch_meta = extract(booter_entries, "BINDATA_LABEL_PATCH_META", want_compressed=False)
+ num_sigs = extract(booter_entries, "BINDATA_LABEL_NUM_SIGS", want_compressed=False)
+
+ if len(image) != BOOTER_IMAGE_SIZE:
+ raise SystemExit(f"booter image size {len(image)} != {BOOTER_IMAGE_SIZE}")
+ if len(header) != 36:
+ raise SystemExit(f"booter header size {len(header)} != 36")
+ fields = struct.unpack("<9I", header)
+ if fields != BOOTER_HEADER:
+ raise SystemExit(f"booter header {fields} != {BOOTER_HEADER}")
+ if len(sig) != 16:
+ raise SystemExit(f"booter signature size {len(sig)} != 16")
+ if patch_loc != struct.pack("<I", BOOTER_SIG_OFFSET):
+ raise SystemExit(f"booter patch location {patch_loc.hex()} unexpected")
+ if patch_sig != bytes(4):
+ raise SystemExit(f"booter patch signature {patch_sig.hex()} != zero")
+ if struct.unpack("<3I", patch_meta) != BOOTER_PATCH_META:
+ raise SystemExit(f"booter patch meta {patch_meta.hex()} unexpected")
+ if struct.unpack("<I", num_sigs)[0] != 1:
+ raise SystemExit("booter archive must carry exactly one signature")
+
+ image = bytearray(image)
+ if bytes(image[BOOTER_SIG_OFFSET : BOOTER_SIG_OFFSET + 16]) != bytes(16):
+ raise SystemExit("booter signature slot is not empty before patching")
+ image[BOOTER_SIG_OFFSET : BOOTER_SIG_OFFSET + 16] = sig
+ if bytes(image[BOOTER_SIG_OFFSET : BOOTER_SIG_OFFSET + 16]) != BOOTER_SIG:
+ raise SystemExit("booter signature patch did not take")
+ booter_sha = hashlib.sha256(bytes(image)).hexdigest()
+
+ fmc_image = extract(fmc_entries, "BINDATA_LABEL_UCODE_IMAGE", want_compressed=True)
+ fmc_desc = extract(fmc_entries, "BINDATA_LABEL_UCODE_DESC", want_compressed=True)
+ if len(fmc_image) != FMC_IMAGE_SIZE:
+ raise SystemExit(f"FMC image size {len(fmc_image)} != {FMC_IMAGE_SIZE}")
+ if len(fmc_desc) != FMC_DESC_SIZE:
+ raise SystemExit(f"FMC descriptor size {len(fmc_desc)} != {FMC_DESC_SIZE}")
+ words = list(struct.unpack("<19I", fmc_desc))
+ if words != FMC_DESC_WORDS:
+ raise SystemExit(f"FMC descriptor words {[hex(w) for w in words]} unexpected")
+ fmc_sha = hashlib.sha256(fmc_image).hexdigest()
+
+ if booter_sha != EXPECTED_BOOTER_SHA256:
+ raise SystemExit(
+ f"booter sha256 {booter_sha} != expected {EXPECTED_BOOTER_SHA256}"
+ )
+ if fmc_sha != EXPECTED_FMC_SHA256:
+ raise SystemExit(f"FMC sha256 {fmc_sha} != expected {EXPECTED_FMC_SHA256}")
+
+ cross_check_booter(args.firmware_booter, bytes(image))
+
+ print(f"booter-load image: {len(image)} bytes sha256={booter_sha}")
+ print(f" header: osCode 0x100, appCode 0x8400@0x100, data 0x6200@0x8500, numApps 1")
+ print(f" signature patched at 0x{BOOTER_SIG_OFFSET:x}: {BOOTER_SIG.hex()}")
+ print(f" cross-check vs {args.firmware_booter}: OK (except the 16-byte patch slot)")
+ print(f"GSP-RM boot (FMC) image: {len(fmc_image)} bytes sha256={fmc_sha}")
+ print(f" descriptor v{FMC_DESC_VERSION}: bootloader 0x488@0, param 0x10@0x488, appVersion 0")
+
+ if args.check:
+ print("--check: headers not rewritten")
+ return 0
+
+ booter_header_path = KERNEL / "gsp_phys_booter_load_img.h"
+ fmc_header_path = KERNEL / "gsp_phys_gsp_rm_boot_img.h"
+ emit_header(
+ booter_header_path,
+ "GSP_PHYS_BOOTER_LOAD_IMG_H",
+ "gsp_phys_booter_load_image",
+ bytes(image),
+ [
+ "Booter Load image (production, Turing) executed on SEC2 to load,",
+ "verify and start the GSP-RM RISC-V image. The 16-byte production",
+ "signature is already patched at its bindata patch location",
+ "(offset 0x8700).",
+ ],
+ )
+ emit_header(
+ fmc_header_path,
+ "GSP_PHYS_GSP_RM_BOOT_IMG_H",
+ "gsp_phys_gsp_rm_boot_image",
+ fmc_image,
+ [
+ "GSP-RM boot image (the 4 KB FMC bootloader, production, Turing)",
+ "consumed by the Booter Load ucode through the WPR metadata",
+ "sysmemAddrOfBootloader field.",
+ ],
+ )
+ print(f"wrote {booter_header_path}")
+ print(f"wrote {fmc_header_path}")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())