237 lines
9.2 KiB
Python
237 lines
9.2 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Validate the consumed Phase-1.0K result and Phase-1.0L analysis."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
from typing import Any
|
|
|
|
|
|
PHASE = "PHASE_1_0L_WRITE_FIREWALL_DEVICE_RESULT_ANALYSIS"
|
|
STATUS = "ONE_SHOT_AUTHORIZATION_CONSUMED_FIRST_WRITE_MKDIR"
|
|
SOURCE_COMMIT = "2e385265ab893dbba5898178dde5e108d3dbfa19"
|
|
ARTIFACT_SHA256 = "6ff0f7ea391da5f15ea43512a871078133e896a6900ae9f8f3fa75711abb8009"
|
|
TRACE_SHA256 = "d06613712df01a51ed4af83b96b844668456589783b6202d669d27b4fd6b915a"
|
|
STAGES = ["D00", "D01", "D02", "I00", "I01", "I02", "I03", "D13", "D12"]
|
|
AUTHORIZATION_FIELDS = (
|
|
"ps5_connection_authorized",
|
|
"device_transfer_authorized",
|
|
"device_execution_authorized",
|
|
"result_receive_authorized",
|
|
"installation_authorized",
|
|
"autoload_authorized",
|
|
"device_write_authorized",
|
|
"automatic_retry",
|
|
)
|
|
DELIVERABLES = (
|
|
"docs/retroarch/phase-1.0l-write-firewall-result-analysis.md",
|
|
"manifests/retroarch/phase-1.0l-write-firewall-result-analysis.json",
|
|
"tools/validate_retroarch_phase10l.py",
|
|
"tests/test_retroarch_phase10l.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.0L 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 result_is_exact(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("magic") == "CHD10J01"
|
|
and record.get("frame_size") == 64
|
|
and record.get("frame_count") == len(STAGES) == 9
|
|
and record.get("stages") == STAGES
|
|
and record.get("raw_results")
|
|
== {
|
|
"D02": 0,
|
|
"D13_first_blocked_write": 6,
|
|
"D13_write_block_count": 1,
|
|
"D12_shutdown_reason": 6,
|
|
"D12_first_error": 118,
|
|
}
|
|
and record.get("terminal_stage") == "D12"
|
|
and record.get("classification")
|
|
== "PRE_I04_FAIL_CLOSED_FIRST_WRITE_MKDIR"
|
|
)
|
|
|
|
|
|
def transport_is_one_shot(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("connect_count") == 1
|
|
and record.get("sendall_count") == 1
|
|
and record.get("bytes_sent") == 1845208
|
|
and record.get("shutdown_write_count") == 1
|
|
and record.get("recv_call_count") == 2
|
|
and record.get("received_byte_count") == 741
|
|
and record.get("raw_stream_stored_bytes") == 741
|
|
and record.get("raw_stream_truncated") is False
|
|
and record.get("retry_count") == 0
|
|
and record.get("reconnect_count") == 0
|
|
and record.get("close_called") is True
|
|
and record.get("valid_terminal_frame_received") is True
|
|
)
|
|
|
|
|
|
def source_binding_is_bounded(record: dict[str, Any]) -> bool:
|
|
return (
|
|
record.get("first_blocked_write_operation") == "MKDIR"
|
|
and record.get("write_block_count") == 1
|
|
and record.get("shutdown_reason") == "WRITE_FIREWALL"
|
|
and record.get("first_error") == "E118_WRITE_BLOCKED"
|
|
and record.get("runtime_path") == "UNOBSERVED"
|
|
and record.get("source_route")
|
|
== "CONFIG_SET_DEFAULTS_BUILTIN_PLAYLIST_DIRECTORY"
|
|
and record.get("source_line") == "configuration.c:3357"
|
|
and record.get("path_mkdir_cb_relocation") == "0x17c280 -> 0x15ae0"
|
|
and record.get("reached_i04") is False
|
|
and record.get("sdl_video") == "NOT_REACHED"
|
|
and record.get("videoout") == "NOT_REACHED"
|
|
and record.get("rendering") == "NOT_REACHED"
|
|
and record.get("complete_cleanup") == "UNPROVEN"
|
|
and record.get("safe_exit") == "UNPROVEN"
|
|
)
|
|
|
|
|
|
def git_show(root: Path, commit: str, path: str) -> str:
|
|
return subprocess.run(
|
|
["git", "show", f"{commit}:{path}"],
|
|
cwd=root,
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
).stdout
|
|
|
|
|
|
def source_evidence_is_exact(retroarch_root: Path) -> bool:
|
|
configuration = git_show(retroarch_root, SOURCE_COMMIT, "configuration.c")
|
|
smoke_c = git_show(
|
|
retroarch_root, SOURCE_COMMIT, "frontend/drivers/platform_ps5_smoke.c"
|
|
)
|
|
smoke_h = git_show(
|
|
retroarch_root, SOURCE_COMMIT, "frontend/drivers/platform_ps5_smoke.h"
|
|
)
|
|
retroarch = git_show(retroarch_root, SOURCE_COMMIT, "retroarch.c")
|
|
return (
|
|
"void config_load(void *data)" in configuration
|
|
and "config_set_defaults(global);" in configuration
|
|
and "if (!path_is_directory(new_path))\n path_mkdir(new_path);"
|
|
in configuration
|
|
and "CHIMERA_REJECT_INT_WRAPPER(mkdir, CHIMERA_SMOKE_WRITE_MKDIR"
|
|
in smoke_c
|
|
and "CHIMERA_SMOKE_WRITE_MKDIR" in smoke_h
|
|
and "CHIMERA_PS5_DIAG_D13" in smoke_c
|
|
and "if (chimera_ps5_smoke_get_status()->shutdown_requested)"
|
|
in retroarch
|
|
)
|
|
|
|
|
|
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-phase10k-device-result.md").is_file():
|
|
errors.append("missing sibling consumed-result document")
|
|
try:
|
|
record = load_json(
|
|
root / "manifests/retroarch/phase-1.0l-write-firewall-result-analysis.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")
|
|
artifact = record.get("artifact", {})
|
|
if not (
|
|
artifact.get("size") == 1845208
|
|
and artifact.get("sha256") == ARTIFACT_SHA256
|
|
and artifact.get("execution_eligible") is False
|
|
and artifact.get("transfer_eligible") is False
|
|
and artifact.get("installation_eligible") is False
|
|
and artifact.get("tracked") is False
|
|
):
|
|
errors.append("artifact identity or ineligible state mismatch")
|
|
authorization = record.get("authorization", {})
|
|
if not (
|
|
authorization.get("consumed") is True
|
|
and authorization.get("authority_inherited_by_future_action") is False
|
|
):
|
|
errors.append("authorization is reusable")
|
|
if not all_false(record.get("current_authorizations", {}), AUTHORIZATION_FIELDS):
|
|
errors.append("a current authorization remains active")
|
|
actions = record.get("performed_actions", {})
|
|
if not (
|
|
actions.get("ps5_connected") is True
|
|
and actions.get("files_transferred") is True
|
|
and actions.get("target_execution_performed") is True
|
|
and actions.get("result_received_from_device") is True
|
|
and actions.get("device_filesystem_write_performed") is False
|
|
and actions.get("normal_sdk_crt_effects") == "INHERITED_PER_ADR_0010"
|
|
and actions.get("retry_performed") is False
|
|
and actions.get("reconnect_performed") is False
|
|
and actions.get("sdl_entered") is False
|
|
and actions.get("videoout_entered") is False
|
|
):
|
|
errors.append("performed action record is incomplete or overclaims")
|
|
if not transport_is_one_shot(record.get("transport", {})):
|
|
errors.append("transport is not exact one-shot")
|
|
trace = record.get("trace", {})
|
|
if not (
|
|
trace.get("tracked") is False
|
|
and trace.get("size") == 5841
|
|
and trace.get("sha256") == TRACE_SHA256
|
|
and trace.get("parser_errors") == []
|
|
):
|
|
errors.append("ignored trace identity/parser result mismatch")
|
|
if not result_is_exact(record.get("protocol_result", {})):
|
|
errors.append("protocol result mismatch")
|
|
if not source_binding_is_bounded(record.get("source_binding", {})):
|
|
errors.append("source binding overclaims or omits a boundary")
|
|
candidate = record.get("next_offline_candidate", {})
|
|
if not (
|
|
candidate.get("preserve_path_derivation") is True
|
|
and candidate.get("preserve_global_write_firewall") is True
|
|
and candidate.get("target_build_authorized") is False
|
|
and candidate.get("device_action_authorized") is False
|
|
):
|
|
errors.append("next offline candidate weakens a safety gate")
|
|
try:
|
|
if not source_evidence_is_exact(retroarch_root):
|
|
errors.append("source evidence is incomplete")
|
|
except subprocess.CalledProcessError as error:
|
|
errors.append(f"cannot inspect exact artifact source: {error}")
|
|
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 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.0L consumed write-firewall result validation passed")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|