260 lines
11 KiB
Python
260 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Validate the offline-only Phase-1.0M write-free defaults artifact."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
from typing import Any
|
|
|
|
|
|
PHASE = "PHASE_1_0M_WRITE_FREE_DEFAULTS"
|
|
STATUS = "OFFLINE_ARTIFACT_AUDITED_DEVICE_ACTION_BLOCKED"
|
|
SOURCE_COMMIT = "12cf1d783c41eb303987e49a5a920805a59ef7a4"
|
|
ARTIFACT_SHA256 = "c99a0856309a357ad2667d89b4924e4063ad214cae09c8a419457b0732f583cd"
|
|
MAP_SHA256 = "6768ffc7267b9b362c3b953571e5dfeed1e70004a3e19f41ef500c3f26272719"
|
|
DISASSEMBLY_SHA256 = "141a620484784e5e01ec0cb2a51f77b67712bc484aae6de0f139ba1e28922c87"
|
|
AUTHORIZATION_FIELDS = (
|
|
"ps5_connection_authorized", "device_transfer_authorized",
|
|
"device_execution_authorized", "result_receive_authorized",
|
|
"target_build_authorized", "installation_authorized",
|
|
"autoload_authorized", "device_write_authorized", "automatic_retry",
|
|
)
|
|
DEVICE_ACTION_FIELDS = (
|
|
"ps5_connected", "device_request_performed", "files_transferred",
|
|
"target_execution_performed", "result_received_from_device",
|
|
"device_write_performed", "installation_performed", "autoload_performed",
|
|
"retry_performed", "reconnect_performed",
|
|
)
|
|
DELIVERABLES = (
|
|
"docs/retroarch/phase-1.0m-write-free-defaults.md",
|
|
"manifests/retroarch/phase-1.0m-write-free-defaults.json",
|
|
"packaging/retroarch/phase10m/SHA256SUMS.txt",
|
|
"tools/validate_retroarch_phase10m.py",
|
|
"tests/test_retroarch_phase10m.py",
|
|
)
|
|
|
|
|
|
def load_json(path: Path) -> dict[str, Any]:
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
if not isinstance(value, dict):
|
|
raise ValueError("Phase-1.0M manifest is not an object")
|
|
return value
|
|
|
|
|
|
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_and_ineligible(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("name") == "retroarch_ps5_write_diag.elf"
|
|
and record.get("size") == 1845208
|
|
and record.get("sha256") == ARTIFACT_SHA256
|
|
and record.get("tracked") is False
|
|
and record.get("execution_eligible") is False
|
|
and record.get("transfer_eligible") is False
|
|
and record.get("installation_eligible") is False
|
|
)
|
|
|
|
|
|
def correction_is_narrow(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("guard") == "!defined(CHIMERA_PS5_NO_FILESYSTEM_WRITES)"
|
|
and record.get("skipped_under_write_free")
|
|
== ["path_is_directory", "path_mkdir"]
|
|
and record.get("path_derivation_preserved") is True
|
|
and record.get("legacy_migration_compiled") is False
|
|
and record.get("have_configfile") is False
|
|
and record.get("config_set_defaults_path_mkdir_calls") == 0
|
|
and record.get("generic_path_mkdir_symbol_retained") is True
|
|
and record.get("runtime_effect") == "UNPROVEN"
|
|
)
|
|
|
|
|
|
def firewall_is_preserved(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("linker_wrap_option_count") == 17
|
|
and record.get("all_linker_wrap_options_preserved") is True
|
|
and record.get("first_rejection_requests_shutdown") is True
|
|
and record.get("stop_before_i04_on_rejection") is True
|
|
and record.get("wrapper_errno") == "EROFS"
|
|
and record.get("surviving_wrappers") == [
|
|
{"symbol": "__wrap_open", "address": "0x475b0", "operation": "OPEN"},
|
|
{"symbol": "__wrap_fopen", "address": "0x47620", "operation": "OPEN"},
|
|
{"symbol": "__wrap_fwrite", "address": "0x476b0", "operation": "STREAM"},
|
|
]
|
|
)
|
|
|
|
|
|
def audit_is_exact(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("entry_point") == "0xff220"
|
|
and record.get("load_segment_permissions") == ["R_E", "R", "RW"]
|
|
and record.get("rwx_load_segments") == 0
|
|
and record.get("init_array_size") == 0
|
|
and record.get("fini_array_size") == 0
|
|
and record.get("tls_sections") == 0
|
|
and record.get("bss_size") == 697632
|
|
and record.get("undefined_dynamic_symbols") == 142
|
|
and record.get("relocations") == {
|
|
"total": 1055,
|
|
"R_X86_64_GLOB_DAT": 142,
|
|
"R_X86_64_RELATIVE": 913,
|
|
}
|
|
and record.get("dt_needed") == [
|
|
"libSceAudioOut.sprx",
|
|
"libSceLibcInternal.sprx",
|
|
"libScePad.sprx",
|
|
"libSceSystemService.sprx",
|
|
"libSceUserService.sprx",
|
|
"libSceVideoOut.sprx",
|
|
"libkernel_web.sprx",
|
|
]
|
|
and record.get("inherited_send_import_count") == 1
|
|
and all(record.get(field) is False for field in (
|
|
"socket_create_import", "connect_import", "listener_import",
|
|
"receive_import",
|
|
))
|
|
and record.get("gnm_imports") == []
|
|
and record.get("normalized_disassembly_sha256") == DISASSEMBLY_SHA256
|
|
and record.get("hardware_evidence") is False
|
|
)
|
|
|
|
|
|
def source_is_exact(retroarch_root: Path) -> bool:
|
|
source = subprocess.run(
|
|
["git", "show", f"{SOURCE_COMMIT}:configuration.c"],
|
|
cwd=retroarch_root, capture_output=True, text=True, check=True,
|
|
).stdout
|
|
makefile = subprocess.run(
|
|
["git", "show", f"{SOURCE_COMMIT}:Makefile.ps5"],
|
|
cwd=retroarch_root, capture_output=True, text=True, check=True,
|
|
).stdout
|
|
block = """ fill_pathname_join_special(
|
|
new_path,
|
|
settings->paths.directory_playlist,
|
|
FILE_PATH_BUILTIN,
|
|
sizeof(new_path));
|
|
|
|
#if !defined(CHIMERA_PS5_NO_FILESYSTEM_WRITES)
|
|
if (!path_is_directory(new_path))
|
|
path_mkdir(new_path);
|
|
#endif"""
|
|
wraps = (
|
|
"open", "openat", "fopen", "write", "fwrite", "creat", "freopen",
|
|
"tmpfile", "mkstemp", "mkdir", "rename", "unlink", "remove", "rmdir",
|
|
"chmod", "chown", "ftruncate",
|
|
)
|
|
return (
|
|
block in source
|
|
and "-DCHIMERA_PS5_NO_FILESYSTEM_WRITES" in makefile
|
|
and all(f"--wrap={name}" in makefile for name in wraps)
|
|
)
|
|
|
|
|
|
def validate(root: Path, retroarch_root: Path) -> list[str]:
|
|
errors: list[str] = []
|
|
for relative in DELIVERABLES:
|
|
if not (root / relative).is_file():
|
|
errors.append(f"missing deliverable: {relative}")
|
|
if not (retroarch_root / "docs/ps5-phase10m-write-free-defaults.md").is_file():
|
|
errors.append("missing sibling Phase-1.0M audit")
|
|
try:
|
|
record = load_json(
|
|
root / "manifests/retroarch/phase-1.0m-write-free-defaults.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 not artifact_is_exact_and_ineligible(record.get("artifact", {})):
|
|
errors.append("artifact identity or eligibility mismatch")
|
|
linker_map = record.get("linker_map", {})
|
|
if not (
|
|
linker_map.get("size") == 637728
|
|
and linker_map.get("sha256") == MAP_SHA256
|
|
and linker_map.get("tracked") is False
|
|
):
|
|
errors.append("linker-map identity mismatch")
|
|
if not correction_is_narrow(record.get("source_correction", {})):
|
|
errors.append("source correction is widened or overclaimed")
|
|
if not firewall_is_preserved(record.get("write_firewall", {})):
|
|
errors.append("global write firewall is incomplete")
|
|
if not audit_is_exact(record.get("artifact_audit", {})):
|
|
errors.append("artifact audit is incomplete or overclaims")
|
|
reproducibility = record.get("reproducibility", {})
|
|
if not (
|
|
reproducibility.get("clean_builds") == 2
|
|
and reproducibility.get("elf_byte_identical") is True
|
|
and reproducibility.get("map_byte_identical") is True
|
|
and reproducibility.get("normalized_disassembly_identical") is True
|
|
and reproducibility.get("warnings_as_errors_for_retroarch_target") is True
|
|
):
|
|
errors.append("reproducibility record is incomplete")
|
|
runtime = record.get("inherited_runtime_contract", {})
|
|
if not (
|
|
runtime.get("normal_sdk_crt_effects") == "INHERITED_PER_ADR_0010"
|
|
and runtime.get("live_runner_authorizes_artifact") is False
|
|
and all(runtime.get(field) == "UNPROVEN" for field in (
|
|
"i04", "sdl", "videoout", "visible_flip", "complete_cleanup",
|
|
))
|
|
):
|
|
errors.append("runtime contract invents progress or omits CRT effects")
|
|
actions = record.get("phase_actions", {})
|
|
if not all_false(actions, DEVICE_ACTION_FIELDS):
|
|
errors.append("Phase-1.0M records a device action")
|
|
if actions.get("target_build_performed") is not True or actions.get("target_artifact_created") is not True:
|
|
errors.append("offline target build is not recorded")
|
|
if not all_false(record.get("current_authorizations", {}), AUTHORIZATION_FIELDS):
|
|
errors.append("a current authorization is active")
|
|
tests = record.get("tests", {})
|
|
if not (
|
|
tests.get("chimera_gfx_ctest") == "54_OF_54_PASS"
|
|
and tests.get("retroarch_ps5_host_suite") == "PASS_WITH_ASAN_UBSAN"
|
|
and tests.get("phase10m_gfx_guardrails") == 20
|
|
and tests.get("phase10m_retroarch_guardrails") == 6
|
|
and tests.get("safety_audit") == "PASS"
|
|
and tests.get("chimera_gfx_secret_scan") == "WHOLE_TREE_PASS"
|
|
and tests.get("retroarch_secret_scan") == "PHASE_CHANGED_FILES_PASS"
|
|
and tests.get("retroarch_whole_tree_scan")
|
|
== "PREEXISTING_UPSTREAM_TEST_KEY_FIXTURES_FLAGGED"
|
|
and tests.get("hardware_evidence_from_phase10m") is False
|
|
):
|
|
errors.append("test evidence is incomplete or promoted to hardware evidence")
|
|
try:
|
|
if not source_is_exact(retroarch_root):
|
|
errors.append("sibling source guard or write firewall mismatch")
|
|
except subprocess.CalledProcessError as error:
|
|
errors.append(f"cannot inspect exact sibling source: {error}")
|
|
sums = (root / "packaging/retroarch/phase10m/SHA256SUMS.txt").read_text(encoding="utf-8")
|
|
if ARTIFACT_SHA256 not in sums or MAP_SHA256 not in sums:
|
|
errors.append("checksum record does not bind both ignored outputs")
|
|
tracked = subprocess.run(
|
|
["git", "ls-files"], cwd=root, capture_output=True, text=True, check=True
|
|
).stdout.splitlines()
|
|
if any(path.lower().endswith((".elf", ".self", ".sprx", ".pkg", ".map")) for path in tracked):
|
|
errors.append("target artifact or map is tracked")
|
|
return errors
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--root", type=Path, required=True)
|
|
parser.add_argument("--retroarch-root", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
errors = validate(args.root.resolve(), args.retroarch_root.resolve())
|
|
if errors:
|
|
for error in errors:
|
|
print(f"ERROR: {error}")
|
|
return 1
|
|
print("Phase-1.0M offline write-free defaults validation passed")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|