285 lines
13 KiB
Python
285 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Validate Phase-1.0Q public VideoOut evidence and fail-closed decision."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
from typing import Any
|
|
|
|
|
|
PHASE = "PHASE_1_0Q_PUBLIC_VIDEOOUT_EVIDENCE"
|
|
STATUS = "PUBLIC_VIDEOOUT_EVIDENCE_INSUFFICIENT_PARAMETER_CHANGE_BLOCKED"
|
|
SDK_COMMIT = "d2e2e585740362976a39fdd5ccf390f199a7bc37"
|
|
SDL_COMMIT = "0baf4ac49382b537ba449901b5b6d0d189bb1fbb"
|
|
AUTHORIZATION_FIELDS = (
|
|
"target_build_authorized", "ps5_connection_authorized",
|
|
"device_transfer_authorized", "device_execution_authorized",
|
|
"result_receive_authorized", "installation_authorized",
|
|
"autoload_authorized", "device_write_authorized", "automatic_retry",
|
|
)
|
|
|
|
|
|
def load_json(path: Path) -> dict[str, Any]:
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
if not isinstance(value, dict):
|
|
raise ValueError("evidence manifest is not an object")
|
|
return value
|
|
|
|
|
|
def sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as stream:
|
|
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def all_false(record: dict[str, Any], fields: tuple[str, ...]) -> bool:
|
|
return all(record.get(field) is False for field in fields)
|
|
|
|
|
|
def network_scope_is_bounded(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("github_metadata") is True
|
|
and record.get("official_source_archives") is True
|
|
and record.get("arbitrary_mirrors") is False
|
|
and record.get("downloaded_code_executed") is False
|
|
and record.get("ps5_address_used") is False
|
|
and record.get("ps5_connected") is False
|
|
)
|
|
|
|
|
|
def sdk_evidence_is_symbol_only(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("repository") == "https://github.com/ps5-payload-dev/sdk"
|
|
and record.get("release") == "v0.41"
|
|
and record.get("release_commit") == SDK_COMMIT
|
|
and record.get("latest_release_during_audit") is True
|
|
and record.get("master_commit_inspected")
|
|
== "a0d2bc60bdcc0a5ee9e790fa3b02fe5051a152d0"
|
|
and record.get("videoout_stub_size") == 25199
|
|
and record.get("videoout_stub_sha256")
|
|
== "da6cff9b3255e9ccb0440790f73e0265696ca47724cf8fe5950b579702940016"
|
|
and record.get("exports_only") is True
|
|
and record.get("public_videoout_header") is False
|
|
and record.get("signatures_or_layouts") is False
|
|
and record.get("return_semantics") is False
|
|
)
|
|
|
|
|
|
def sdl_lineage_is_single(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("repository") == "https://github.com/ps5-payload-dev/SDL"
|
|
and record.get("pinned_commit") == SDL_COMMIT
|
|
and record.get("initial_ps5_video_commit")
|
|
== "2682a5e31e8aee888538b0fb7253d8cadf2797a1"
|
|
and record.get("initial_source_size") == 7873
|
|
and record.get("initial_source_sha256")
|
|
== "1b9f917cac4e00ba1eccb22093e3ca079238f91c9a44ceefa9e23570ca9abab3"
|
|
and record.get("declaration_move_commit")
|
|
== "fdfa470a0fa33215c677193982dfe3651ac1321a"
|
|
and record.get("uint32_cleanup_commit")
|
|
== "14ac2ec2ab5889af47218a957e3456e613b13d3d"
|
|
and record.get("pinned_header_size") == 2878
|
|
and record.get("pinned_header_sha256")
|
|
== "e60766e0b43c2a7fceba2ada0c030e7f7b05e63885590092f2f8f5b9da34ed75"
|
|
and record.get("pinned_source_size") == 12162
|
|
and record.get("pinned_source_sha256")
|
|
== "44124546da132ea6e12b1f06a1808d61690c73460a65c3161e8453e8108684b6"
|
|
and record.get("independent_abi_source_cited") is False
|
|
and record.get("opaque_buffer_member_name") == "junk0"
|
|
and record.get("opaque_attribute_member_name") == "junk0"
|
|
and record.get("single_lineage_only") is True
|
|
)
|
|
|
|
|
|
def archives_do_not_corroborate(records: Any) -> bool:
|
|
expected = {
|
|
"ps5-payload-dev/libcross2d": ("829b167262d3567a048b6e69416f8e3be399c098", 1838927, "eb952a041396ba01224a890d14d41662b017488f487ddd4c4018ffddd4a59ae2", "SDL_CONSUMER"),
|
|
"ps5-payload-dev/pemu": ("4136088e13e825f33c9d0cc43ff9a8c8f749dcb0", 7540449, "9ed14a1ea432638992b614f9946bcbc9770f70b6f18ded5778fd6809a927eaeb", "SDL_CONSUMER"),
|
|
"ps5-payload-dev/FBNeo": ("ca4222ca2cd52215a673c565a5eae3589b352ba2", 15318689, "824d6e5b6b61555f5ecc598204052dcb4b1cf16d28be29b9594a44866e726ef3", "NO_DIRECT_IMPLEMENTATION"),
|
|
"ps5-payload-dev/LakeSnes": ("a2db690123649c7ffbc68a663af31efb3a41bf3f", 413067, "253a08957a68461a8535a4727ee1242c21b2dc40c8eb8c99bdb85af799ac3ca6", "SDL_CONSUMER"),
|
|
"ps5-payload-dev/yquake2": ("9e233b6a601c393be4ff1dcb1c003750346b8bbb", 2914919, "01ef41608b2be0ace119019901a6ea22f304542972bace714d1177cb7f567978", "SDL_CONSUMER"),
|
|
"ps5-payload-dev/pacbrew-repo": ("c2abcfcb60f569128abd0e8e70ad03a67bee5ea7", 143496, "17be50219ce41772cfc50ea9274b0a62a2259162556b6eeecfaed4a691822872", "REFERENCES_SAME_SDL_FORK"),
|
|
}
|
|
if not isinstance(records, list) or len(records) != 6:
|
|
return False
|
|
if {item.get("repository") for item in records} != set(expected):
|
|
return False
|
|
for item in records:
|
|
repository = item["repository"]
|
|
commit, size, digest, relationship = expected[repository]
|
|
if item.get("direct_videoout_hits") != 0:
|
|
return False
|
|
if item.get("relationship") != relationship:
|
|
return False
|
|
if item.get("commit") != commit or item.get("size") != size:
|
|
return False
|
|
if item.get("sha256") != digest:
|
|
return False
|
|
return True
|
|
|
|
|
|
def ps4_analogue_is_not_promoted(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("repository")
|
|
== "https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain"
|
|
and record.get("commit") == "0a1aaf9dd4a92695538bdeb09fb056d06dd11725"
|
|
and record.get("four_argument_submit") is True
|
|
and record.get("mode_one_documented_as_vsync") is True
|
|
and record.get("classification") == "PS4_ANALOG_ONLY"
|
|
and record.get("accepted_as_ps5_abi") is False
|
|
)
|
|
|
|
|
|
def evidence_matrix_is_fail_closed(record: dict[str, Any]) -> bool:
|
|
return record == {
|
|
"submit_signature": "PARTIAL_SINGLE_LINEAGE",
|
|
"argument_positions": "PARTIAL_SINGLE_LINEAGE",
|
|
"mode_one_semantics": "UNPROVEN_ON_PS5",
|
|
"frame_zero_validity": "UNPROVEN_ON_PS5",
|
|
"video_buffer_layout": "UNPROVEN",
|
|
"video_attribute_layout": "UNPROVEN",
|
|
"format_constant_semantics": "UNPROVEN",
|
|
"register_return_semantics": "PARTIAL_RUNTIME_ONLY",
|
|
"submit_error_semantics": "UNPROVEN",
|
|
"flip_master_or_active_app_requirement": "UNPROVEN",
|
|
"status_query_signatures_and_layouts": "UNPROVEN",
|
|
}
|
|
|
|
|
|
def decision_is_blocked(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("independent_ps5_abi_corroboration_found") is False
|
|
and record.get("root_cause_resolved") is False
|
|
and record.get("parameter_change_allowed") is False
|
|
and record.get("new_videoout_call_allowed") is False
|
|
and record.get("phase10r_target_artifact_allowed") is False
|
|
and record.get("phase10r_device_action_allowed") is False
|
|
and record.get("safe_next_steps") == [
|
|
"OFFLINE_LAUNCH_CONTEXT_COMPARISON",
|
|
"HOST_OR_SOFTWARE_ONLY_INTEGRATION",
|
|
]
|
|
)
|
|
|
|
|
|
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
|
|
|
|
|
|
def validate(root: Path, sdk_root: Path, sdl_root: Path) -> list[str]:
|
|
errors: list[str] = []
|
|
try:
|
|
record = load_json(
|
|
root / "manifests/retroarch/phase-1.0q-public-videoout-evidence.json")
|
|
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
return [str(error)]
|
|
if record.get("phase") != PHASE or record.get("status") != STATUS:
|
|
errors.append("phase/status mismatch")
|
|
if record.get("start_commit") != "df470c691f375b73768fd0d4a51dc2d35523e71d":
|
|
errors.append("start commit mismatch")
|
|
if not network_scope_is_bounded(record.get("network_scope", {})):
|
|
errors.append("network scope is broadened")
|
|
if not all_false(record.get("authorizations", {}), AUTHORIZATION_FIELDS):
|
|
errors.append("authorization remains active")
|
|
if not sdk_evidence_is_symbol_only(record.get("sdk", {})):
|
|
errors.append("SDK evidence was promoted")
|
|
if not sdl_lineage_is_single(record.get("sdl_lineage", {})):
|
|
errors.append("SDL lineage is incomplete or promoted")
|
|
if not archives_do_not_corroborate(record.get("official_project_archives")):
|
|
errors.append("official archive inventory mismatch")
|
|
storage = record.get("temporary_research_storage", {})
|
|
if storage != {
|
|
"archive_count": 6,
|
|
"downloaded_bytes": 28169547,
|
|
"retained_in_git": False,
|
|
"cleaned_after_static_scan": False,
|
|
"cleanup_status": "RETAINED_TEMP_LOCAL_DELETE_POLICY_BLOCKED",
|
|
}:
|
|
errors.append("temporary research storage boundary mismatch")
|
|
additional = record.get("additional_ps5_source", {})
|
|
if not (
|
|
additional.get("repository") == "https://github.com/PS5Dev/PS5SDK"
|
|
and additional.get("commit") == "a2e03a2a0231a3a3397fa6cd087a01ca6d04f273"
|
|
and additional.get("videoout_implementation_found") is False
|
|
and additional.get("classification") == "NO_CORROBORATION"
|
|
):
|
|
errors.append("additional PS5 source was promoted")
|
|
if not ps4_analogue_is_not_promoted(record.get("ps4_analogue", {})):
|
|
errors.append("PS4 analogue was promoted to PS5 ABI")
|
|
if not evidence_matrix_is_fail_closed(record.get("evidence_matrix", {})):
|
|
errors.append("evidence matrix mismatch")
|
|
if not decision_is_blocked(record.get("decision", {})):
|
|
errors.append("parameter or device path is not blocked")
|
|
performed = record.get("performed_actions", {})
|
|
if not all(value is False for value in performed.values()) or set(performed) != {
|
|
"target_source_changed", "target_artifact_created", "ps5_connected",
|
|
"device_transfer_performed", "target_execution_performed",
|
|
"result_received_from_device",
|
|
}:
|
|
errors.append("performed-action boundary mismatch")
|
|
tests = record.get("tests", {})
|
|
if not (
|
|
tests.get("chimera_gfx_ctest") == "62_OF_62_PASS"
|
|
and tests.get("phase10q_guardrails") == 20
|
|
and tests.get("network_required_by_tests") is False
|
|
and tests.get("hardware_claim_from_host_test") is False
|
|
):
|
|
errors.append("test evidence mismatch")
|
|
try:
|
|
if git(sdk_root, "rev-parse", "HEAD").strip() != SDK_COMMIT:
|
|
errors.append("local SDK release commit mismatch")
|
|
if git(sdl_root, "rev-parse", "HEAD").strip() != SDL_COMMIT:
|
|
errors.append("local SDL commit mismatch")
|
|
if git(sdk_root, "status", "--porcelain"):
|
|
errors.append("local SDK tree is dirty")
|
|
if git(sdl_root, "status", "--porcelain"):
|
|
errors.append("local SDL tree is dirty")
|
|
stub = sdk_root / "sce_stubs/libSceVideoOut.c"
|
|
header = sdl_root / "src/video/ps5/SDL_ps5video.h"
|
|
source = sdl_root / "src/video/ps5/SDL_ps5video.c"
|
|
if stub.stat().st_size != 25199 or sha256(stub) != record["sdk"]["videoout_stub_sha256"]:
|
|
errors.append("local VideoOut stub identity mismatch")
|
|
if header.stat().st_size != 2878 or sha256(header) != record["sdl_lineage"]["pinned_header_sha256"]:
|
|
errors.append("local SDL header identity mismatch")
|
|
if source.stat().st_size != 12162 or sha256(source) != record["sdl_lineage"]["pinned_source_sha256"]:
|
|
errors.append("local SDL source identity mismatch")
|
|
header_text = header.read_text(encoding="utf-8")
|
|
if "uint64_t junk0[3]" not in header_text or "uint8_t junk0[80]" not in header_text:
|
|
errors.append("opaque SDL declarations changed")
|
|
tracked = git(root, "ls-files").splitlines()
|
|
if any(path.lower().endswith((".elf", ".self", ".sprx", ".pkg", ".zip"))
|
|
for path in tracked):
|
|
errors.append("target or downloaded archive is tracked")
|
|
except (OSError, RuntimeError, KeyError) 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("--sdk-root", type=Path, required=True)
|
|
parser.add_argument("--sdl-root", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
errors = validate(args.root.resolve(), args.sdk_root.resolve(), args.sdl_root.resolve())
|
|
if errors:
|
|
for error in errors:
|
|
print(f"ERROR: {error}")
|
|
return 1
|
|
print("Phase-1.0Q public VideoOut evidence validation passed")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|