#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-3.0-or-later """Statically audit the fail-closed PS5 ELFs without executing them.""" from __future__ import annotations import argparse import json import re import subprocess from pathlib import Path EXPECTED_PHASE1_SCE_IMPORTS = { "sceKernelAllocateMainDirectMemory", "sceKernelCreateEqueue", "sceKernelDeleteEqueue", "sceKernelMapDirectMemory", "sceKernelReleaseDirectMemory", "sceKernelWaitEqueue", "sceSystemServiceHideSplashScreen", "sceVideoOutAddFlipEvent", "sceVideoOutClose", "sceVideoOutDeleteFlipEvent", "sceVideoOutOpen", "sceVideoOutRegisterBuffers2", "sceVideoOutSetBufferAttribute2", "sceVideoOutSetFlipRate", "sceVideoOutSubmitFlip", } EXPECTED_PROBE_UNDEFINED_IMPORTS = { "__stderrp", "__stdoutp", "fprintf", "fwrite", "snprintf", "strcmp", } EXPECTED_PROBE_NEEDED = { "libkernel_web.sprx", "libSceLibcInternal.sprx", "libSceNet.sprx", } def run_nm(nm: Path, artifact: Path, undefined_only: bool) -> str: command = [str(nm)] command.append("-u" if undefined_only else "-a") command.append(str(artifact)) result = subprocess.run(command, check=True, capture_output=True, text=True) return result.stdout def run_readelf(readelf: Path, artifact: Path) -> str: result = subprocess.run( [str(readelf), "--dynamic-table", str(artifact)], check=True, capture_output=True, text=True, ) return result.stdout def extract_sce_imports(undefined_symbols: str) -> set[str]: imports: set[str] = set() for line in undefined_symbols.splitlines(): match = re.search(r"\bU\s+(sce[A-Za-z0-9_]+)\s*$", line) if match is not None: imports.add(match.group(1)) return imports def extract_undefined_imports(undefined_symbols: str) -> set[str]: imports: set[str] = set() for line in undefined_symbols.splitlines(): match = re.search(r"\bU\s+(\S+)\s*$", line) if match is not None: imports.add(match.group(1)) return imports def extract_needed(dynamic_table: str) -> set[str]: return set(re.findall(r"Shared library: \[([^\]]+)\]", dynamic_table)) def require_symbols(symbols: str, names: set[str], artifact: Path) -> None: missing = sorted(name for name in names if name not in symbols) if missing: raise ValueError(f"{artifact.name}: missing retained symbols: {', '.join(missing)}") def require_bytes(artifact: Path, values: set[bytes]) -> None: content = artifact.read_bytes() missing = sorted(value.decode("ascii") for value in values if value not in content) if missing: raise ValueError(f"{artifact.name}: missing gate strings: {', '.join(missing)}") def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--nm", type=Path, required=True) parser.add_argument("--readelf", type=Path, required=True) parser.add_argument("--probe", type=Path, required=True) parser.add_argument("--symbol-manifest", type=Path, required=True) parser.add_argument("--phase1", type=Path) parser.add_argument("--firmware", default="NONE") args = parser.parse_args() nm = args.nm.resolve(strict=True) readelf = args.readelf.resolve(strict=True) probe = args.probe.resolve(strict=True) symbol_manifest = json.loads( args.symbol_manifest.read_text(encoding="utf-8") ) probe_undefined = run_nm(nm, probe, undefined_only=True) probe_all_imports = extract_undefined_imports(probe_undefined) probe_imports = extract_sce_imports(probe_undefined) if probe_imports: raise ValueError( f"{probe.name}: unexpected direct Sce imports: " f"{', '.join(sorted(probe_imports))}" ) if probe_all_imports != EXPECTED_PROBE_UNDEFINED_IMPORTS: missing = sorted(EXPECTED_PROBE_UNDEFINED_IMPORTS - probe_all_imports) unexpected = sorted(probe_all_imports - EXPECTED_PROBE_UNDEFINED_IMPORTS) raise ValueError( f"{probe.name}: undefined import inventory changed; " f"missing={missing}, unexpected={unexpected}" ) dynamic_table = run_readelf(readelf, probe) needed = extract_needed(dynamic_table) if needed != EXPECTED_PROBE_NEEDED: missing = sorted(EXPECTED_PROBE_NEEDED - needed) unexpected = sorted(needed - EXPECTED_PROBE_NEEDED) raise ValueError( f"{probe.name}: DT_NEEDED inventory changed; " f"missing={missing}, unexpected={unexpected}" ) for array in ("INIT_ARRAYSZ", "FINI_ARRAYSZ"): if not re.search( rf"(?:\({array}\)|{array})\s+0 \(bytes\)", dynamic_table ): raise ValueError(f"{probe.name}: {array} is not empty") require_symbols( run_nm(nm, probe, undefined_only=False), { "chimera_gfx_firmware_gate_allows", "chimera_gfx_ps5_make_loader_ops", "chimera_gfx_ps5_probe_symbols", "dlclose", "dlopen", "dlsym", }, probe, ) require_bytes( probe, { args.firmware.encode("ascii"), b"--acknowledge-read-only-probe", b"firmware is not allowlisted", b"libSceGnmDriver.sprx", } | { entry["name"].encode("ascii") for entry in symbol_manifest["symbols"] }, ) if args.phase1 is not None: phase1 = args.phase1.resolve(strict=True) phase1_undefined = run_nm(nm, phase1, undefined_only=True) phase1_imports = extract_sce_imports(phase1_undefined) if phase1_imports != EXPECTED_PHASE1_SCE_IMPORTS: missing = sorted(EXPECTED_PHASE1_SCE_IMPORTS - phase1_imports) unexpected = sorted(phase1_imports - EXPECTED_PHASE1_SCE_IMPORTS) raise ValueError( f"{phase1.name}: Sce import inventory changed; " f"missing={missing}, unexpected={unexpected}" ) if any(name.startswith("sceGnm") for name in phase1_imports): raise ValueError(f"{phase1.name}: direct GNM import detected") require_symbols( run_nm(nm, phase1, undefined_only=False), { "chimera_gfx_firmware_gate_allows", "SDL_Init", "SDL_UpdateWindowSurface", }, phase1, ) require_bytes( phase1, { args.firmware.encode("ascii"), b"--acknowledge-phase1-videoout-clear", b"firmware is not allowlisted", }, ) print( "PS5 artifact audit passed: probe has exactly 6 reviewed undefined " "imports, 3 reviewed DT_NEEDED modules, empty init/fini arrays, and " f"0 direct Sce imports; Phase-1 has {len(phase1_imports)} reviewed " "Sce imports and 0 GNM imports" ) else: print( "PS5 artifact audit passed: probe has exactly 6 reviewed undefined " "imports, 3 reviewed DT_NEEDED modules, empty init/fini arrays, all " "manifest names, and 0 direct Sce/GNM imports" ) return 0 if __name__ == "__main__": try: raise SystemExit(main()) except (OSError, subprocess.CalledProcessError, ValueError, json.JSONDecodeError, KeyError, TypeError) as error: print(f"PS5 artifact audit failed: {error}") raise SystemExit(1) from error