235 lines
10 KiB
Python
235 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Validate the offline-only Phase-1.0H startup-argument artifact record."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
from typing import Any
|
|
|
|
|
|
PHASE = "PHASE_1_0H_STARTUP_ARGUMENT_CORRECTION"
|
|
STATUS = "OFFLINE_ARTIFACT_PREPARED_NO_DEVICE_AUTHORIZATION"
|
|
SOURCE_COMMIT = "c710f85816b5e456dd3a85a46f85eb28883522b3"
|
|
RUNNER_COMMIT = "f2dd710f2dff8c840a4c747cbbb3654c24cf23a7"
|
|
ARTIFACT_SHA256 = "822f2cf1f4d33a514d2bdd88fde40ad580dda5d85f537362ef6dff2eafcb56b6"
|
|
ARTIFACT_SIZE = 1845152
|
|
MAP_SHA256 = "638642b750b8d5b108cf6c73215a3f1759bcb6da0b29ee0a0ade5c47e8b7b2d5"
|
|
MAP_SIZE = 637603
|
|
DISASSEMBLY_SHA256 = "d38ccaecb2f52b1529680dab68314b6b014ec446ac66cba645887fbe8f1d8c11"
|
|
AUTHORIZATION_FIELDS = (
|
|
"ps5_connection_authorized", "device_transfer_authorized",
|
|
"device_execution_authorized", "result_receive_authorized",
|
|
"installation_authorized", "autoload_authorized",
|
|
"device_write_authorized", "automatic_retry",
|
|
)
|
|
ACTION_FIELDS = (
|
|
"ps5_connected", "device_request_performed", "files_transferred",
|
|
"device_write_performed", "target_execution_performed",
|
|
"result_received_from_device", "installation_performed",
|
|
"autoload_performed",
|
|
)
|
|
DELIVERABLES = (
|
|
"docs/retroarch/phase-1.0h-startup-args.md",
|
|
"docs/approvals/phase-1.0h-device-test-template.md",
|
|
"manifests/retroarch/phase-1.0h-startup-args.json",
|
|
"tools/validate_retroarch_phase10h.py",
|
|
"tests/test_retroarch_phase10h.py",
|
|
"packaging/retroarch/phase10h/SHA256SUMS.txt",
|
|
)
|
|
|
|
|
|
def load_json(path: Path) -> dict[str, Any]:
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
if not isinstance(value, dict):
|
|
raise ValueError(f"{path} is not a JSON object")
|
|
return value
|
|
|
|
|
|
def git(root: Path, *args: str) -> str:
|
|
result = subprocess.run(
|
|
["git", *args], cwd=root, capture_output=True, text=True, check=False
|
|
)
|
|
if result.returncode:
|
|
raise RuntimeError(result.stderr.strip() or "git failed")
|
|
return result.stdout.strip()
|
|
|
|
|
|
def all_false(record: dict[str, Any], fields: tuple[str, ...]) -> bool:
|
|
return all(record.get(field) is False for field in fields)
|
|
|
|
|
|
def artifact_is_exact(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("name") == "retroarch_ps5_startup_args_diag.elf"
|
|
and record.get("profile") == "startup-args-diag"
|
|
and record.get("size") == ARTIFACT_SIZE
|
|
and record.get("sha256") == ARTIFACT_SHA256
|
|
and record.get("clean_build_sha256") == [ARTIFACT_SHA256] * 2
|
|
and record.get("linker_map_size") == MAP_SIZE
|
|
and record.get("linker_map_sha256") == MAP_SHA256
|
|
and record.get("clean_map_sha256") == [MAP_SHA256] * 2
|
|
and record.get("changed_from_phase10g") is True
|
|
and record.get("execution_eligible") is False
|
|
and record.get("transfer_eligible") is False
|
|
and record.get("installation_eligible") is False
|
|
and record.get("device_action_performed") is False
|
|
and record.get("tracked") is False
|
|
)
|
|
|
|
|
|
def correction_is_minimal(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("profile") == "startup-args-diag"
|
|
and record.get("have_menu") is False
|
|
and record.get("content_path") is None
|
|
and record.get("config_path") is None
|
|
and record.get("libretro_path") is None
|
|
and record.get("existing_flag_added") == "RARCH_MAIN_WRAP_FLAG_VERBOSE"
|
|
and set(record.get("preserved_flags", [])) == {
|
|
"RARCH_MAIN_WRAP_FLAG_TOUCHED", "RARCH_MAIN_WRAP_FLAG_NO_CONTENT"
|
|
}
|
|
and record.get("disassembled_flag_mask") == "0x07"
|
|
and record.get("generic_argument_added") == "-v"
|
|
and record.get("modeled_argv") == ["retroarch", "-v"]
|
|
and record.get("modeled_argc") == 2
|
|
and record.get("expected_next_checkpoint") == "I04"
|
|
and record.get("runtime_outcome") == "UNPROVEN"
|
|
)
|
|
|
|
|
|
def protocol_is_fail_closed(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("magic") == "CHD10H01"
|
|
and record.get("version") == 1
|
|
and record.get("frame_size") == 64
|
|
and record.get("d_stages") == [f"D{index:02d}" for index in range(13)]
|
|
and record.get("interval_stages") == [f"I{index:02d}" for index in range(15)]
|
|
and record.get("target_write_retry") is False
|
|
and record.get("short_write_retry") is False
|
|
and record.get("live_activation_available") is True
|
|
and record.get("live_activation_contract")
|
|
== "EXACT_ACTIVE_MANIFEST_AND_SEPARATE_LOCAL_APPROVAL_ONLY"
|
|
and record.get("protocol_activation_authorized") is False
|
|
and record.get("tracked_target") is None
|
|
and record.get("tracked_run_id") is None
|
|
and record.get("offline_parser_available") is True
|
|
)
|
|
|
|
|
|
def elf_matches_phase10f(record: dict[str, Any], phase10f: dict[str, Any]) -> bool:
|
|
old = phase10f.get("elf", {})
|
|
return (
|
|
record.get("type") == "ET_DYN"
|
|
and record.get("machine") == "EM_X86_64"
|
|
and record.get("rwx_load_segment_count") == 0
|
|
and record.get("tls") is False
|
|
and record.get("init_array_size") == 0
|
|
and record.get("fini_array_size") == 0
|
|
and record.get("relocations") == old.get("relocations")
|
|
and record.get("dt_needed") == old.get("dt_needed")
|
|
and record.get("undefined_symbol_count") == old.get("undefined_symbol_count") == 142
|
|
and record.get("undefined_symbols_match_phase10f") is True
|
|
and record.get("inherited_send_present") is True
|
|
and record.get("normalized_disassembly_sha256") == [DISASSEMBLY_SHA256] * 2
|
|
and not ({"socket", "connect", "bind", "listen", "accept", "recv"}
|
|
- set(record.get("forbidden_imports_absent", [])))
|
|
)
|
|
|
|
|
|
def validate(root: Path, retroarch_root: Path | None = None) -> list[str]:
|
|
errors: list[str] = []
|
|
for relative in DELIVERABLES:
|
|
if not (root / relative).is_file():
|
|
errors.append(f"missing deliverable: {relative}")
|
|
try:
|
|
record = load_json(root / "manifests/retroarch/phase-1.0h-startup-args.json")
|
|
phase10f = load_json(root / "manifests/retroarch/phase-1.0f-startup-interval.json")
|
|
phase10g = load_json(root / "manifests/retroarch/phase-1.0g-device-result.json")
|
|
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
return errors + [str(error)]
|
|
|
|
if record.get("phase") != PHASE or record.get("status") != STATUS:
|
|
errors.append("phase/status mismatch")
|
|
if record.get("source_commit") != SOURCE_COMMIT:
|
|
errors.append("source commit mismatch")
|
|
if record.get("runner_commit") != RUNNER_COMMIT:
|
|
errors.append("runner commit mismatch")
|
|
if not all_false(record.get("authorizations", {}), AUTHORIZATION_FIELDS):
|
|
errors.append("all Phase-1.0H authorizations must remain false")
|
|
if not all_false(record.get("phase_actions", {}), ACTION_FIELDS):
|
|
errors.append("Phase-1.0H must record no device action")
|
|
prior = record.get("prior_evidence", {})
|
|
if not (
|
|
prior.get("last_proven_stage") == phase10g.get("protocol_result", {}).get("last_proven_stage") == "I03"
|
|
and prior.get("first_unproven_stage") == "I04"
|
|
and prior.get("authorization_consumed") is True
|
|
and prior.get("authority_inherited") is False
|
|
):
|
|
errors.append("consumed Phase-1.0G evidence is overclaimed")
|
|
if not artifact_is_exact(record.get("artifact", {})):
|
|
errors.append("artifact identity/reproducibility mismatch")
|
|
if not correction_is_minimal(record.get("correction", {})):
|
|
errors.append("startup correction is broadened or incomplete")
|
|
if not protocol_is_fail_closed(record.get("result_protocol", {})):
|
|
errors.append("H protocol is active or unbounded")
|
|
if not elf_matches_phase10f(record.get("elf", {}), phase10f):
|
|
errors.append("ELF closure no longer matches Phase 1.0F")
|
|
if record.get("startup_effects", {}).get("side_effect_free") is not False:
|
|
errors.append("startup effects are hidden")
|
|
if record.get("tests", {}).get("hardware_evidence_from_phase10h") is not False:
|
|
errors.append("host evidence is mislabeled as hardware evidence")
|
|
|
|
tracked = git(root, "ls-files").splitlines()
|
|
if any(path.lower().endswith((".elf", ".self", ".sprx", ".pkg", ".map")) for path in tracked):
|
|
errors.append("target artifact or map is tracked")
|
|
sums = (root / "packaging/retroarch/phase10h/SHA256SUMS.txt").read_text(encoding="utf-8")
|
|
if ARTIFACT_SHA256 not in sums or MAP_SHA256 not in sums:
|
|
errors.append("hash-only record is incomplete")
|
|
|
|
if retroarch_root is not None:
|
|
try:
|
|
git(retroarch_root, "cat-file", "-e", f"{SOURCE_COMMIT}^{{commit}}")
|
|
git(retroarch_root, "cat-file", "-e", f"{RUNNER_COMMIT}^{{commit}}")
|
|
platform = git(retroarch_root, "show", f"{SOURCE_COMMIT}:frontend/drivers/platform_ps5.c")
|
|
makefile = git(retroarch_root, "show", f"{SOURCE_COMMIT}:Makefile.ps5")
|
|
stream = git(retroarch_root, "show", f"{SOURCE_COMMIT}:pkg/ps5/chimera_ps5_diag_stream.c")
|
|
host = git(retroarch_root, "show", f"{RUNNER_COMMIT}:tools/ps5_diag_duplex.py")
|
|
if "CHIMERA_PS5_STARTUP_ARGS_DIAG" not in platform or "RARCH_MAIN_WRAP_FLAG_VERBOSE" not in platform:
|
|
errors.append("source lacks scoped verbose flag")
|
|
if "PS5_PROFILE),startup-args-diag" not in makefile or "CHIMERA_PS5_STARTUP_ARGS_DIAG=1" not in makefile:
|
|
errors.append("source lacks startup-args profile")
|
|
if "'C', 'H', 'D', '1', '0', 'H', '0', '1'" not in stream:
|
|
errors.append("source lacks H magic")
|
|
if (
|
|
"STARTUP_ARGS_PROTOCOL" not in host
|
|
or "STARTUP_ARGS_ACTIVE_PHASE" not in host
|
|
or "EXACT_ONE_SHOT_PHASE_1_0H" not in host
|
|
or 'parser.add_argument("--protocol"' in host
|
|
):
|
|
errors.append("host H runner is missing or freely selectable")
|
|
except RuntimeError as error:
|
|
errors.append(str(error))
|
|
return errors
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--root", type=Path, required=True)
|
|
parser.add_argument("--retroarch-root", type=Path)
|
|
args = parser.parse_args()
|
|
errors = validate(args.root.resolve(), args.retroarch_root)
|
|
if errors:
|
|
for error in errors:
|
|
print(f"ERROR: {error}")
|
|
return 1
|
|
print("Phase-1.0H offline startup-argument validation passed")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|