Files
chimera-gfx-Public/tools/audit_ps5_artifacts.py
T
Chimera GFX release export a6037502d7
phase0-ci / build-and-audit (push) Successful in 2m14s
Publish Chimera GFX source
2026-09-03 03:27:14 +02:00

226 lines
7.4 KiB
Python

#!/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