This commit is contained in:
@@ -0,0 +1,390 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Generate deterministic Phase-0.7 static ELF evidence without execution."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
EXPECTED_COMMITS = {
|
||||
"lifecycle": "fe08300339a13f899fb78ea404ada381a5cba87c",
|
||||
"loader": "197623058f509eddde18868dafcb92fdcac66464",
|
||||
"manager": "e23d94ff91233aa770e2342800c1467875bdef44",
|
||||
}
|
||||
EXPECTED_HASHES = {
|
||||
"lifecycle": "bfb4a5cc768e162fe4c2fddf41c3978e152722918a39085277fd172cb95a7182",
|
||||
"loader": "63e810982471eb40cae3a20aa9df9a0a02892f420e429874fae4e99aa400b561",
|
||||
"manager": "8fecf8241a46246eddbd21e8bb4d875f5d76f1f4f4c6a11384df1f131aa5e5b1",
|
||||
}
|
||||
EXPECTED_SIZES = {"lifecycle": 112680, "loader": 397000, "manager": 99560}
|
||||
LIFECYCLE_IMPORTS = {"_exit", "sceKernelSendNotificationRequest"}
|
||||
LIFECYCLE_NEEDED = {"libSceLibcInternal.sprx", "libkernel_web.sprx"}
|
||||
LIFECYCLE_FORBIDDEN = re.compile(
|
||||
r"(sceGnm|VideoOut|SDL_|sceNet|socket|connect|dlopen|dlsym|"
|
||||
r"kernel_copyin|kernel_copyout|kernel_set_ucred|__patch_init)"
|
||||
)
|
||||
SENSITIVE_CATEGORIES = {
|
||||
"dynamic_loading": re.compile(
|
||||
r"(^|_)(dlopen|dlsym)$|^__(dlopen|dlsym)$|dynlib|"
|
||||
r"LoadStartModule|StopUnloadModule"
|
||||
),
|
||||
"graphics_or_display": re.compile(
|
||||
r"sceGnm|[Vv]ideoOut|SDL_|[Gg]pu|[Mm][Mm][Ii][Oo]"
|
||||
),
|
||||
"kernel_runtime_write": re.compile(
|
||||
r"__patch_init|kernel_copy(in|out)|kernel_set_ucred|"
|
||||
r"kernel_overlap_sockets|syscall.*(patch|set)|"
|
||||
r"(patch|set).*syscall"
|
||||
),
|
||||
"network": re.compile(r"(^|_)(socket|connect|listen|accept)$|sceNet"),
|
||||
"ptrace_or_jit": re.compile(r"ptrace|PT_[A-Z]|[Jj][Ii][Tt]"),
|
||||
}
|
||||
|
||||
|
||||
def sha256_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
return sha256_bytes(path.read_bytes())
|
||||
|
||||
|
||||
def run(command: list[str]) -> str:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
return result.stdout.replace("\r\n", "\n")
|
||||
|
||||
|
||||
def git_head(path: Path, expected: str, lifecycle_source: bool = False) -> str:
|
||||
git = [
|
||||
"git",
|
||||
"-c",
|
||||
"core.autocrlf=true",
|
||||
"-c",
|
||||
"core.fileMode=false",
|
||||
"-C",
|
||||
str(path),
|
||||
]
|
||||
head = run([*git, "rev-parse", "HEAD"]).strip()
|
||||
if lifecycle_source:
|
||||
subprocess.run(
|
||||
[*git, "merge-base", "--is-ancestor", expected, head],
|
||||
check=True,
|
||||
)
|
||||
changed = subprocess.run(
|
||||
[
|
||||
*git,
|
||||
"diff",
|
||||
"--quiet",
|
||||
expected,
|
||||
"--",
|
||||
"samples/lifecycle_probe/main.c",
|
||||
],
|
||||
check=False,
|
||||
)
|
||||
if changed.returncode != 0:
|
||||
raise ValueError(f"{path}: lifecycle source differs from {expected}")
|
||||
return expected
|
||||
if head != expected:
|
||||
raise ValueError(f"{path}: expected {expected}, got {head}")
|
||||
if run([*git, "status", "--porcelain"]):
|
||||
raise ValueError(f"{path}: source tree is dirty")
|
||||
return head
|
||||
|
||||
|
||||
def parse_undefined(symbols: str) -> list[str]:
|
||||
imports: set[str] = set()
|
||||
for line in symbols.splitlines():
|
||||
match = re.search(r"\bU\s+(\S+)\s*$", line)
|
||||
if match:
|
||||
imports.add(match.group(1))
|
||||
return sorted(imports)
|
||||
|
||||
|
||||
def parse_needed(dynamic: str) -> list[str]:
|
||||
return sorted(set(re.findall(r"Shared library: \[([^\]]+)\]", dynamic)))
|
||||
|
||||
|
||||
def parse_callgraph(disassembly: str) -> list[dict[str, Any]]:
|
||||
current = "<outside-symbol>"
|
||||
edges: list[dict[str, Any]] = []
|
||||
for line in disassembly.splitlines():
|
||||
function = re.match(r"^[0-9a-fA-F]+\s+<([^>]+)>:$", line)
|
||||
if function:
|
||||
current = function.group(1)
|
||||
continue
|
||||
call = re.match(
|
||||
r"^\s*([0-9a-fA-F]+):.*\bcallq?\b\s+(.+?)\s*$", line
|
||||
)
|
||||
if not call:
|
||||
continue
|
||||
operand = call.group(2).strip()
|
||||
target_match = re.search(r"<([^>]+)>", operand)
|
||||
edges.append(
|
||||
{
|
||||
"address": f"0x{call.group(1).lower()}",
|
||||
"from": current,
|
||||
"indirect": "*" in operand or target_match is None,
|
||||
"target": target_match.group(1) if target_match else operand,
|
||||
}
|
||||
)
|
||||
return edges
|
||||
|
||||
|
||||
def function_name(name: str) -> str:
|
||||
return re.sub(r"\+0x[0-9a-fA-F]+$", "", name)
|
||||
|
||||
|
||||
def defined_functions(disassembly: str) -> set[str]:
|
||||
return {
|
||||
match.group(1)
|
||||
for match in re.finditer(
|
||||
r"^[0-9a-fA-F]+\s+<([^>]+)>:$", disassembly, re.MULTILINE
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def map_symbols(linker_map: str) -> set[str]:
|
||||
symbols = set()
|
||||
pattern = re.compile(
|
||||
r"^\s*[0-9a-fA-F]+\s+[0-9a-fA-F]+\s+[0-9a-fA-F]+"
|
||||
r"\s+\d+\s+([A-Za-z_][A-Za-z0-9_.$@]*)\s*$"
|
||||
)
|
||||
for line in linker_map.splitlines():
|
||||
match = pattern.match(line)
|
||||
if match:
|
||||
symbols.add(match.group(1))
|
||||
return symbols
|
||||
|
||||
|
||||
def direct_reachable(
|
||||
callgraph: list[dict[str, Any]], entrypoint: str
|
||||
) -> set[str]:
|
||||
adjacency: dict[str, set[str]] = {}
|
||||
for edge in callgraph:
|
||||
if edge["indirect"]:
|
||||
continue
|
||||
source = function_name(edge["from"])
|
||||
target = function_name(edge["target"])
|
||||
adjacency.setdefault(source, set()).add(target)
|
||||
reachable = {entrypoint}
|
||||
pending = [entrypoint]
|
||||
while pending:
|
||||
source = pending.pop()
|
||||
for target in adjacency.get(source, set()):
|
||||
if target not in reachable:
|
||||
reachable.add(target)
|
||||
pending.append(target)
|
||||
return reachable
|
||||
|
||||
|
||||
def sensitive_inventory(
|
||||
disassembly: str,
|
||||
linker_map: str,
|
||||
callgraph: list[dict[str, Any]],
|
||||
entrypoint: str,
|
||||
) -> dict[str, Any]:
|
||||
defined = defined_functions(disassembly)
|
||||
mapped = map_symbols(linker_map)
|
||||
reachable = direct_reachable(callgraph, entrypoint)
|
||||
categories = {}
|
||||
for category, pattern in SENSITIVE_CATEGORIES.items():
|
||||
disassembly_linked = sorted(
|
||||
name for name in defined if pattern.search(name)
|
||||
)
|
||||
map_linked = sorted(name for name in mapped if pattern.search(name))
|
||||
categories[category] = {
|
||||
"directly_reachable_from_entrypoint": sorted(
|
||||
name for name in disassembly_linked if name in reachable
|
||||
),
|
||||
"linked": sorted(set(disassembly_linked) | set(map_linked)),
|
||||
"linked_in_disassembly": disassembly_linked,
|
||||
"linked_in_linker_map": map_linked,
|
||||
}
|
||||
return {
|
||||
"categories": categories,
|
||||
"direct_call_reachability_available": entrypoint in defined,
|
||||
"direct_call_reachability_only": True,
|
||||
"entrypoint": entrypoint,
|
||||
"indirect_call_edges_retained_but_not_resolved": sum(
|
||||
1 for edge in callgraph if edge["indirect"]
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def normalize_map(data: str) -> str:
|
||||
return re.sub(
|
||||
r"/tmp/([A-Za-z0-9_]+)-[0-9a-f]{6}\.o",
|
||||
r"/tmp/\1-<deterministic-temp>.o",
|
||||
data.replace("\r\n", "\n"),
|
||||
)
|
||||
|
||||
|
||||
def analyze(
|
||||
*,
|
||||
name: str,
|
||||
first: Path,
|
||||
second: Path,
|
||||
map_path: Path,
|
||||
nm: Path,
|
||||
readelf: Path,
|
||||
objdump: Path,
|
||||
output: Path,
|
||||
) -> dict[str, Any]:
|
||||
first_bytes = first.read_bytes()
|
||||
second_bytes = second.read_bytes()
|
||||
if first_bytes != second_bytes:
|
||||
raise ValueError(f"{name}: the two clean builds are not byte-identical")
|
||||
digest = sha256_bytes(first_bytes)
|
||||
if digest != EXPECTED_HASHES[name] or len(first_bytes) != EXPECTED_SIZES[name]:
|
||||
raise ValueError(f"{name}: identity differs from the reviewed profile")
|
||||
|
||||
dynamic = run([str(readelf), "-d", str(first)])
|
||||
symbols = run([str(nm), "-u", str(first)])
|
||||
headers = run([str(readelf), "-h", "-l", "-S", str(first)])
|
||||
relocations = run([str(readelf), "-r", str(first)])
|
||||
all_symbols = run([str(readelf), "-Ws", str(first)])
|
||||
disassembly = run([str(objdump), "-d", str(first)])
|
||||
callgraph = parse_callgraph(disassembly)
|
||||
normalized_map = normalize_map(map_path.read_text(encoding="utf-8"))
|
||||
sensitive = sensitive_inventory(
|
||||
disassembly, normalized_map, callgraph, "_start"
|
||||
)
|
||||
|
||||
reports = {
|
||||
"callgraph": json.dumps(callgraph, indent=2, sort_keys=True) + "\n",
|
||||
"disassembly": disassembly,
|
||||
"dynamic": dynamic,
|
||||
"headers_sections": headers,
|
||||
"linker_map_normalized": normalized_map,
|
||||
"relocations": relocations,
|
||||
"symbols": all_symbols,
|
||||
"undefined": symbols,
|
||||
}
|
||||
report_hashes: dict[str, str] = {}
|
||||
for report_name, content in reports.items():
|
||||
report_path = output / f"{name}.{report_name}.txt"
|
||||
report_path.write_text(content, encoding="utf-8", newline="\n")
|
||||
report_hashes[report_name] = sha256_file(report_path)
|
||||
|
||||
imports = parse_undefined(symbols)
|
||||
needed = parse_needed(dynamic)
|
||||
if name == "lifecycle":
|
||||
if set(imports) != LIFECYCLE_IMPORTS:
|
||||
raise ValueError(f"lifecycle: unexpected imports {imports}")
|
||||
if set(needed) != LIFECYCLE_NEEDED:
|
||||
raise ValueError(f"lifecycle: unexpected DT_NEEDED {needed}")
|
||||
forbidden_matches = sorted(set(LIFECYCLE_FORBIDDEN.findall(symbols)))
|
||||
if forbidden_matches:
|
||||
raise ValueError(f"lifecycle: forbidden import {forbidden_matches}")
|
||||
if b"phase07-fw960-v1" not in first_bytes or b"9.60" not in first_bytes:
|
||||
raise ValueError("lifecycle: firmware/build gates are absent")
|
||||
|
||||
return {
|
||||
"byte_identical_clean_builds": True,
|
||||
"call_edges": callgraph,
|
||||
"dt_needed": needed,
|
||||
"filename": first.name,
|
||||
"imports": imports,
|
||||
"report_sha256": report_hashes,
|
||||
"sensitive_static_inventory": sensitive,
|
||||
"sha256": digest,
|
||||
"size": len(first_bytes),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
parser.add_argument("--loader-source", type=Path, required=True)
|
||||
parser.add_argument("--manager-source", type=Path, required=True)
|
||||
parser.add_argument("--artifact-dir", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--manifest-output", type=Path, required=True)
|
||||
parser.add_argument("--nm", type=Path, required=True)
|
||||
parser.add_argument("--readelf", type=Path, required=True)
|
||||
parser.add_argument("--objdump", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
root = args.root.resolve()
|
||||
sources = {
|
||||
"lifecycle": root,
|
||||
"loader": args.loader_source.resolve(),
|
||||
"manager": args.manager_source.resolve(),
|
||||
}
|
||||
source_commits = {
|
||||
name: git_head(
|
||||
path, EXPECTED_COMMITS[name], lifecycle_source=name == "lifecycle"
|
||||
)
|
||||
for name, path in sources.items()
|
||||
}
|
||||
artifact_dir = args.artifact_dir.resolve()
|
||||
output = args.output.resolve()
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
artifacts: dict[str, Any] = {}
|
||||
for name, stem in {
|
||||
"lifecycle": "chimera-gfx-lifecycle-probe-phase07",
|
||||
"loader": "chimera-elfldr-phase07",
|
||||
"manager": "chimera-pldmgr-phase07",
|
||||
}.items():
|
||||
artifacts[name] = analyze(
|
||||
name=name,
|
||||
first=artifact_dir / f"{stem}-a.elf",
|
||||
second=artifact_dir / f"{stem}-b.elf",
|
||||
map_path=artifact_dir / f"{stem}-b.map",
|
||||
nm=args.nm.resolve(strict=True),
|
||||
readelf=args.readelf.resolve(strict=True),
|
||||
objdump=args.objdump.resolve(strict=True),
|
||||
output=output,
|
||||
)
|
||||
|
||||
document = {
|
||||
"schema_version": 1,
|
||||
"decision": "READY_FOR_HARDENED_RUNTIME_DEPLOYMENT",
|
||||
"firmware": "9.60",
|
||||
"offline_only": True,
|
||||
"ps5_actions": {
|
||||
"connected": False,
|
||||
"installed": False,
|
||||
"transferred": False,
|
||||
"executed": False,
|
||||
},
|
||||
"source_commits": source_commits,
|
||||
"artifacts": artifacts,
|
||||
"forbidden_lifecycle_imports": [],
|
||||
"permanent_denylist_sha256": (
|
||||
"4be1c17b4964f2b68c39b5145bc4af4619c32512d60269ecf5c39728b390fa63"
|
||||
),
|
||||
}
|
||||
machine_output = output / "phase-0.7-offline-audit.json"
|
||||
content = json.dumps(document, indent=2, sort_keys=True) + "\n"
|
||||
machine_output.write_text(content, encoding="utf-8", newline="\n")
|
||||
args.manifest_output.resolve().write_text(
|
||||
content, encoding="utf-8", newline="\n"
|
||||
)
|
||||
print(
|
||||
"Phase-0.7 audit passed: three byte-identical ELF pairs, exact "
|
||||
"firmware 9.60 lifecycle imports, and no PS5 action"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (OSError, subprocess.CalledProcessError, ValueError) as error:
|
||||
print(f"Phase-0.7 artifact audit failed: {error}")
|
||||
raise SystemExit(1) from error
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Conservative source/build audit for the Phase-0 non-rendering boundary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
FORBIDDEN_CALLS = (
|
||||
r"\bsceGnm[A-Za-z0-9_]*\s*\(",
|
||||
r"\bsceVideoOutSubmitFlip\s*\(",
|
||||
r"\b(mmap|mprotect|ioctl)\s*\(",
|
||||
)
|
||||
|
||||
FORBIDDEN_TERMS = (
|
||||
"MMIO",
|
||||
"SCE_PROSPERO_SDK_DIR", # should exist only in the external SDK, not project code
|
||||
"smu_",
|
||||
"fan_control",
|
||||
"clock_boost",
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
root = args.root.resolve()
|
||||
errors: list[str] = []
|
||||
|
||||
phase0_sources = (
|
||||
sorted((root / "src").rglob("*.c"))
|
||||
+ sorted((root / "adapters").rglob("*.c"))
|
||||
+ sorted((root / "samples/capability_probe").rglob("*.c"))
|
||||
)
|
||||
all_sources = phase0_sources + sorted(
|
||||
(root / "samples/phase1_videoout_clear").rglob("*.c")
|
||||
)
|
||||
combined = "\n".join(path.read_text(encoding="utf-8") for path in all_sources)
|
||||
|
||||
for pattern in FORBIDDEN_CALLS:
|
||||
if re.search(pattern, combined):
|
||||
errors.append(f"forbidden Phase-0 call expression matched: {pattern}")
|
||||
for term in FORBIDDEN_TERMS:
|
||||
if term.lower() in combined.lower():
|
||||
errors.append(f"forbidden Phase-0 implementation term: {term}")
|
||||
|
||||
platform = (root / "src/backends/ps5/probe_platform.c").read_text(encoding="utf-8")
|
||||
if platform.count("dlsym(") != 1:
|
||||
errors.append("PS5 loader must contain exactly one dlsym call site")
|
||||
if "address = dlsym(" not in platform or "address = NULL;" not in platform:
|
||||
errors.append("resolved address must be local and explicitly discarded")
|
||||
if re.search(r"\([^\n;]*\(\s*\*[^)]*\)\s*\)\s*dlsym", platform):
|
||||
errors.append("dlsym result is cast to a callable function pointer")
|
||||
if "fprintf" in platform or "%p" in combined:
|
||||
errors.append("platform shim must not log addresses or loader details")
|
||||
|
||||
cmake = (root / "CMakeLists.txt").read_text(encoding="utf-8")
|
||||
for term in ("prospero-deploy", "PS5_HOST", "PS5_PORT", "add_custom_target(deploy",
|
||||
"add_custom_target(upload", "add_custom_target(run"):
|
||||
if term.lower() in cmake.lower():
|
||||
errors.append(f"deployment behavior in build graph: {term}")
|
||||
if 'set(CHIMERA_GFX_PS5_ALLOWED_FIRMWARE "NONE"' not in cmake:
|
||||
errors.append("default firmware gate is not NONE")
|
||||
if 'option(CHIMERA_GFX_BUILD_PHASE1_VIDEOOUT_CLEAR' not in cmake:
|
||||
errors.append("Phase-1 candidate does not have an explicit build gate")
|
||||
if 'option(CHIMERA_GFX_BUILD_PS5_MINIMAL_STARTUP' not in cmake:
|
||||
errors.append("Phase-0.5 minimal startup lacks an explicit build gate")
|
||||
if "no minimal PS5 ELF may be built" not in cmake:
|
||||
errors.append("Phase-0.5 minimal startup is not explicitly blocked")
|
||||
|
||||
phase1 = (root / "samples/phase1_videoout_clear/main.c").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
gate_position = phase1.find("if (!chimera_gfx_firmware_gate_allows(")
|
||||
init_position = phase1.find("SDL_Init(SDL_INIT_VIDEO)")
|
||||
present_position = phase1.find("SDL_UpdateWindowSurface(window)")
|
||||
if min(gate_position, init_position, present_position) < 0:
|
||||
errors.append("Phase-1 source is missing its reviewed gate/init/present sequence")
|
||||
elif not gate_position < init_position < present_position:
|
||||
errors.append("Phase-1 firmware gate must precede SDL init and present")
|
||||
if "SDL2main" in cmake:
|
||||
errors.append("Phase-1 target must not link the pre-gate SDL2main wrapper")
|
||||
if any((root / "samples/clear_screen").rglob("*.c")):
|
||||
errors.append("legacy clear-screen placeholder unexpectedly contains source")
|
||||
|
||||
compatibility = (root / "FIRMWARE_COMPATIBILITY.md").read_text(encoding="utf-8")
|
||||
if "| _none_ |" not in compatibility or "| `9.60` |" not in compatibility:
|
||||
errors.append("firmware compatibility table lacks NONE or exact 9.60 rows")
|
||||
if '"firmware_allowlist": ["9.60"]' not in (
|
||||
root / "manifests/ps5_gnm_symbols.json"
|
||||
).read_text(encoding="utf-8"):
|
||||
errors.append("discovery-only firmware allowlist is not exactly 9.60")
|
||||
|
||||
denylist = json.loads(
|
||||
(root / "manifests/artifact-denylist.json").read_text(encoding="utf-8")
|
||||
)
|
||||
blocked_hash = (
|
||||
"4be1c17b4964f2b68c39b5145bc4af4619c32512d60269ecf5c39728b390fa63"
|
||||
)
|
||||
entries = denylist.get("entries", [])
|
||||
if len(entries) != 1 or entries[0].get("sha256") != blocked_hash:
|
||||
errors.append("permanent firmware-9.60 artifact denial is absent")
|
||||
elif (
|
||||
entries[0].get("permanent") is not True
|
||||
or entries[0].get("execution_eligible") is not False
|
||||
):
|
||||
errors.append("firmware-9.60 artifact denial is not permanent and ineligible")
|
||||
|
||||
artifact_manifest = json.loads(
|
||||
(
|
||||
root
|
||||
/ "manifests/artifacts/chimera-gfx-capability-probe-0.1.0-fw-9.60.json"
|
||||
).read_text(encoding="utf-8")
|
||||
)
|
||||
if artifact_manifest["execution"].get("execution_eligible") is not False:
|
||||
errors.append("firmware-9.60 artifact manifest is not execution-ineligible")
|
||||
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"safety audit failed: {error}")
|
||||
return 1
|
||||
print(
|
||||
f"Phase-0/disabled-Phase-1 safety audit passed across "
|
||||
f"{len(all_sources)} C source files"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,225 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,573 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Generate the exact Phase-0.6 PS5 loader/runtime audit without execution."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ELFLDR_COMMIT = "699e8bcff03e91e8d6ca6eba281af25c5a58d8c2"
|
||||
ELFLDR_SHA256 = "092d16ee0ede0c494947efd38d1a17bbd7cc4b022d3858ea898833c188c703e8"
|
||||
ELFLDR_SIZE = 397000
|
||||
PLDMGR_COMMIT = "cfbc70f30f419b09bf2b52283f7409e2d3117ee1"
|
||||
PLDMGR_SHA256 = "518740adbacccb9094fadb07dd424c53ee290f38306449ccc9d6957fdf813c0b"
|
||||
SDK_COMMIT = "d2e2e585740362976a39fdd5ccf390f199a7bc37"
|
||||
|
||||
|
||||
def digest_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def digest_file(path: Path) -> str:
|
||||
return digest_bytes(path.read_bytes())
|
||||
|
||||
|
||||
def run(command: list[str]) -> str:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
return result.stdout.replace("\r\n", "\n")
|
||||
|
||||
|
||||
def git_identity(path: Path, expected: str) -> dict[str, Any]:
|
||||
git = ["git", "-c", "core.autocrlf=true", "-C", str(path)]
|
||||
commit = run([*git, "rev-parse", "HEAD"]).strip()
|
||||
if commit != expected:
|
||||
raise ValueError(f"{path}: expected commit {expected}, got {commit}")
|
||||
status = run([*git, "status", "--porcelain"])
|
||||
if status:
|
||||
raise ValueError(f"{path}: source checkout is dirty")
|
||||
return {"commit": commit, "dirty": False}
|
||||
|
||||
|
||||
def source_record(root: Path, path: Path) -> dict[str, Any]:
|
||||
data = path.read_bytes().replace(b"\r\n", b"\n")
|
||||
return {
|
||||
"normalization": "lf",
|
||||
"path": path.relative_to(root).as_posix(),
|
||||
"sha256": digest_bytes(data),
|
||||
"size": len(data),
|
||||
}
|
||||
|
||||
|
||||
def require_tokens(path: Path, tokens: list[str]) -> None:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
missing = [token for token in tokens if token not in text]
|
||||
if missing:
|
||||
raise ValueError(f"{path}: required evidence missing: {missing}")
|
||||
|
||||
|
||||
def parse_binary(readelf_output: str, disassembly: str) -> dict[str, Any]:
|
||||
entry_match = re.search(r"Entry point address:\s+(0x[0-9a-f]+)", readelf_output)
|
||||
relocation_match = re.search(
|
||||
r"Relocation section '\.rela\.dyn'.*contains (\d+) entries",
|
||||
readelf_output,
|
||||
)
|
||||
relative_match = re.search(r"\(RELACOUNT\)\s+(\d+)", readelf_output)
|
||||
if entry_match is None or relocation_match is None or relative_match is None:
|
||||
raise ValueError("readelf output lacks required header/relocation evidence")
|
||||
|
||||
needed = sorted(re.findall(r"\(NEEDED\).*\[([^\]]+)\]", readelf_output))
|
||||
dynsym_match = re.search(
|
||||
r"Symbol table '\.dynsym'.*?\n(?P<body>.*?)(?:\nSymbol table|\Z)",
|
||||
readelf_output,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
if dynsym_match is None:
|
||||
raise ValueError("readelf output lacks .dynsym")
|
||||
undefined: list[str] = []
|
||||
for line in dynsym_match.group("body").splitlines():
|
||||
match = re.search(r"\bUND\s+(\S+)\s*$", line)
|
||||
if match and match.group(1):
|
||||
undefined.append(match.group(1))
|
||||
|
||||
array_sizes: dict[str, int] = {}
|
||||
for name in ("PREINIT_ARRAY", "INIT_ARRAY", "FINI_ARRAY"):
|
||||
match = re.search(rf"\({name}SZ\)\s+(\d+)", readelf_output)
|
||||
if match is None:
|
||||
raise ValueError(f"readelf output lacks {name}SZ")
|
||||
array_sizes[name.lower()] = int(match.group(1))
|
||||
|
||||
load_segments = []
|
||||
for line in readelf_output.splitlines():
|
||||
match = re.match(
|
||||
r"\s*LOAD\s+\S+\s+\S+\s+\S+\s+(\S+)\s+(\S+)\s+([RWE ]+)\s+\S+",
|
||||
line,
|
||||
)
|
||||
if match:
|
||||
load_segments.append(
|
||||
{
|
||||
"file_size": int(match.group(1), 16),
|
||||
"memory_size": int(match.group(2), 16),
|
||||
"permissions": match.group(3).replace(" ", ""),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"disassembly_sha256": digest_bytes(disassembly.encode("utf-8")),
|
||||
"dt_needed": needed,
|
||||
"entry_point": entry_match.group(1),
|
||||
"init_fini_array_sizes": array_sizes,
|
||||
"load_segments": load_segments,
|
||||
"readelf_report_sha256": digest_bytes(readelf_output.encode("utf-8")),
|
||||
"relocations": {
|
||||
"relative": int(relative_match.group(1)),
|
||||
"total": int(relocation_match.group(1)),
|
||||
},
|
||||
"tls_present": bool(
|
||||
re.search(r"^\s*TLS\s", readelf_output, flags=re.MULTILINE)
|
||||
or re.search(r"\.(?:tdata|tbss)\b", readelf_output)
|
||||
),
|
||||
"undefined_dynamic_symbols": sorted(undefined),
|
||||
}
|
||||
|
||||
|
||||
def effect(
|
||||
effect_id: str,
|
||||
classification: str,
|
||||
scope: str,
|
||||
evidence: list[str],
|
||||
blocker: bool,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"blocker": blocker,
|
||||
"classification": classification,
|
||||
"evidence": evidence,
|
||||
"id": effect_id,
|
||||
"scope": scope,
|
||||
}
|
||||
|
||||
|
||||
def build_audit(args: argparse.Namespace) -> dict[str, Any]:
|
||||
root = args.root.resolve()
|
||||
loader_source = args.loader_source.resolve()
|
||||
manager_source = args.payload_manager_source.resolve()
|
||||
sdk_source = args.sdk_source.resolve()
|
||||
asset = args.loader_asset.resolve()
|
||||
|
||||
loader_git = git_identity(loader_source, ELFLDR_COMMIT)
|
||||
manager_git = git_identity(manager_source, PLDMGR_COMMIT)
|
||||
sdk_git = git_identity(sdk_source, SDK_COMMIT)
|
||||
if asset.stat().st_size != ELFLDR_SIZE or digest_file(asset) != ELFLDR_SHA256:
|
||||
raise ValueError("elfldr release asset identity mismatch")
|
||||
|
||||
loader_files = [
|
||||
loader_source / name
|
||||
for name in ("main.c", "elfldr.c", "elfldr.h", "pt.c", "pt.h", "socksrv.c", "Makefile")
|
||||
]
|
||||
manager_files = [
|
||||
manager_source / name
|
||||
for name in (
|
||||
"src/ps5_launcher.c",
|
||||
"src/http_server.c",
|
||||
"src/autoload.c",
|
||||
"src/main.c",
|
||||
"src/payload_mgr.c",
|
||||
"include/pldmgr.h",
|
||||
"Makefile",
|
||||
)
|
||||
]
|
||||
sdk_files = [
|
||||
sdk_source / name
|
||||
for name in (
|
||||
"crt/crt.c",
|
||||
"crt/patch.c",
|
||||
"crt/kernel.c",
|
||||
"include/ps5/payload.h",
|
||||
"crt/Makefile",
|
||||
)
|
||||
]
|
||||
|
||||
require_tokens(
|
||||
loader_source / "elfldr.c",
|
||||
[
|
||||
"rfork_thread(RFPROC | RFCFDG | RFMEM",
|
||||
"execve(SceSpZeroConf, argv, 0)",
|
||||
"pt_setlong(pid, r.r_rsp-8, r.r_rip)",
|
||||
"r.r_rip = entry",
|
||||
"r.r_rdi = args",
|
||||
"kernel_overlap_sockets",
|
||||
"kernel_set_ucred_uid(pid, 0)",
|
||||
"pt_detach(pid, 0)",
|
||||
],
|
||||
)
|
||||
require_tokens(
|
||||
loader_source / "pt.c",
|
||||
[
|
||||
"kernel_set_ucred_authid(mypid, 0x4800000000010003l)",
|
||||
"kernel_set_ucred_authid(mypid, authid)",
|
||||
"while(jmp_reg.r_rsp <= bak_reg.r_rsp)",
|
||||
],
|
||||
)
|
||||
require_tokens(
|
||||
loader_source / "main.c",
|
||||
["kernel_set_qaflags(qa_flags)", "elfldr_raise_privileges(mypid)"],
|
||||
)
|
||||
require_tokens(
|
||||
loader_source / "socksrv.c",
|
||||
["signal(SIGCHLD, SIG_IGN)", "while(1)", "serve_elfldr(port)"],
|
||||
)
|
||||
require_tokens(
|
||||
manager_source / "src/ps5_launcher.c",
|
||||
['server_addr.sin_addr.s_addr = inet_addr("127.0.0.1")', "send(sock"],
|
||||
)
|
||||
require_tokens(
|
||||
manager_source / "src/http_server.c",
|
||||
[
|
||||
"ps5_launch_elf(final_path)",
|
||||
"fopen(path, \"wb\")",
|
||||
"payload_mgr_import_to_storage",
|
||||
],
|
||||
)
|
||||
require_tokens(
|
||||
sdk_source / "crt/crt.c",
|
||||
["__patch_init()", "payload_terminate(void)", "_start(payload_args_t *args)"],
|
||||
)
|
||||
|
||||
readelf_output = run(
|
||||
[
|
||||
str(args.readelf),
|
||||
"-h",
|
||||
"-l",
|
||||
"-S",
|
||||
"-d",
|
||||
"-r",
|
||||
"-s",
|
||||
"-W",
|
||||
str(asset),
|
||||
]
|
||||
)
|
||||
disassembly = run(
|
||||
[str(args.objdump), "-d", "--no-show-raw-insn", str(asset)]
|
||||
)
|
||||
binary = parse_binary(readelf_output, disassembly)
|
||||
|
||||
effects = [
|
||||
effect(
|
||||
"elfldr_first_stage_qaflags_enable",
|
||||
"EXPECTED_VOLATILE_RUNTIME_EFFECT",
|
||||
"existing_loader_bootstrap",
|
||||
["elfldr/main.c:48-59"],
|
||||
False,
|
||||
),
|
||||
effect(
|
||||
"elfldr_first_stage_privilege_restore",
|
||||
"UNBOUNDED_OR_UNKNOWN",
|
||||
"existing_exploit_host_process",
|
||||
[
|
||||
"elfldr/main.c:61-105 restores jail/root/caps/authid",
|
||||
"UID is changed by elfldr_raise_privileges but is not backed up or restored",
|
||||
],
|
||||
True,
|
||||
),
|
||||
effect(
|
||||
"payload_process_creation",
|
||||
"PAYLOAD_PROCESS_LOCAL",
|
||||
"new_SceSpZeroConf_child",
|
||||
["elfldr/elfldr.c:570-710"],
|
||||
False,
|
||||
),
|
||||
effect(
|
||||
"ptrace_authid_restore_success_path",
|
||||
"RESTORED_BY_LOADER",
|
||||
"elfldr_service_process",
|
||||
["elfldr/pt.c:35-54"],
|
||||
False,
|
||||
),
|
||||
effect(
|
||||
"ptrace_authid_restore_failure_path",
|
||||
"UNBOUNDED_OR_UNKNOWN",
|
||||
"elfldr_service_process",
|
||||
[
|
||||
"elfldr/pt.c:50-51 returns after failed restoration",
|
||||
"no second restoration or process shutdown is present",
|
||||
],
|
||||
True,
|
||||
),
|
||||
effect(
|
||||
"ptrace_single_step_completion",
|
||||
"UNBOUNDED_OR_UNKNOWN",
|
||||
"loader_control_path",
|
||||
["elfldr/pt.c:238-246", "elfldr/pt.c:291-299"],
|
||||
True,
|
||||
),
|
||||
effect(
|
||||
"breakpoint_byte",
|
||||
"RESTORED_BY_LOADER",
|
||||
"payload_child",
|
||||
["elfldr/elfldr.c:675-700"],
|
||||
False,
|
||||
),
|
||||
effect(
|
||||
"breakpoint_page_permissions",
|
||||
"PAYLOAD_PROCESS_LOCAL",
|
||||
"payload_child",
|
||||
[
|
||||
"elfldr/elfldr.c:669 changes page to RWX",
|
||||
"no source edge restores the original protection",
|
||||
],
|
||||
False,
|
||||
),
|
||||
effect(
|
||||
"payload_credentials",
|
||||
"UNBOUNDED_OR_UNKNOWN",
|
||||
"payload_child",
|
||||
[
|
||||
"elfldr/elfldr.c:447-513 restores jail/root/caps/authid",
|
||||
"UID is set to zero and is not restored",
|
||||
],
|
||||
True,
|
||||
),
|
||||
effect(
|
||||
"payload_mapping_args_sockets_pipes",
|
||||
"UNBOUNDED_OR_UNKNOWN",
|
||||
"payload_child",
|
||||
[
|
||||
"elfldr/elfldr.c:143-343",
|
||||
"successful detach has no explicit unmap/close cleanup",
|
||||
"cleanup depends on an unproven child termination path",
|
||||
],
|
||||
True,
|
||||
),
|
||||
effect(
|
||||
"sdk_patch_init",
|
||||
"PAYLOAD_PROCESS_LOCAL",
|
||||
"payload_child",
|
||||
["sdk/crt/crt.c", "sdk/crt/patch.c"],
|
||||
False,
|
||||
),
|
||||
effect(
|
||||
"sdk_termination_branch",
|
||||
"UNBOUNDED_OR_UNKNOWN",
|
||||
"payload_child",
|
||||
[
|
||||
"sdk/crt/crt.c payload_terminate may return, call exit, or trap",
|
||||
"the exact branch for the injected SceSpZeroConf child is not proven",
|
||||
],
|
||||
True,
|
||||
),
|
||||
effect(
|
||||
"payload_runtime_limit",
|
||||
"UNBOUNDED_OR_UNKNOWN",
|
||||
"detached_payload_child",
|
||||
[
|
||||
"elfldr/elfldr.c:703-710 detaches and returns the PID",
|
||||
"no 2000 ms watchdog, wait, kill, or retry budget is present",
|
||||
],
|
||||
True,
|
||||
),
|
||||
effect(
|
||||
"child_reaping",
|
||||
"UNBOUNDED_OR_UNKNOWN",
|
||||
"elfldr_service_process",
|
||||
[
|
||||
"elfldr/socksrv.c:400 ignores SIGCHLD",
|
||||
"post-detach exit/resource cleanup semantics are not documented",
|
||||
],
|
||||
True,
|
||||
),
|
||||
effect(
|
||||
"payload_manager_launch_hash_binding",
|
||||
"UNBOUNDED_OR_UNKNOWN",
|
||||
"payload_manager_to_elfldr",
|
||||
[
|
||||
"pldmgr/http_server.c resolves a path then calls ps5_launch_elf",
|
||||
"pldmgr/ps5_launcher.c streams bytes without calculating or checking SHA-256",
|
||||
],
|
||||
True,
|
||||
),
|
||||
effect(
|
||||
"payload_manager_upload",
|
||||
"PERSISTENT_WRITE",
|
||||
"payload_manager_storage",
|
||||
[
|
||||
"pldmgr/http_server.c writes /data/pldmgr/payloads/<name>.tmp",
|
||||
"the upload is committed into payload storage",
|
||||
],
|
||||
True,
|
||||
),
|
||||
]
|
||||
|
||||
source_files = {
|
||||
"elfldr": [source_record(root, path) for path in loader_files],
|
||||
"payload_manager": [source_record(root, path) for path in manager_files],
|
||||
"sdk": [source_record(root, path) for path in sdk_files],
|
||||
}
|
||||
hard_blockers = [
|
||||
item["id"]
|
||||
for item in effects
|
||||
if item["classification"] in ("PERSISTENT_WRITE", "UNBOUNDED_OR_UNKNOWN")
|
||||
and item["blocker"]
|
||||
]
|
||||
hard_blockers.extend(
|
||||
[
|
||||
"exact_exploit_and_autoloader_identity_unproven",
|
||||
"firmware_9_60_not_independently_device_attested",
|
||||
"return_continuation_after_payload_start_unproven",
|
||||
]
|
||||
)
|
||||
|
||||
return {
|
||||
"artifact": {
|
||||
"built": False,
|
||||
"execution_eligible": False,
|
||||
"filename": None,
|
||||
"sha256": None,
|
||||
"size": None,
|
||||
},
|
||||
"binary_evidence": binary,
|
||||
"callgraph": {
|
||||
"entry": "Payload Manager /loadpayload:<path>",
|
||||
"edges": [
|
||||
["Payload Manager /loadpayload:<path>", "ps5_launch_elf"],
|
||||
["ps5_launch_elf", "connect 127.0.0.1:9021"],
|
||||
["ps5_launch_elf", "send ELF bytes"],
|
||||
["serve_elfldr", "elfldr_spawn"],
|
||||
["elfldr_spawn", "rfork_thread"],
|
||||
["rfork_thread child", "elfldr_rfork_entry"],
|
||||
["elfldr_rfork_entry", "ptrace PT_TRACE_ME"],
|
||||
["elfldr_rfork_entry", "execve SceSpZeroConf"],
|
||||
["elfldr_spawn parent", "pt_syscall 599"],
|
||||
["elfldr_spawn parent", "install then restore INT3 byte"],
|
||||
["elfldr_spawn parent", "elfldr_exec"],
|
||||
["elfldr_exec", "elfldr_raise_privileges"],
|
||||
["elfldr_exec", "elfldr_prepare_exec"],
|
||||
["elfldr_prepare_exec", "elfldr_load"],
|
||||
["elfldr_prepare_exec", "elfldr_payload_args"],
|
||||
["elfldr_prepare_exec", "push observed RIP at RSP-8"],
|
||||
["elfldr_prepare_exec", "set RIP=payload entry"],
|
||||
["elfldr_prepare_exec", "set RDI=payload_args"],
|
||||
["elfldr_exec", "restore subset of credentials"],
|
||||
["elfldr_exec", "ptrace PT_DETACH"],
|
||||
["payload _start", "SDK __patch_init"],
|
||||
["payload _start", "payload main"],
|
||||
["payload _start", "payload_terminate"],
|
||||
["payload_terminate", "return or exit or trap"],
|
||||
],
|
||||
"extraction": "reviewed source edges with required-token assertions",
|
||||
},
|
||||
"decision": "BLOCKED_VERSION_OR_UNBOUNDED_EFFECT",
|
||||
"effects": effects,
|
||||
"firmware": {
|
||||
"device_attested": False,
|
||||
"exact": "9.60",
|
||||
"evidence": "user_provided_only",
|
||||
},
|
||||
"hard_blockers": sorted(hard_blockers),
|
||||
"identity": {
|
||||
"elfldr": {
|
||||
**loader_git,
|
||||
"installed_asset_hash_match": True,
|
||||
"observed_inventory_path": (
|
||||
"/data/pldmgr/payloads/elfldr/elfldr_v0.23.elf"
|
||||
),
|
||||
"observed_inventory_sha256": ELFLDR_SHA256,
|
||||
"observed_inventory_version": "v0.23",
|
||||
"release": "v0.23",
|
||||
"release_asset_sha256": ELFLDR_SHA256,
|
||||
"release_asset_size": ELFLDR_SIZE,
|
||||
"repository": "https://github.com/ps5-payload-dev/elfldr.git",
|
||||
},
|
||||
"exact_exploit_autoloader": {
|
||||
"identified": False,
|
||||
"local_candidate": {
|
||||
"filename": "Y2JB-Autoloader-403-1240.zip",
|
||||
"sha256": (
|
||||
"805e3f87f0c371223619ffc7d3a7b3c0d41a1fae8a8b1171d9e2f162659e8291"
|
||||
),
|
||||
"size": 504159435,
|
||||
"status": "local_backup_candidate_not_installed_identity_proof",
|
||||
},
|
||||
"status": "UNPROVEN",
|
||||
},
|
||||
"payload_manager": {
|
||||
**manager_git,
|
||||
"installed_asset_hash_match": True,
|
||||
"observed_inventory_path": (
|
||||
"/data/pldmgr/payloads/pldmgr/pldmgr_v0.3.1.elf"
|
||||
),
|
||||
"observed_inventory_sha256": PLDMGR_SHA256,
|
||||
"observed_inventory_version": "v0.3.1",
|
||||
"observed_version_endpoint": "0.3.1",
|
||||
"release": "v0.3.1",
|
||||
"release_asset_sha256": PLDMGR_SHA256,
|
||||
"repository": "https://github.com/itsPLK/ps5-payload-manager.git",
|
||||
},
|
||||
"sdk": {
|
||||
**sdk_git,
|
||||
"release": "v0.41",
|
||||
"repository": "https://github.com/ps5-payload-dev/sdk.git",
|
||||
},
|
||||
},
|
||||
"no_console_actions": {
|
||||
"elf_executed": False,
|
||||
"elf_transferred": False,
|
||||
"gnm": False,
|
||||
"raw_port_9021_contacted": False,
|
||||
"rendering": False,
|
||||
"videoout": False,
|
||||
},
|
||||
"observation_scope": {
|
||||
"date": "2026-07-17",
|
||||
"payload_manager_routes": [
|
||||
"/autoload_status",
|
||||
"/get_config",
|
||||
"/list_payloads",
|
||||
"/log",
|
||||
"/sources_list",
|
||||
"/version",
|
||||
],
|
||||
"payload_manager_routes_read_only": True,
|
||||
"strict_status_port_744_result": "ECONNREFUSED",
|
||||
},
|
||||
"phase": "0.6",
|
||||
"schema_version": 1,
|
||||
"source_evidence": source_files,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
parser.add_argument("--loader-source", type=Path, required=True)
|
||||
parser.add_argument("--loader-asset", type=Path, required=True)
|
||||
parser.add_argument("--payload-manager-source", type=Path, required=True)
|
||||
parser.add_argument("--sdk-source", type=Path, required=True)
|
||||
parser.add_argument("--readelf", type=Path, required=True)
|
||||
parser.add_argument("--objdump", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
document = build_audit(args)
|
||||
output = args.output.resolve()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(
|
||||
json.dumps(document, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
print(
|
||||
"Phase-0.6 loader audit: BLOCKED_VERSION_OR_UNBOUNDED_EFFECT; "
|
||||
f"{len(document['hard_blockers'])} hard blockers"
|
||||
)
|
||||
return 0
|
||||
except (OSError, subprocess.CalledProcessError, ValueError) as error:
|
||||
print(f"Phase-0.6 loader audit failed: {error}")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Audit the pinned SDK startup/loader chain without executing a PS5 ELF."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
EXPECTED_SDK_COMMIT = "d2e2e585740362976a39fdd5ccf390f199a7bc37"
|
||||
|
||||
|
||||
def require_in_order(text: str, snippets: tuple[str, ...], source: Path) -> None:
|
||||
position = -1
|
||||
for snippet in snippets:
|
||||
position = text.find(snippet, position + 1)
|
||||
if position < 0:
|
||||
raise ValueError(f"{source}: missing or reordered evidence: {snippet}")
|
||||
|
||||
|
||||
def read(source: Path, relative: str) -> str:
|
||||
path = source / relative
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def git_head(source: Path) -> str:
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(source), "rev-parse", "HEAD"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--sdk-source", type=Path, required=True)
|
||||
parser.add_argument("--project-root", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
sdk = args.sdk_source.resolve(strict=True)
|
||||
project = args.project_root.resolve(strict=True)
|
||||
if git_head(sdk) != EXPECTED_SDK_COMMIT:
|
||||
raise ValueError("SDK source checkout differs from the pinned v0.41 commit")
|
||||
|
||||
crt = read(sdk, "crt/crt.c")
|
||||
patch = read(sdk, "crt/patch.c")
|
||||
rtld = read(sdk, "crt/rtld.c")
|
||||
dlfcn = read(sdk, "crt/rtld_dlfcn.c")
|
||||
sprx = read(sdk, "crt/rtld_sprx.c")
|
||||
kernel = read(sdk, "crt/kernel.c")
|
||||
probe_main = read(project, "samples/capability_probe/main.c")
|
||||
probe_platform = read(project, "src/backends/ps5/probe_platform.c")
|
||||
|
||||
require_in_order(
|
||||
crt,
|
||||
(
|
||||
"__crt_syscall_init(args)",
|
||||
"__kernel_init(args)",
|
||||
"__klog_init()",
|
||||
"__patch_init()",
|
||||
"__rtld_init()",
|
||||
"*payload_args->payloadout = main(argc, argv, environ)",
|
||||
),
|
||||
sdk / "crt/crt.c",
|
||||
)
|
||||
require_in_order(
|
||||
patch,
|
||||
(
|
||||
"patch_kernel_ucred()",
|
||||
"patch_syscall_permissions()",
|
||||
),
|
||||
sdk / "crt/patch.c",
|
||||
)
|
||||
for snippet in (
|
||||
"kernel_set_ucred_caps(pid, caps)",
|
||||
"kernel_set_ucred_attrs(pid, attrs)",
|
||||
"kernel_copyin(&uaddr, kaddr + 0xf0, sizeof(uaddr))",
|
||||
"kernel_copyin(&uaddr, kaddr + 0xf8, sizeof(uaddr))",
|
||||
):
|
||||
if snippet not in patch:
|
||||
raise ValueError(f"crt/patch.c: missing expected write: {snippet}")
|
||||
require_in_order(
|
||||
rtld,
|
||||
("__rtld_sprx_init()", "__rtld_dlfcn_init()"),
|
||||
sdk / "crt/rtld.c",
|
||||
)
|
||||
for snippet in (
|
||||
"sceKernelLoadStartModule(\"/system/common/lib/libSceSysmodule.sprx\"",
|
||||
"sceKernelLoadStartModule(path, 0, 0, 0, 0, 0)",
|
||||
"sceKernelStopUnloadModule(lib->handle, 0, 0, 0, 0, 0)",
|
||||
):
|
||||
if snippet not in sprx:
|
||||
raise ValueError(f"crt/rtld_sprx.c: missing loader evidence: {snippet}")
|
||||
require_in_order(
|
||||
dlfcn,
|
||||
("__rtld_lib_open(lib)", "__rtld_lib_init(lib, getargc(), getargv(), environ)"),
|
||||
sdk / "crt/rtld_dlfcn.c",
|
||||
)
|
||||
require_in_order(
|
||||
dlfcn,
|
||||
("__rtld_lib_fini(lib)", "__rtld_lib_close(lib)", "__rtld_lib_destroy(lib)"),
|
||||
sdk / "crt/rtld_dlfcn.c",
|
||||
)
|
||||
for snippet in ("int\nkernel_copyin", "int\nkernel_copyout"):
|
||||
if snippet not in kernel:
|
||||
raise ValueError(f"crt/kernel.c: missing kernel I/O primitive: {snippet}")
|
||||
require_in_order(
|
||||
probe_main,
|
||||
(
|
||||
"chimera_gfx_firmware_gate_allows",
|
||||
"chimera_gfx_ps5_make_loader_ops",
|
||||
"chimera_gfx_ps5_probe_symbols",
|
||||
),
|
||||
project / "samples/capability_probe/main.c",
|
||||
)
|
||||
for snippet in (
|
||||
"dlopen(module_name, RTLD_LAZY | RTLD_LOCAL)",
|
||||
"address = dlsym(loader->module, symbol_name)",
|
||||
"address = NULL;",
|
||||
"dlclose(loader->module)",
|
||||
):
|
||||
if snippet not in probe_platform:
|
||||
raise ValueError(f"probe_platform.c: missing loader boundary: {snippet}")
|
||||
|
||||
document = {
|
||||
"schema_version": 1,
|
||||
"sdk": {
|
||||
"release": "v0.41",
|
||||
"commit": EXPECTED_SDK_COMMIT,
|
||||
},
|
||||
"execution_eligible_under_project_policy": False,
|
||||
"blocking_side_effects_before_main": [
|
||||
"__patch_init calls patch_kernel_ucred",
|
||||
"patch_kernel_ucred writes process capability and attribute fields",
|
||||
"__patch_init calls patch_syscall_permissions",
|
||||
"patch_syscall_permissions writes the process syscall-address bounds",
|
||||
],
|
||||
"startup_calls_before_main": [
|
||||
"__crt_syscall_init",
|
||||
"__kernel_init",
|
||||
"__klog_init",
|
||||
"__patch_init",
|
||||
"__rtld_init",
|
||||
"__rtld_sprx_init",
|
||||
"__rtld_dlfcn_init",
|
||||
"payload constructors",
|
||||
],
|
||||
"project_requested_calls_after_firmware_gate": [
|
||||
"fprintf",
|
||||
"chimera_gfx_ps5_make_loader_ops",
|
||||
"dlopen",
|
||||
"dlerror",
|
||||
"dlsym (21 bounded lookups; returned addresses discarded)",
|
||||
"snprintf",
|
||||
"dlclose",
|
||||
],
|
||||
"loader_side_effects": [
|
||||
"SDK rtld startup may load and start libSceSysmodule.sprx",
|
||||
"probe dlopen may load and start libSceGnmDriver.sprx",
|
||||
"module open allocates user memory and copies symbol/string tables",
|
||||
],
|
||||
"cleanup_side_effects": [
|
||||
"dlclose invokes rtld fini, close, and destroy paths",
|
||||
"a newly loaded SPRX is stopped/unloaded with sceKernelStopUnloadModule",
|
||||
"SDK sprx_init and sprx_fini are empty at the pinned commit",
|
||||
"cleanup is not guaranteed after a hang, crash, or loader failure",
|
||||
],
|
||||
"conclusion": (
|
||||
"Project code requests no GNM call, rendering, or GPU mutation, but the "
|
||||
"linked SDK payload CRT performs prohibited kernel writes before main. "
|
||||
"The resulting ELF must not be transferred or executed."
|
||||
),
|
||||
}
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(
|
||||
json.dumps(document, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||
)
|
||||
print("SDK runtime audit completed: execution blocked by pre-main kernel writes")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (OSError, subprocess.CalledProcessError, ValueError) as error:
|
||||
print(f"SDK runtime audit failed: {error}")
|
||||
raise SystemExit(1) from error
|
||||
@@ -0,0 +1,612 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Audit SDK v0.41 startup and decide whether a minimal ELF may be built."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
from collections import deque
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
EXPECTED_SDK_COMMIT = "d2e2e585740362976a39fdd5ccf390f199a7bc37"
|
||||
CRT_OBJECTS = {
|
||||
"crt.o": "crt/crt.c",
|
||||
"syscall.o": "crt/syscall.c",
|
||||
"klog.o": "crt/klog.c",
|
||||
"nid.o": "crt/nid.c",
|
||||
"kernel.o": "crt/kernel.c",
|
||||
"rtld.o": "crt/rtld.c",
|
||||
"rtld_so.o": "crt/rtld_so.c",
|
||||
"rtld_sprx.o": "crt/rtld_sprx.c",
|
||||
"rtld_payload.o": "crt/rtld_payload.c",
|
||||
"rtld_dlfcn.o": "crt/rtld_dlfcn.c",
|
||||
"mdbg.o": "crt/mdbg.c",
|
||||
"patch.o": "crt/patch.c",
|
||||
}
|
||||
EMPTY_CRT_ARCHIVES = (
|
||||
"crti.o",
|
||||
"crtn.o",
|
||||
"crtbegin.o",
|
||||
"crtend.o",
|
||||
"crtbeginS.o",
|
||||
"crtendS.o",
|
||||
)
|
||||
DANGEROUS_REACHABLE = {
|
||||
"__patch_init",
|
||||
"kernel_copyin",
|
||||
"kernel_copyout",
|
||||
"kernel_set_ucred_attrs",
|
||||
"kernel_set_ucred_caps",
|
||||
}
|
||||
LINKED_PROHIBITED = DANGEROUS_REACHABLE | {
|
||||
"__dlopen",
|
||||
"__dlsym",
|
||||
"kernel_mprotect",
|
||||
"kernel_overlap_sockets",
|
||||
"kernel_set_vmem_protection",
|
||||
}
|
||||
|
||||
|
||||
def run(command: list[str], allowed: tuple[int, ...] = (0,)) -> str:
|
||||
result = subprocess.run(command, check=False, capture_output=True, text=True)
|
||||
if result.returncode not in allowed:
|
||||
raise ValueError(
|
||||
f"command failed ({result.returncode}): {' '.join(command)}\n"
|
||||
f"{result.stdout}{result.stderr}"
|
||||
)
|
||||
return result.stdout + result.stderr
|
||||
|
||||
|
||||
def sha256_bytes(content: bytes) -> str:
|
||||
return hashlib.sha256(content).hexdigest()
|
||||
|
||||
|
||||
def sha256_file(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 require(text: str, snippets: tuple[str, ...], source: str) -> None:
|
||||
for snippet in snippets:
|
||||
if snippet not in text:
|
||||
raise ValueError(f"{source}: missing pinned evidence: {snippet}")
|
||||
|
||||
|
||||
def normalize_target(symbol: str) -> str:
|
||||
symbol = re.sub(r"[-+]0x[0-9a-f]+$", "", symbol)
|
||||
symbol = re.sub(r"[-+][0-9a-f]+$", "", symbol)
|
||||
return symbol
|
||||
|
||||
|
||||
def parse_symbols(nm_output: str) -> tuple[set[str], set[str]]:
|
||||
functions: set[str] = set()
|
||||
undefined: set[str] = set()
|
||||
for line in nm_output.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
name, kind = parts[0], parts[1]
|
||||
if kind in {"T", "t"}:
|
||||
functions.add(name)
|
||||
elif kind.lower() == "u" or kind == "w":
|
||||
undefined.add(name)
|
||||
return functions, undefined
|
||||
|
||||
|
||||
def parse_callgraph(disassembly: str) -> list[dict[str, str]]:
|
||||
label = re.compile(r"^[0-9a-f]+ <([^>]+)>:$")
|
||||
instruction = re.compile(
|
||||
r"^\s*([0-9a-f]+):\s+([a-z][a-z0-9.]*)\s*(.*?)\s*$"
|
||||
)
|
||||
relocation = re.compile(r"^\s*[0-9a-f]+:\s+R_X86_64_\S+\s+(\S+)")
|
||||
current: str | None = None
|
||||
pending: dict[str, str] | None = None
|
||||
edges: list[dict[str, str]] = []
|
||||
|
||||
def flush() -> None:
|
||||
nonlocal pending
|
||||
if pending is None:
|
||||
return
|
||||
operand = pending.pop("operand")
|
||||
direct = re.search(r"<([^>]+)>", operand)
|
||||
if "target" not in pending:
|
||||
if direct is not None and "+0x" not in direct.group(1):
|
||||
pending["target"] = direct.group(1)
|
||||
else:
|
||||
pending["target"] = f"INDIRECT:{operand}"
|
||||
edges.append(pending)
|
||||
pending = None
|
||||
|
||||
for raw_line in disassembly.splitlines():
|
||||
line = raw_line.rstrip()
|
||||
match = label.match(line)
|
||||
if match is not None:
|
||||
flush()
|
||||
current = match.group(1)
|
||||
continue
|
||||
match = relocation.match(line)
|
||||
if match is not None and pending is not None:
|
||||
pending["target"] = normalize_target(match.group(1))
|
||||
flush()
|
||||
continue
|
||||
match = instruction.match(line)
|
||||
if match is None:
|
||||
continue
|
||||
flush()
|
||||
if current is None:
|
||||
continue
|
||||
address, mnemonic, operand = match.groups()
|
||||
if mnemonic in {"call", "callq"}:
|
||||
pending = {
|
||||
"address": address,
|
||||
"caller": current,
|
||||
"kind": "call",
|
||||
"operand": operand,
|
||||
}
|
||||
elif mnemonic in {"jmp", "jmpq"} and (
|
||||
operand.startswith("*") or ("<" in operand and "+0x" not in operand)
|
||||
):
|
||||
pending = {
|
||||
"address": address,
|
||||
"caller": current,
|
||||
"kind": "tail_call",
|
||||
"operand": operand,
|
||||
}
|
||||
flush()
|
||||
return edges
|
||||
|
||||
|
||||
def reachable_callgraph(
|
||||
edges: list[dict[str, str]], functions: set[str]
|
||||
) -> tuple[list[str], list[dict[str, str]]]:
|
||||
by_caller: dict[str, list[dict[str, str]]] = {}
|
||||
for edge in edges:
|
||||
by_caller.setdefault(edge["caller"], []).append(edge)
|
||||
visited: set[str] = set()
|
||||
selected: list[dict[str, str]] = []
|
||||
queue: deque[str] = deque(["_start"])
|
||||
while queue:
|
||||
caller = queue.popleft()
|
||||
if caller in visited:
|
||||
continue
|
||||
visited.add(caller)
|
||||
for edge in by_caller.get(caller, []):
|
||||
selected.append(edge)
|
||||
target = edge["target"]
|
||||
if target in functions and target not in visited:
|
||||
queue.append(target)
|
||||
return sorted(visited), sorted(
|
||||
selected, key=lambda item: (item["caller"], item["address"], item["target"])
|
||||
)
|
||||
|
||||
|
||||
def driver_trace(compiler: Path, source: Path, freestanding: bool) -> str:
|
||||
command = [str(compiler), "-###", "-Qunused-arguments", "-Werror"]
|
||||
if freestanding:
|
||||
command.extend(["-nostartfiles", "-nodefaultlibs"])
|
||||
command.extend([str(source), "-o", str(source.with_suffix(".never.elf"))])
|
||||
trace = run(command)
|
||||
if source.with_suffix(".never.elf").exists():
|
||||
raise ValueError("compiler -### unexpectedly produced an ELF")
|
||||
trace = trace.replace(str(source.parent), "<TEMP>")
|
||||
return re.sub(r"trace-[0-9a-f]+\.o", "trace-<ID>.o", trace)
|
||||
|
||||
|
||||
def git_grep_loader_contract(checkout: Path) -> list[str]:
|
||||
if not (checkout / ".git").exists():
|
||||
return []
|
||||
output = run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(checkout),
|
||||
"grep",
|
||||
"-n",
|
||||
"-e",
|
||||
"payload_args_t",
|
||||
"-e",
|
||||
"payloadout",
|
||||
"--",
|
||||
],
|
||||
allowed=(0, 1),
|
||||
)
|
||||
return sorted(line for line in output.splitlines() if line.strip())
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
parser.add_argument("--sdk-source", type=Path, required=True)
|
||||
parser.add_argument("--sdk-install", type=Path, required=True)
|
||||
parser.add_argument("--objdump", type=Path, required=True)
|
||||
parser.add_argument("--readelf", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
root = args.root.resolve(strict=True)
|
||||
sdk_source = args.sdk_source.resolve(strict=True)
|
||||
sdk_install = args.sdk_install.resolve(strict=True)
|
||||
sdk_head = run(["git", "-C", str(sdk_source), "rev-parse", "HEAD"]).strip()
|
||||
if sdk_head != EXPECTED_SDK_COMMIT:
|
||||
raise ValueError("SDK source does not match the pinned v0.41 commit")
|
||||
|
||||
source_files = set(CRT_OBJECTS.values()) | {
|
||||
"Makefile.inc",
|
||||
"crt/Makefile",
|
||||
"crt/payload.h",
|
||||
"include/ps5/payload.h",
|
||||
"host/bin/prospero-clang",
|
||||
"host/bin/prospero-lld",
|
||||
"host/elf_x86_64.x",
|
||||
"host/Makefile",
|
||||
"host/toolchain/prospero.cmake",
|
||||
"host/toolchain/prospero.mk",
|
||||
"host/toolchain/prospero.sh",
|
||||
}
|
||||
source_files.update(
|
||||
str(path.relative_to(sdk_source)).replace("\\", "/")
|
||||
for path in (sdk_source / "crt").glob("*.h")
|
||||
)
|
||||
source_hashes = {
|
||||
relative: sha256_file(sdk_source / relative)
|
||||
for relative in sorted(source_files)
|
||||
}
|
||||
|
||||
makefile = (sdk_source / "crt/Makefile").read_text(encoding="utf-8")
|
||||
object_text_match = re.search(r"OBJ := (.+?)\n\n", makefile, re.DOTALL)
|
||||
if object_text_match is None:
|
||||
raise ValueError("could not parse CRT object inventory")
|
||||
makefile_objects = object_text_match.group(1).replace("\\\n", " ").split()
|
||||
if makefile_objects != list(CRT_OBJECTS):
|
||||
raise ValueError(f"CRT object inventory changed: {makefile_objects}")
|
||||
require(
|
||||
makefile,
|
||||
(
|
||||
"-ffreestanding -fno-builtin -nostdlib -fPIC",
|
||||
"-target x86_64-sie-ps5",
|
||||
"crt1.o: $(OBJ)",
|
||||
"$(LD) -m elf_x86_64 -r -o $@ $^",
|
||||
),
|
||||
"crt/Makefile",
|
||||
)
|
||||
|
||||
crt = (sdk_source / "crt/crt.c").read_text(encoding="utf-8")
|
||||
patch = (sdk_source / "crt/patch.c").read_text(encoding="utf-8")
|
||||
payload_header = (sdk_source / "include/ps5/payload.h").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
linker_script = (sdk_source / "host/elf_x86_64.x").read_text(encoding="utf-8")
|
||||
compiler_wrapper = (sdk_source / "host/bin/prospero-clang").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
require(
|
||||
crt,
|
||||
(
|
||||
"_start(payload_args_t *args)",
|
||||
"for(unsigned char* bss=__bss_start; bss<__bss_end; bss++)",
|
||||
"__crt_syscall_init(args)",
|
||||
"__kernel_init(args)",
|
||||
"__klog_init()",
|
||||
"__patch_init()",
|
||||
"__rtld_init()",
|
||||
"return payload_terminate()",
|
||||
),
|
||||
"crt/crt.c",
|
||||
)
|
||||
require(
|
||||
patch,
|
||||
(
|
||||
"kernel_set_ucred_caps(pid, caps)",
|
||||
"kernel_set_ucred_attrs(pid, attrs)",
|
||||
"caps[15] |= 0x40",
|
||||
"attrs[3] |= 0x80",
|
||||
"kernel_copyin(&uaddr, kaddr + 0xf0, sizeof(uaddr))",
|
||||
"kernel_copyin(&uaddr, kaddr + 0xf8, sizeof(uaddr))",
|
||||
),
|
||||
"crt/patch.c",
|
||||
)
|
||||
require(
|
||||
payload_header,
|
||||
(
|
||||
"int (*sys_dynlib_dlsym)(int, const char*, void*)",
|
||||
"int* rwpipe",
|
||||
"int* rwpair",
|
||||
"intptr_t kpipe_addr",
|
||||
"intptr_t kdata_base_addr",
|
||||
"int* payloadout",
|
||||
),
|
||||
"include/ps5/payload.h",
|
||||
)
|
||||
require(
|
||||
linker_script,
|
||||
(
|
||||
"OUTPUT_ARCH(i386:x86-64)",
|
||||
"PROVIDE_HIDDEN (__bss_start = .)",
|
||||
"PROVIDE_HIDDEN (__bss_end = .)",
|
||||
".init_array",
|
||||
".fini_array",
|
||||
"PT_DYNAMIC",
|
||||
),
|
||||
"host/elf_x86_64.x",
|
||||
)
|
||||
require(
|
||||
compiler_wrapper,
|
||||
(
|
||||
'LIBS_CRT="${PS5_PAYLOAD_SDK}/target/lib/crt1.o"',
|
||||
'if [[ "$ARG" == "-nostdlibc" ||',
|
||||
'"$ARG" == "-nodefaultlibs"',
|
||||
'if [[ "$ARG" == "-nostartfiles" ||',
|
||||
"-target x86_64-sie-ps5",
|
||||
),
|
||||
"host/bin/prospero-clang",
|
||||
)
|
||||
|
||||
crt1 = sdk_install / "target/lib/crt1.o"
|
||||
nm = sdk_install / "bin/llvm-nm"
|
||||
ar = sdk_install / "bin/llvm-ar"
|
||||
compiler = sdk_install / "bin/prospero-clang"
|
||||
linker = sdk_install / "bin/prospero-lld"
|
||||
for path in (crt1, nm, ar, compiler, linker, args.objdump, args.readelf):
|
||||
path.resolve(strict=True)
|
||||
|
||||
nm_output = run([str(nm), "--format=posix", str(crt1)])
|
||||
functions, undefined = parse_symbols(nm_output)
|
||||
expected_undefined = {
|
||||
"_DYNAMIC",
|
||||
"__bss_end",
|
||||
"__bss_start",
|
||||
"__fini_array_end",
|
||||
"__fini_array_start",
|
||||
"__image_end",
|
||||
"__image_start",
|
||||
"__init_array_end",
|
||||
"__init_array_start",
|
||||
"main",
|
||||
}
|
||||
if undefined != expected_undefined:
|
||||
raise ValueError(f"crt1.o undefined symbol inventory changed: {undefined}")
|
||||
if not DANGEROUS_REACHABLE.issubset(functions):
|
||||
raise ValueError("crt1.o no longer contains the reviewed dangerous functions")
|
||||
if not LINKED_PROHIBITED.issubset(functions):
|
||||
raise ValueError("crt1.o linked prohibited-function inventory changed")
|
||||
|
||||
empty_archives: dict[str, list[str]] = {}
|
||||
for filename in EMPTY_CRT_ARCHIVES:
|
||||
members = run([str(ar), "t", str(sdk_install / "target/lib" / filename)])
|
||||
empty_archives[filename] = members.splitlines()
|
||||
if empty_archives[filename]:
|
||||
raise ValueError(f"{filename} unexpectedly contains members")
|
||||
|
||||
disassembly = run(
|
||||
[str(args.objdump), "-dr", "--no-show-raw-insn", str(crt1)]
|
||||
)
|
||||
all_edges = parse_callgraph(disassembly)
|
||||
reachable_functions, reachable_edges = reachable_callgraph(all_edges, functions)
|
||||
reachable_set = set(reachable_functions)
|
||||
if not DANGEROUS_REACHABLE.issubset(reachable_set):
|
||||
missing = sorted(DANGEROUS_REACHABLE - reachable_set)
|
||||
raise ValueError(f"disassembly call graph lost expected reachable writes: {missing}")
|
||||
|
||||
section_table = run([str(args.readelf), "-SW", str(crt1)])
|
||||
relocation_table = run([str(args.readelf), "-Wr", str(crt1)])
|
||||
elf_header = run([str(args.readelf), "-h", str(crt1)])
|
||||
if ".text._start" not in section_table or ".rela.text._start" not in section_table:
|
||||
raise ValueError("crt1.o lacks the audited _start sections")
|
||||
if "__patch_init" not in relocation_table or "kernel_copyin" not in relocation_table:
|
||||
raise ValueError("crt1.o relocation evidence no longer exposes the write path")
|
||||
tls_sections = sorted(
|
||||
match.group(1)
|
||||
for line in section_table.splitlines()
|
||||
if (match := re.search(r"\]\s+(\.\S+)\s+\S+.*\sT\s", line)) is not None
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
trace_source = Path(directory) / "trace.c"
|
||||
trace_source.write_text("int main(void) { return 0; }\n", encoding="utf-8")
|
||||
default_trace = driver_trace(compiler, trace_source, freestanding=False)
|
||||
free_trace = driver_trace(compiler, trace_source, freestanding=True)
|
||||
for expected in ("crt1.o", '"-lc"', '"-lkernel_web"', '"-lSceLibcInternal"', '"-lSceNet"'):
|
||||
if expected not in default_trace:
|
||||
raise ValueError(f"default driver trace lacks {expected}")
|
||||
for forbidden in ("crt1.o", '"-lc"', '"-lkernel_web"', '"-lSceLibcInternal"', '"-lSceNet"'):
|
||||
if forbidden in free_trace:
|
||||
raise ValueError(f"freestanding driver trace retains {forbidden}")
|
||||
|
||||
upstream = root / "work/upstream"
|
||||
loader_search: dict[str, list[str]] = {}
|
||||
for name in ("SDL", "RetroArch", "pacbrew-repo", "ps5-linux-loader"):
|
||||
loader_search[name] = git_grep_loader_contract(upstream / name)
|
||||
if any(loader_search.values()):
|
||||
raise ValueError("an unreviewed local loader caller appeared in the evidence set")
|
||||
|
||||
document: dict[str, Any] = {
|
||||
"schema_version": 1,
|
||||
"audit_date": "2026-07-17",
|
||||
"decision": "BLOCKED",
|
||||
"scope": "phase-0.5-kernelwrite-free-startup-offline-only",
|
||||
"artifact": {
|
||||
"built": False,
|
||||
"execution_eligible": False,
|
||||
"reason": "safe loader return and cleanup are not proven",
|
||||
"sha256": None,
|
||||
},
|
||||
"sdk": {
|
||||
"release": "v0.41",
|
||||
"commit": EXPECTED_SDK_COMMIT,
|
||||
"target": "x86_64-sie-ps5",
|
||||
"source_hashes": source_hashes,
|
||||
},
|
||||
"tool_versions": {
|
||||
"prospero_clang": run([str(compiler), "--version"]).splitlines()[0],
|
||||
"prospero_lld": run([str(linker), "--version"]).splitlines()[0],
|
||||
"objdump": run([str(args.objdump), "--version"]).splitlines()[0],
|
||||
"readelf": run([str(args.readelf), "--version"]).splitlines()[0],
|
||||
},
|
||||
"startup_linkage": {
|
||||
"default_driver_additions": [
|
||||
"target/lib/crt1.o",
|
||||
"libc",
|
||||
"libkernel_web",
|
||||
"libSceLibcInternal",
|
||||
"libSceNet",
|
||||
],
|
||||
"default_driver_trace_sha256": sha256_bytes(default_trace.encode()),
|
||||
"freestanding_flags": ["-nostartfiles", "-nodefaultlibs"],
|
||||
"freestanding_driver_additions": [],
|
||||
"freestanding_driver_trace_sha256": sha256_bytes(free_trace.encode()),
|
||||
"crt1_sha256": sha256_file(crt1),
|
||||
"installed_linker_script_sha256": sha256_file(
|
||||
sdk_install / "ldscripts/elf_x86_64.x"
|
||||
),
|
||||
"installed_main_script_sha256": sha256_file(
|
||||
sdk_install / "target/lib/main.script"
|
||||
),
|
||||
"installed_prx_script_sha256": sha256_file(
|
||||
sdk_install / "target/lib/prx.script"
|
||||
),
|
||||
"crt1_composition": [
|
||||
{"object": obj, "source": source}
|
||||
for obj, source in CRT_OBJECTS.items()
|
||||
],
|
||||
"empty_startup_archives": empty_archives,
|
||||
"crt1_undefined_symbols": sorted(undefined),
|
||||
},
|
||||
"crt1_static_evidence": {
|
||||
"disassembly_sha256": sha256_bytes(disassembly.encode()),
|
||||
"elf_header_sha256": sha256_bytes(elf_header.encode()),
|
||||
"section_table_sha256": sha256_bytes(section_table.encode()),
|
||||
"relocation_table_sha256": sha256_bytes(relocation_table.encode()),
|
||||
"relocation_count": len(re.findall(r"R_X86_64_", relocation_table)),
|
||||
"tls_sections": tls_sections,
|
||||
"reachable_functions": reachable_functions,
|
||||
"reachable_callgraph": reachable_edges,
|
||||
"reachable_prohibited_functions": sorted(
|
||||
DANGEROUS_REACHABLE & reachable_set
|
||||
),
|
||||
"linked_prohibited_functions": sorted(LINKED_PROHIBITED & functions),
|
||||
"linked_but_not_startup_reachable": sorted(
|
||||
LINKED_PROHIBITED - reachable_set
|
||||
),
|
||||
"source_token_inventory": {
|
||||
"GNM": any(
|
||||
"sceGnm" in (sdk_source / source).read_text(encoding="utf-8")
|
||||
for source in CRT_OBJECTS.values()
|
||||
),
|
||||
"SDL": any(
|
||||
"SDL_" in (sdk_source / source).read_text(encoding="utf-8")
|
||||
for source in CRT_OBJECTS.values()
|
||||
),
|
||||
"VideoOut": any(
|
||||
"sceVideoOut" in (sdk_source / source).read_text(encoding="utf-8")
|
||||
for source in CRT_OBJECTS.values()
|
||||
),
|
||||
"module_loading": True,
|
||||
"network_socket_helper": True,
|
||||
},
|
||||
},
|
||||
"loader_evidence": {
|
||||
"sdk_readme_loader_candidates": [
|
||||
"ps5-payload-dev/elfldr",
|
||||
"cryonumb/elfloader via ps5-jar-loader",
|
||||
"shahrilnet/remote_lua_loader",
|
||||
],
|
||||
"local_non_sdk_contract_hits": loader_search,
|
||||
"exact_loader_used_for_firmware_9_60_identified": False,
|
||||
"caller_source_present": False,
|
||||
"entry_argument_layout": "SAFE_SOURCE_FACT",
|
||||
"callee_first_argument_register_rdi": "SAFE_DISASSEMBLY_FACT",
|
||||
"caller_stack_alignment": "UNPROVEN",
|
||||
"payloadout_lifetime_and_writability": "UNPROVEN",
|
||||
"return_address_and_return_consumption": "UNPROVEN",
|
||||
"post_return_cleanup": "UNPROVEN",
|
||||
"crash_cleanup": "UNPROVEN",
|
||||
"pre_entry_loader_process_changes": "UNPROVEN",
|
||||
},
|
||||
"review": [
|
||||
{
|
||||
"component": "payload_args_t field order and 48-byte x86_64 layout",
|
||||
"status": "SAFE",
|
||||
"basis": "public SDK header plus _start payloadout access at offset 0x28",
|
||||
},
|
||||
{
|
||||
"component": "SDK v0.41 default crt1.o",
|
||||
"status": "UNSAFE",
|
||||
"basis": "source, relocation, and reachable disassembly prove kernel-write paths",
|
||||
},
|
||||
{
|
||||
"component": "-nostartfiles -nodefaultlibs removes SDK CRT/default libs",
|
||||
"status": "SAFE",
|
||||
"basis": "compiler -### trace only; no output ELF was produced",
|
||||
},
|
||||
{
|
||||
"component": "custom BSS zeroing implementation",
|
||||
"status": "SAFE",
|
||||
"basis": "linker symbols and byte-zero loop can be freestanding",
|
||||
},
|
||||
{
|
||||
"component": "loader entry stack and ABI preconditions",
|
||||
"status": "UNPROVEN",
|
||||
"basis": "callee disassembly does not prove caller behavior",
|
||||
},
|
||||
{
|
||||
"component": "return, exit, crash, and cleanup behavior",
|
||||
"status": "UNPROVEN",
|
||||
"basis": "no exact pinned public loader caller source is locally available",
|
||||
},
|
||||
{
|
||||
"component": "loader changes before _start",
|
||||
"status": "UNPROVEN",
|
||||
"basis": "payload args expose pre-existing kernel access but not how it was established",
|
||||
},
|
||||
],
|
||||
"custom_artifact_verification": {
|
||||
"linker_map": "NOT_PERFORMED_BLOCKED_BEFORE_BUILD",
|
||||
"disassembly": "NOT_PERFORMED_BLOCKED_BEFORE_BUILD",
|
||||
"undefined_symbols": "NOT_PERFORMED_BLOCKED_BEFORE_BUILD",
|
||||
"dt_needed": "NOT_PERFORMED_BLOCKED_BEFORE_BUILD",
|
||||
"relocations": "NOT_PERFORMED_BLOCKED_BEFORE_BUILD",
|
||||
"init_fini_arrays": "NOT_PERFORMED_BLOCKED_BEFORE_BUILD",
|
||||
"tls": "NOT_PERFORMED_BLOCKED_BEFORE_BUILD",
|
||||
"double_clean_build": "NOT_PERFORMED_BLOCKED_BEFORE_BUILD",
|
||||
"artifact_sha256": None,
|
||||
},
|
||||
"minimal_missing_evidence": [
|
||||
"the exact loader and pinned source commit used on firmware 9.60",
|
||||
"the caller instruction sequence that establishes RDI, RSP alignment, and return address",
|
||||
"payload_args_t allocation, payloadout lifetime, writability, and ownership",
|
||||
"the caller path after _start returns, including restoration and cleanup",
|
||||
"crash/fault behavior before and during _start",
|
||||
"all process, credential, syscall, module, and memory changes made before entry",
|
||||
],
|
||||
"safe_alternatives": [
|
||||
"continue libchimera-gfx through mock and software backends",
|
||||
"develop a separately scoped Linux-on-PS5 backend as a long-term track",
|
||||
],
|
||||
}
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(
|
||||
json.dumps(document, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||
)
|
||||
print(
|
||||
"Phase-0.5 startup audit: BLOCKED; loader return contract is unproven; "
|
||||
"no PS5 ELF built"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (KeyError, OSError, TypeError, ValueError) as error:
|
||||
print(f"startup feasibility audit failed: {error}")
|
||||
raise SystemExit(1) from error
|
||||
@@ -0,0 +1,52 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Destination = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
if ([string]::IsNullOrWhiteSpace($Destination)) {
|
||||
$Destination = Join-Path $root "work/toolchains/ps5-payload-sdk-v0.41"
|
||||
}
|
||||
|
||||
$cache = Join-Path $root "work/cache"
|
||||
$archive = Join-Path $cache "ps5-payload-sdk-v0.41.zip"
|
||||
$url = "https://github.com/ps5-payload-dev/sdk/releases/download/v0.41/ps5-payload-sdk.zip"
|
||||
$expectedHash = "ebfb0acb5260511951a80e17db41650c62d20a8caf8659a230b928dc85005984"
|
||||
$expectedSize = 8810966
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $cache | Out-Null
|
||||
if (-not (Test-Path -LiteralPath $archive)) {
|
||||
& curl.exe -fL --retry 3 --output $archive $url
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "SDK download failed"
|
||||
}
|
||||
}
|
||||
|
||||
$file = Get-Item -LiteralPath $archive
|
||||
if ($file.Length -ne $expectedSize) {
|
||||
throw "SDK size mismatch: expected $expectedSize, got $($file.Length)"
|
||||
}
|
||||
$actualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $archive).Hash.ToLowerInvariant()
|
||||
if ($actualHash -ne $expectedHash) {
|
||||
throw "SDK SHA-256 mismatch"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Destination)) {
|
||||
$temporary = Join-Path $root "work/toolchains/sdk-extract"
|
||||
$resolvedRoot = [System.IO.Path]::GetFullPath($root).TrimEnd('\') + '\'
|
||||
$resolvedTemporary = [System.IO.Path]::GetFullPath($temporary)
|
||||
if (-not $resolvedTemporary.StartsWith($resolvedRoot,
|
||||
[System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Refusing temporary cleanup outside the workspace"
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path $temporary | Out-Null
|
||||
Expand-Archive -LiteralPath $archive -DestinationPath $temporary -Force
|
||||
$extracted = Join-Path $temporary "ps5-payload-sdk"
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Destination) | Out-Null
|
||||
Move-Item -LiteralPath $extracted -Destination $Destination
|
||||
Remove-Item -LiteralPath $temporary -Recurse -Force
|
||||
}
|
||||
|
||||
Write-Output "Verified PS5 Payload SDK v0.41 at $Destination"
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
set -euo pipefail
|
||||
|
||||
readonly allowed_firmware="9.60"
|
||||
readonly expected_sdk_commit="d2e2e585740362976a39fdd5ccf390f199a7bc37"
|
||||
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly root="$(cd "${script_dir}/.." && pwd)"
|
||||
readonly sdk="${PS5_PAYLOAD_SDK:-${root}/work/toolchains/ps5-payload-sdk-v0.41}"
|
||||
readonly sdk_source="${CHIMERA_GFX_SDK_SOURCE:-${root}/work/upstream/sdk}"
|
||||
readonly build="${root}/build/probe-9.60"
|
||||
readonly outputs="${root}/outputs"
|
||||
readonly artifact_name="chimera-gfx-capability-probe-0.1.0-fw-9.60-offline-audit-only.elf"
|
||||
readonly manifest_name="chimera-gfx-capability-probe-0.1.0-fw-9.60.json"
|
||||
|
||||
if [[ "${1:-}" != "${allowed_firmware}" || $# -ne 1 ]]; then
|
||||
echo "usage: $0 9.60" >&2
|
||||
exit 64
|
||||
fi
|
||||
if [[ ! -f "${sdk}/toolchain/prospero.cmake" ||
|
||||
! -x "${sdk}/bin/prospero-nm" ]]; then
|
||||
echo "missing verified PS5 Payload SDK v0.41 toolchain" >&2
|
||||
exit 2
|
||||
fi
|
||||
if ! command -v llvm-readelf-18 >/dev/null 2>&1; then
|
||||
echo "missing llvm-readelf-18 for the strict dynamic-table audit" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -d "${sdk_source}/.git" ||
|
||||
"$(git -C "${sdk_source}" rev-parse HEAD)" != "${expected_sdk_commit}" ]]; then
|
||||
echo "SDK source checkout is not at the locked v0.41 commit" >&2
|
||||
exit 2
|
||||
fi
|
||||
if ! git -C "${root}" diff --quiet ||
|
||||
! git -C "${root}" diff --cached --quiet; then
|
||||
echo "refusing provenance build from a dirty project tree" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mkdir -p "${build}" "${outputs}"
|
||||
python3 "${root}/tools/audit_ps5_sdk_runtime.py" \
|
||||
--sdk-source "${sdk_source}" \
|
||||
--project-root "${root}" \
|
||||
--output "${build}/sdk-runtime-audit.json"
|
||||
|
||||
export LLVM_CONFIG="${LLVM_CONFIG:-/usr/bin/llvm-config-18}"
|
||||
cmake -S "${root}" -B "${build}" -G Ninja \
|
||||
-DCMAKE_TOOLCHAIN_FILE="${sdk}/toolchain/prospero.cmake" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_TESTING=OFF \
|
||||
-DCHIMERA_GFX_BUILD_PS5_PROBE=ON \
|
||||
-DCHIMERA_GFX_BUILD_PHASE1_VIDEOOUT_CLEAR=OFF \
|
||||
-DCHIMERA_GFX_PS5_ALLOWED_FIRMWARE="${allowed_firmware}"
|
||||
cmake --build "${build}" --clean-first \
|
||||
--target chimera-gfx-capability-probe
|
||||
|
||||
cmake -E copy_if_different \
|
||||
"${build}/chimera-gfx-capability-probe.elf" \
|
||||
"${outputs}/${artifact_name}"
|
||||
cmake -E copy_if_different \
|
||||
"${build}/sdk-runtime-audit.json" \
|
||||
"${outputs}/sdk-runtime-audit-v0.41.json"
|
||||
|
||||
python3 "${root}/tools/audit_ps5_artifacts.py" \
|
||||
--nm "${sdk}/bin/prospero-nm" \
|
||||
--readelf "$(command -v llvm-readelf-18)" \
|
||||
--probe "${outputs}/${artifact_name}" \
|
||||
--symbol-manifest "${root}/manifests/ps5_gnm_symbols.json" \
|
||||
--firmware "${allowed_firmware}"
|
||||
|
||||
readonly source_commit="$(git -C "${root}" rev-parse HEAD)"
|
||||
python3 "${root}/tools/generate_artifact_manifest.py" \
|
||||
--artifact "${outputs}/${artifact_name}" \
|
||||
--output "${outputs}/${manifest_name}" \
|
||||
--id "chimera-gfx-capability-probe-fw-9.60" \
|
||||
--version "0.1.0" \
|
||||
--source-repository \
|
||||
"https://gitea.itworx.tech/Jens/chimera-gfx.git" \
|
||||
--source-commit "${source_commit}" \
|
||||
--target "ps5-x86_64" \
|
||||
--firmware "${allowed_firmware}" \
|
||||
--profile "symbol-discovery-only-offline-audit" \
|
||||
--note "Transfer and execution are not authorized." \
|
||||
--note "Project code calls no resolved GNM symbol and requests no rendering or GPU mutation." \
|
||||
--note "SDK v0.41 CRT performs kernel credential and syscall-permission writes before main; execution is blocked by project policy."
|
||||
python3 "${root}/tools/verify_artifact_manifest.py" \
|
||||
--manifest "${outputs}/${manifest_name}" \
|
||||
--artifact "${outputs}/${artifact_name}"
|
||||
|
||||
echo "Built and audited offline only: ${outputs}/${artifact_name}"
|
||||
echo "Execution eligibility: false; do not transfer or execute this ELF."
|
||||
@@ -0,0 +1,40 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet("Debug", "Release")]
|
||||
[string]$Configuration = "Debug"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$cmake = "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe"
|
||||
$ctest = "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\ctest.exe"
|
||||
if (-not (Test-Path -LiteralPath $cmake)) {
|
||||
$cmakeCommand = Get-Command cmake -ErrorAction Stop
|
||||
$cmake = $cmakeCommand.Source
|
||||
$ctestCommand = Get-Command ctest -ErrorAction Stop
|
||||
$ctest = $ctestCommand.Source
|
||||
}
|
||||
|
||||
$rootBytes = [System.Text.Encoding]::UTF8.GetBytes(
|
||||
[System.IO.Path]::GetFullPath($root).ToLowerInvariant())
|
||||
$sha256 = [System.Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
$rootHashBytes = $sha256.ComputeHash($rootBytes)
|
||||
}
|
||||
finally {
|
||||
$sha256.Dispose()
|
||||
}
|
||||
$rootHash = (
|
||||
[System.BitConverter]::ToString($rootHashBytes) -replace "-", ""
|
||||
).Substring(0, 12).ToLowerInvariant()
|
||||
$buildName = "build/windows-{0}-{1}" -f $Configuration.ToLowerInvariant(), $rootHash
|
||||
$build = Join-Path $root $buildName
|
||||
|
||||
& $cmake -S $root -B $build -G "Visual Studio 17 2022" -A x64 `
|
||||
-DBUILD_TESTING=ON -DCHIMERA_GFX_REGISTER_EXTERNAL_EVIDENCE_VALIDATORS=OFF
|
||||
if ($LASTEXITCODE -ne 0) { throw "Host configure failed" }
|
||||
& $cmake --build $build --config $Configuration
|
||||
if ($LASTEXITCODE -ne 0) { throw "Host build failed" }
|
||||
& $ctest --test-dir $build -C $Configuration --output-on-failure
|
||||
if ($LASTEXITCODE -ne 0) { throw "Host tests failed" }
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
set -euo pipefail
|
||||
|
||||
readonly expected_sdl_commit="0baf4ac49382b537ba449901b5b6d0d189bb1fbb"
|
||||
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly root="$(cd "${script_dir}/.." && pwd)"
|
||||
readonly sdk="${PS5_PAYLOAD_SDK:-${root}/work/toolchains/ps5-payload-sdk-v0.41}"
|
||||
readonly sdl_source="${CHIMERA_GFX_SDL_SOURCE:-${root}/work/upstream/SDL}"
|
||||
readonly sdl_stage="${root}/work/generated/SDL-phase1-video-only"
|
||||
readonly sdl_build="${root}/build/phase1-sdl"
|
||||
readonly project_build="${root}/build/phase1-videoout"
|
||||
readonly sdl_patch="${root}/packaging/patches/sdl2-phase1-video-only.patch"
|
||||
|
||||
if [[ ! -f "${sdk}/toolchain/prospero.cmake" ]]; then
|
||||
echo "missing verified PS5 Payload SDK v0.41" >&2
|
||||
exit 2
|
||||
fi
|
||||
if ! command -v llvm-readelf-18 >/dev/null 2>&1; then
|
||||
echo "missing llvm-readelf-18 for the strict dynamic-table audit" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -d "${sdl_source}/.git" ]]; then
|
||||
echo "missing pinned PS5 SDL source checkout" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ "$(git -C "${sdl_source}" rev-parse HEAD)" != "${expected_sdl_commit}" ]]; then
|
||||
echo "PS5 SDL checkout is not at the locked commit" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -e "${sdl_stage}/.git" ]]; then
|
||||
mkdir -p "$(dirname "${sdl_stage}")"
|
||||
git -C "${sdl_source}" -c core.autocrlf=false -c core.eol=lf \
|
||||
worktree add --detach "${sdl_stage}" "${expected_sdl_commit}"
|
||||
fi
|
||||
if [[ "$(git -C "${sdl_stage}" rev-parse HEAD)" != "${expected_sdl_commit}" ]]; then
|
||||
echo "staged PS5 SDL checkout is not at the locked commit" >&2
|
||||
exit 2
|
||||
fi
|
||||
if git -C "${sdl_stage}" apply --check "${sdl_patch}" 2>/dev/null; then
|
||||
git -C "${sdl_stage}" apply "${sdl_patch}"
|
||||
elif ! git -C "${sdl_stage}" apply --reverse --check "${sdl_patch}" \
|
||||
2>/dev/null; then
|
||||
echo "staged PS5 SDL checkout differs from the reviewed overlay" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ "$(git -C "${sdl_stage}" diff --name-only)" != \
|
||||
"src/video/ps5/SDL_ps5video.c" ]]; then
|
||||
echo "staged PS5 SDL checkout has changes outside the reviewed overlay" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
export LLVM_CONFIG="${LLVM_CONFIG:-/usr/bin/llvm-config-18}"
|
||||
|
||||
cmake -S "${sdl_stage}" -B "${sdl_build}" -G Ninja \
|
||||
-DCMAKE_TOOLCHAIN_FILE="${sdk}/toolchain/prospero.cmake" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DSDL_SHARED=OFF \
|
||||
-DSDL_STATIC=ON \
|
||||
-DSDL_TEST=OFF \
|
||||
-DSDL2_DISABLE_SDL2MAIN=ON \
|
||||
-DSDL_AUDIO=OFF \
|
||||
-DSDL_JOYSTICK=OFF \
|
||||
-DSDL_HAPTIC=OFF \
|
||||
-DSDL_SENSOR=OFF \
|
||||
-DSDL_POWER=OFF \
|
||||
-DSDL_FILE=OFF \
|
||||
-DSDL_FILESYSTEM=OFF \
|
||||
-DSDL_LOCALE=OFF \
|
||||
-DSDL_MISC=OFF \
|
||||
-DSDL_OPENGL=OFF \
|
||||
-DSDL_LOADSO=OFF \
|
||||
-DSDL_RENDER=OFF \
|
||||
-DSDL_VULKAN=OFF \
|
||||
-DSDL_DUMMYVIDEO=OFF \
|
||||
-DSDL_OFFSCREEN=OFF \
|
||||
-DSDL_HIDAPI=OFF
|
||||
cmake --build "${sdl_build}" --target SDL2-static
|
||||
|
||||
cmake -S "${root}" -B "${project_build}" -G Ninja \
|
||||
-DCMAKE_TOOLCHAIN_FILE="${sdk}/toolchain/prospero.cmake" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_TESTING=OFF \
|
||||
-DCHIMERA_GFX_BUILD_PS5_PROBE=ON \
|
||||
-DCHIMERA_GFX_BUILD_PHASE1_VIDEOOUT_CLEAR=ON \
|
||||
-DCHIMERA_GFX_PHASE1_SDL_BUILD="${sdl_build}" \
|
||||
-DCHIMERA_GFX_PS5_ALLOWED_FIRMWARE=NONE
|
||||
cmake --build "${project_build}" --clean-first
|
||||
|
||||
python3 "${root}/tools/audit_ps5_artifacts.py" \
|
||||
--nm "${sdk}/bin/prospero-nm" \
|
||||
--readelf "$(command -v llvm-readelf-18)" \
|
||||
--probe "${project_build}/chimera-gfx-capability-probe.elf" \
|
||||
--symbol-manifest "${root}/manifests/ps5_gnm_symbols.json" \
|
||||
--phase1 "${project_build}/chimera-gfx-phase1-videoout-clear.elf"
|
||||
|
||||
echo "Built offline only; do not transfer or execute either ELF."
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Derive the W^X linker script and build the offline Phase-1.0DM ELF."""
|
||||
from __future__ import annotations
|
||||
import argparse,hashlib,subprocess
|
||||
from pathlib import Path
|
||||
|
||||
SDK_SCRIPT_SHA256="3bacf56a21602a1298752023a4c5b0c305954ddf6cdbce1464476fc9da9d9093"
|
||||
def main():
|
||||
p=argparse.ArgumentParser();p.add_argument("--root",type=Path,required=True);a=p.parse_args();root=a.root.resolve();sdk=root/"work/toolchains/ps5-payload-sdk-v0.41";source=sdk/"target/lib/main.script";raw=source.read_bytes()
|
||||
if hashlib.sha256(raw).hexdigest()!=SDK_SCRIPT_SHA256:raise SystemExit("SDK linker script identity mismatch")
|
||||
text=raw.decode();needle="ph_text PT_LOAD FLAGS (0x7);"
|
||||
if text.count(needle)!=1:raise SystemExit("SDK text PHDR differs")
|
||||
derived=text.replace(needle,"ph_text PT_LOAD FLAGS (0x5);")
|
||||
out=root/"build/phase10dm";out.mkdir(parents=True,exist_ok=True);script=out/"observer-wx.script";script.write_text(derived,newline="\n")
|
||||
source=root.parent/"chimera-retroarch/pkg/ps5/chimera_ps5_title_snapshot_observer.c";artifact=out/"chimera_title_snapshot_observer.elf";linkmap=out/"chimera_title_snapshot_observer.map"
|
||||
if not source.is_file():raise SystemExit("target source missing")
|
||||
def wsl(path):return "/mnt/c/"+path.as_posix().split(":/",1)[1]
|
||||
command=f"set -euo pipefail; '{wsl(sdk)}/bin/prospero-clang' -std=c11 -Wall -Wextra -Werror -O2 -fno-common -Wl,-T,'{wsl(script)}' -Wl,-Map,'{wsl(linkmap)}' -o '{wsl(artifact)}' '{wsl(source)}'"
|
||||
subprocess.run(["wsl.exe","bash","-lc",command],check=True)
|
||||
return 0
|
||||
if __name__=="__main__":raise SystemExit(main())
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Derive the W^X linker script and build the offline Phase-1.0DQ ELF."""
|
||||
from __future__ import annotations
|
||||
import argparse,hashlib,subprocess
|
||||
from pathlib import Path
|
||||
|
||||
SDK_SCRIPT_SHA256="3bacf56a21602a1298752023a4c5b0c305954ddf6cdbce1464476fc9da9d9093"
|
||||
def main():
|
||||
p=argparse.ArgumentParser();p.add_argument("--root",type=Path,required=True);a=p.parse_args();root=a.root.resolve();sdk=root/"work/toolchains/ps5-payload-sdk-v0.41";source_script=sdk/"target/lib/main.script";raw=source_script.read_bytes()
|
||||
if hashlib.sha256(raw).hexdigest()!=SDK_SCRIPT_SHA256:raise SystemExit("SDK linker script identity mismatch")
|
||||
text=raw.decode();needle="ph_text PT_LOAD FLAGS (0x7);"
|
||||
if text.count(needle)!=1:raise SystemExit("SDK text PHDR differs")
|
||||
out=root/"build/phase10dq";out.mkdir(parents=True,exist_ok=True);script=out/"observer-wx.script";script.write_text(text.replace(needle,"ph_text PT_LOAD FLAGS (0x5);"),newline="\n")
|
||||
source=root.parent/"chimera-retroarch/pkg/ps5/chimera_ps5_fake00000_inventory_observer.c";artifact=out/"chimera_fake00000_inventory_observer.elf";linkmap=out/"chimera_fake00000_inventory_observer.map"
|
||||
if not source.is_file():raise SystemExit("target source missing")
|
||||
def wsl(path):return "/mnt/c/"+path.as_posix().split(":/",1)[1]
|
||||
command=f"set -euo pipefail; '{wsl(sdk)}/bin/prospero-clang' -std=c11 -Wall -Wextra -Werror -O2 -fno-common -Wl,-T,'{wsl(script)}' -Wl,-Map,'{wsl(linkmap)}' -o '{wsl(artifact)}' '{wsl(source)}'"
|
||||
subprocess.run(["wsl.exe","bash","-lc",command],check=True)
|
||||
return 0
|
||||
if __name__=="__main__":raise SystemExit(main())
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import annotations
|
||||
import argparse,hashlib,subprocess
|
||||
from pathlib import Path
|
||||
SDK_SCRIPT_SHA256="3bacf56a21602a1298752023a4c5b0c305954ddf6cdbce1464476fc9da9d9093"
|
||||
def main():
|
||||
p=argparse.ArgumentParser();p.add_argument("--root",type=Path,required=True);root=p.parse_args().root.resolve();sdk=root/"work/toolchains/ps5-payload-sdk-v0.41";raw=(sdk/"target/lib/main.script").read_bytes()
|
||||
if hashlib.sha256(raw).hexdigest()!=SDK_SCRIPT_SHA256:raise SystemExit("SDK linker script identity mismatch")
|
||||
text=raw.decode();needle="ph_text PT_LOAD FLAGS (0x7);"
|
||||
if text.count(needle)!=1:raise SystemExit("SDK text PHDR differs")
|
||||
out=root/"build/phase10ds";out.mkdir(parents=True,exist_ok=True);script=out/"observer-wx.script";script.write_text(text.replace(needle,"ph_text PT_LOAD FLAGS (0x5);"),newline="\n")
|
||||
source=root.parent/"chimera-retroarch/pkg/ps5/chimera_ps5_fake00000_metadata_observer.c";artifact=out/"chimera_fake00000_metadata_observer.elf";linkmap=out/"chimera_fake00000_metadata_observer.map"
|
||||
def wsl(path):return "/mnt/c/"+path.as_posix().split(":/",1)[1]
|
||||
subprocess.run(["wsl.exe","bash","-lc",f"set -euo pipefail; '{wsl(sdk)}/bin/prospero-clang' -std=c11 -Wall -Wextra -Werror -O2 -fno-common -Wl,-T,'{wsl(script)}' -Wl,-Map,'{wsl(linkmap)}' -o '{wsl(artifact)}' '{wsl(source)}'"],check=True)
|
||||
if __name__=="__main__":main()
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import argparse,hashlib,subprocess
|
||||
from pathlib import Path
|
||||
H="3bacf56a21602a1298752023a4c5b0c305954ddf6cdbce1464476fc9da9d9093"
|
||||
def main():
|
||||
p=argparse.ArgumentParser();p.add_argument("--root",type=Path,required=True);r=p.parse_args().root.resolve();sdk=r/"work/toolchains/ps5-payload-sdk-v0.41";raw=(sdk/"target/lib/main.script").read_bytes()
|
||||
if hashlib.sha256(raw).hexdigest()!=H:raise SystemExit("SDK identity")
|
||||
out=r/"build/phase10du";out.mkdir(parents=True,exist_ok=True);script=out/"wx.script";script.write_text(raw.decode().replace("ph_text PT_LOAD FLAGS (0x7);","ph_text PT_LOAD FLAGS (0x5);"),newline="\n");src=r.parent/"chimera-retroarch/pkg/ps5/chimera_ps5_fake00000_package_stat.c";elf=out/"chimera_fake00000_package_stat.elf"
|
||||
def w(p):return "/mnt/c/"+p.as_posix().split(":/",1)[1]
|
||||
subprocess.run(["wsl.exe","bash","-lc",f"set -e; '{w(sdk)}/bin/prospero-clang' -std=c11 -Wall -Wextra -Werror -O2 -Wl,-T,'{w(script)}' -o '{w(elf)}' '{w(src)}'"],check=True)
|
||||
if __name__=="__main__":main()
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import argparse,hashlib,subprocess
|
||||
from pathlib import Path
|
||||
H="3bacf56a21602a1298752023a4c5b0c305954ddf6cdbce1464476fc9da9d9093"
|
||||
def main():
|
||||
p=argparse.ArgumentParser();p.add_argument("--root",type=Path,required=True);r=p.parse_args().root.resolve();sdk=r/"work/toolchains/ps5-payload-sdk-v0.41";raw=(sdk/"target/lib/main.script").read_bytes()
|
||||
if hashlib.sha256(raw).hexdigest()!=H:raise SystemExit("SDK identity")
|
||||
out=r/"build/phase10dw";out.mkdir(parents=True,exist_ok=True);script=out/"wx.script";script.write_text(raw.decode().replace("ph_text PT_LOAD FLAGS (0x7);","ph_text PT_LOAD FLAGS (0x5);"),newline="\n");src=r.parent/"chimera-retroarch/pkg/ps5/chimera_ps5_fake00000_package_readback.c";elf=out/"chimera_fake00000_package_readback.elf"
|
||||
def w(p):return "/mnt/c/"+p.as_posix().split(":/",1)[1]
|
||||
subprocess.run(["wsl.exe","bash","-lc",f"set -e; '{w(sdk)}/bin/prospero-clang' -std=c11 -Wall -Wextra -Werror -O2 -Wl,-T,'{w(script)}' -o '{w(elf)}' '{w(src)}'"],check=True)
|
||||
if __name__=="__main__":main()
|
||||
@@ -0,0 +1,341 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Fail-closed static eligibility gate for artifact execution tooling."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
SHA256_PATTERN = re.compile(r"[0-9a-f]{64}")
|
||||
COMMIT_PATTERN = re.compile(r"[0-9a-f]{40}")
|
||||
READY_DECISION = "READY_FOR_HARDENED_RUNTIME_DEPLOYMENT"
|
||||
HARD_EFFECTS = {"PERSISTENT_WRITE", "UNBOUNDED_OR_UNKNOWN"}
|
||||
ALLOWED_EFFECTS = {
|
||||
"ALLOWED_APPLICATION_WRITE",
|
||||
"BOUNDED_WATCHDOG",
|
||||
"EXPLICIT_PROCESS_EXIT",
|
||||
"EXPECTED_VOLATILE_RUNTIME_EFFECT",
|
||||
"FAIL_CLOSED_TERMINATION",
|
||||
"HASH_BOUND_SAME_FD",
|
||||
"OS_RECLAIMED_ON_EXIT",
|
||||
"RESTORED_BY_LOADER",
|
||||
"PAYLOAD_PROCESS_LOCAL",
|
||||
*HARD_EFFECTS,
|
||||
}
|
||||
EXPECTED_PAYLOAD_MANAGER = {
|
||||
"base_commit": "cfbc70f30f419b09bf2b52283f7409e2d3117ee1",
|
||||
"hardened_commit": "e23d94ff91233aa770e2342800c1467875bdef44",
|
||||
"installed": False,
|
||||
"release": "v0.3.1-chimera-controlled-phase07",
|
||||
"reproducible": True,
|
||||
"sha256": "8fecf8241a46246eddbd21e8bb4d875f5d76f1f4f4c6a11384df1f131aa5e5b1",
|
||||
"size": 99560,
|
||||
}
|
||||
EXPECTED_LOADER = {
|
||||
"base_commit": "699e8bcff03e91e8d6ca6eba281af25c5a58d8c2",
|
||||
"hardened_commit": "197623058f509eddde18868dafcb92fdcac66464",
|
||||
"installed": False,
|
||||
"release": "v0.23-chimera-phase07",
|
||||
"reproducible": True,
|
||||
"sha256": "63e810982471eb40cae3a20aa9df9a0a02892f420e429874fae4e99aa400b561",
|
||||
"size": 397000,
|
||||
}
|
||||
EXPECTED_SDK = {
|
||||
"commit": "d2e2e585740362976a39fdd5ccf390f199a7bc37",
|
||||
"release": "v0.41",
|
||||
}
|
||||
|
||||
|
||||
def hash_file(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 deny(reason_codes: list[str], artifact_sha256: str | None = None) -> int:
|
||||
result: dict[str, Any] = {
|
||||
"decision": "DENY",
|
||||
"execution_authorized": False,
|
||||
"reason_codes": sorted(set(reason_codes)),
|
||||
"schema_version": 1,
|
||||
}
|
||||
if artifact_sha256 is not None:
|
||||
result["artifact_sha256"] = artifact_sha256
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 2
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
document = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(document, dict):
|
||||
raise ValueError(f"{path}: root must be an object")
|
||||
return document
|
||||
|
||||
|
||||
def validate_runtime_profile(
|
||||
profile: dict[str, Any],
|
||||
manifest: dict[str, Any],
|
||||
firmware: str | None,
|
||||
) -> list[str]:
|
||||
if profile.get("schema_version") != 1:
|
||||
raise ValueError("unsupported controlled runtime profile schema")
|
||||
if profile.get("profile") != "controlled-ps5-runtime":
|
||||
raise ValueError("unexpected controlled runtime profile")
|
||||
|
||||
reasons: list[str] = []
|
||||
artifact = profile["artifact"]
|
||||
execution = profile["execution"]
|
||||
profile_firmware = profile["firmware"]
|
||||
payload_manager = profile["payload_manager"]
|
||||
loader = profile["loader"]
|
||||
sdk = profile["sdk"]
|
||||
budgets = profile["budgets"]
|
||||
deployment = profile["deployment"]
|
||||
effects = profile["effects"]
|
||||
expected_volatile_effects = profile["expected_volatile_effects"]
|
||||
blockers = profile["hard_blockers"]
|
||||
if not all(
|
||||
isinstance(item, dict)
|
||||
for item in (
|
||||
artifact,
|
||||
execution,
|
||||
profile_firmware,
|
||||
payload_manager,
|
||||
loader,
|
||||
sdk,
|
||||
budgets,
|
||||
deployment,
|
||||
)
|
||||
):
|
||||
raise ValueError("controlled runtime profile contains a malformed object")
|
||||
if (
|
||||
not isinstance(effects, list)
|
||||
or not isinstance(expected_volatile_effects, list)
|
||||
or not isinstance(blockers, list)
|
||||
):
|
||||
raise ValueError("controlled runtime profile arrays are malformed")
|
||||
|
||||
if profile.get("decision") != READY_DECISION:
|
||||
reasons.append("RUNTIME_PROFILE_BLOCKED")
|
||||
if (
|
||||
profile.get("execution_authorized") is not False
|
||||
or execution.get("authorized") is not False
|
||||
or execution.get("transferred") is not False
|
||||
or execution.get("executed") is not False
|
||||
):
|
||||
reasons.append("RUNTIME_PROFILE_EXECUTION_STATE_INVALID")
|
||||
if execution.get("execution_eligible") is not True:
|
||||
reasons.append("RUNTIME_PROFILE_EXECUTION_INELIGIBLE")
|
||||
if artifact.get("built") is not True:
|
||||
reasons.append("RUNTIME_PROFILE_ARTIFACT_NOT_BUILT")
|
||||
if blockers:
|
||||
reasons.append("RUNTIME_PROFILE_HARD_BLOCKERS_PRESENT")
|
||||
|
||||
manifest_artifact = manifest["artifact"]
|
||||
manifest_source = manifest.get("source")
|
||||
if not isinstance(manifest_source, dict):
|
||||
raise ValueError("artifact manifest source record is required")
|
||||
for key in ("id", "filename", "sha256", "size"):
|
||||
if artifact.get(key) != manifest_artifact.get(key):
|
||||
reasons.append("RUNTIME_PROFILE_ARTIFACT_MISMATCH")
|
||||
source_commit = artifact.get("source_commit")
|
||||
if (
|
||||
not isinstance(source_commit, str)
|
||||
or COMMIT_PATTERN.fullmatch(source_commit) is None
|
||||
or source_commit != manifest_source.get("commit")
|
||||
or manifest_source.get("dirty") is not False
|
||||
):
|
||||
reasons.append("RUNTIME_PROFILE_SOURCE_COMMIT_MISMATCH")
|
||||
|
||||
if firmware is None:
|
||||
reasons.append("EXACT_FIRMWARE_REQUIRED")
|
||||
if profile_firmware.get("exact") != "9.60" or firmware != "9.60":
|
||||
reasons.append("FIRMWARE_MISMATCH")
|
||||
if profile_firmware.get("evidence") != "jens_explicitly_confirmed_exact_9.60":
|
||||
reasons.append("FIRMWARE_EVIDENCE_MISMATCH")
|
||||
|
||||
for key, value in EXPECTED_PAYLOAD_MANAGER.items():
|
||||
if payload_manager.get(key) != value:
|
||||
reasons.append("PAYLOAD_MANAGER_IDENTITY_MISMATCH")
|
||||
for key, value in EXPECTED_LOADER.items():
|
||||
if loader.get(key) != value:
|
||||
reasons.append("LOADER_IDENTITY_MISMATCH")
|
||||
for key, value in EXPECTED_SDK.items():
|
||||
if sdk.get(key) != value:
|
||||
reasons.append("SDK_IDENTITY_MISMATCH")
|
||||
|
||||
expected_budgets = {
|
||||
"automatic_retry": False,
|
||||
"filesystem_write_budget": "controlled_artifact_directory_only",
|
||||
"payload_network_access": "none",
|
||||
"persistent_write_budget": "controlled_artifact_removable",
|
||||
}
|
||||
for key, value in expected_budgets.items():
|
||||
if budgets.get(key) != value:
|
||||
reasons.append("RUNTIME_BUDGET_MISMATCH")
|
||||
runtime = budgets.get("maximum_runtime_ms")
|
||||
if (
|
||||
not isinstance(runtime, int)
|
||||
or isinstance(runtime, bool)
|
||||
or not (1 <= runtime <= 2000)
|
||||
):
|
||||
reasons.append("RUNTIME_BUDGET_MISMATCH")
|
||||
if deployment != {
|
||||
"installed": False,
|
||||
"ready_for_installation": True,
|
||||
"rollback_prepared": True,
|
||||
}:
|
||||
reasons.append("DEPLOYMENT_STATE_MISMATCH")
|
||||
|
||||
classified_volatile_effects: list[str] = []
|
||||
effect_ids: set[str] = set()
|
||||
for item in effects:
|
||||
if not isinstance(item, dict):
|
||||
raise ValueError("controlled runtime effect must be an object")
|
||||
effect_id = item.get("id")
|
||||
if not isinstance(effect_id, str) or not effect_id or effect_id in effect_ids:
|
||||
raise ValueError("controlled runtime effect IDs must be unique strings")
|
||||
effect_ids.add(effect_id)
|
||||
if item.get("classification") not in ALLOWED_EFFECTS:
|
||||
raise ValueError("controlled runtime effect classification is invalid")
|
||||
if item.get("classification") == "EXPECTED_VOLATILE_RUNTIME_EFFECT":
|
||||
classified_volatile_effects.append(effect_id)
|
||||
if item.get("classification") in HARD_EFFECTS:
|
||||
reasons.append("RUNTIME_PROFILE_HARD_EFFECT")
|
||||
if (
|
||||
not all(
|
||||
isinstance(item, str) and item for item in expected_volatile_effects
|
||||
)
|
||||
or len(set(expected_volatile_effects)) != len(expected_volatile_effects)
|
||||
or sorted(expected_volatile_effects) != sorted(classified_volatile_effects)
|
||||
):
|
||||
reasons.append("RUNTIME_PROFILE_VOLATILE_EFFECTS_MISMATCH")
|
||||
|
||||
return reasons
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--manifest", type=Path, required=True)
|
||||
parser.add_argument("--denylist", type=Path, required=True)
|
||||
parser.add_argument("--artifact", type=Path)
|
||||
parser.add_argument("--runtime-profile", type=Path)
|
||||
parser.add_argument("--firmware")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
manifest = load_json(args.manifest)
|
||||
denylist = load_json(args.denylist)
|
||||
if manifest.get("schema_version") != 1:
|
||||
raise ValueError("unsupported artifact manifest schema")
|
||||
if denylist.get("schema_version") != 1:
|
||||
raise ValueError("unsupported denylist schema")
|
||||
if denylist.get("hash_algorithm") != "sha256":
|
||||
raise ValueError("denylist hash algorithm must be sha256")
|
||||
if denylist.get("fail_closed") is not True:
|
||||
raise ValueError("denylist must be fail-closed")
|
||||
|
||||
artifact_record = manifest["artifact"]
|
||||
execution = manifest["execution"]
|
||||
if not isinstance(artifact_record, dict) or not isinstance(execution, dict):
|
||||
raise ValueError("manifest artifact and execution records must be objects")
|
||||
digest = artifact_record["sha256"]
|
||||
if not isinstance(digest, str) or SHA256_PATTERN.fullmatch(digest) is None:
|
||||
raise ValueError("manifest artifact SHA-256 is invalid")
|
||||
if not isinstance(execution.get("execution_eligible"), bool):
|
||||
raise ValueError("execution_eligible must be an explicit boolean")
|
||||
if any(
|
||||
execution.get(key) is not False
|
||||
for key in ("authorized", "transferred", "executed")
|
||||
):
|
||||
reasons = ["MANIFEST_EXECUTION_STATE_INVALID"]
|
||||
else:
|
||||
reasons = []
|
||||
if not isinstance(artifact_record.get("filename"), str):
|
||||
raise ValueError("artifact filename must be a string")
|
||||
if (
|
||||
not isinstance(artifact_record.get("size"), int)
|
||||
or artifact_record["size"] < 1
|
||||
):
|
||||
raise ValueError("artifact size must be a positive integer")
|
||||
|
||||
blocked_hashes: set[str] = set()
|
||||
entries = denylist["entries"]
|
||||
if not isinstance(entries, list) or not entries:
|
||||
raise ValueError("denylist entries must be a non-empty array")
|
||||
for entry in entries:
|
||||
entry_digest = entry["sha256"]
|
||||
if not isinstance(entry_digest, str) or SHA256_PATTERN.fullmatch(
|
||||
entry_digest
|
||||
) is None:
|
||||
raise ValueError("denylist contains an invalid SHA-256")
|
||||
if (
|
||||
entry.get("status") != "BLOCKED"
|
||||
or entry.get("permanent") is not True
|
||||
or entry.get("execution_eligible") is not False
|
||||
):
|
||||
raise ValueError("denylist entry is not permanently blocked")
|
||||
if entry_digest in blocked_hashes:
|
||||
raise ValueError("denylist contains a duplicate SHA-256")
|
||||
blocked_hashes.add(entry_digest)
|
||||
|
||||
if execution["execution_eligible"] is not True:
|
||||
reasons.append("MANIFEST_EXECUTION_INELIGIBLE")
|
||||
if digest in blocked_hashes:
|
||||
reasons.append("ARTIFACT_PERMANENTLY_DENYLISTED")
|
||||
|
||||
if execution["execution_eligible"] is True:
|
||||
if args.runtime_profile is None:
|
||||
reasons.append("CONTROLLED_RUNTIME_PROFILE_REQUIRED")
|
||||
else:
|
||||
profile = load_json(args.runtime_profile)
|
||||
reasons.extend(
|
||||
validate_runtime_profile(profile, manifest, args.firmware)
|
||||
)
|
||||
elif args.runtime_profile is not None:
|
||||
profile = load_json(args.runtime_profile)
|
||||
reasons.extend(validate_runtime_profile(profile, manifest, args.firmware))
|
||||
|
||||
if args.artifact is not None:
|
||||
artifact = args.artifact.resolve(strict=True)
|
||||
if not artifact.is_file():
|
||||
raise ValueError("artifact is not a regular file")
|
||||
if artifact.name != artifact_record["filename"]:
|
||||
reasons.append("ARTIFACT_FILENAME_MISMATCH")
|
||||
if artifact.stat().st_size != artifact_record["size"]:
|
||||
reasons.append("ARTIFACT_SIZE_MISMATCH")
|
||||
if hash_file(artifact) != digest:
|
||||
reasons.append("ARTIFACT_DIGEST_MISMATCH")
|
||||
else:
|
||||
reasons.append("ARTIFACT_BYTES_NOT_SUPPLIED")
|
||||
|
||||
if reasons:
|
||||
return deny(reasons, digest)
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"artifact_sha256": digest,
|
||||
"decision": "PASS_STATIC_DEPLOYMENT_ELIGIBILITY_GATE",
|
||||
"execution_authorized": False,
|
||||
"reason_codes": [],
|
||||
"schema_version": 1,
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
except (KeyError, OSError, TypeError, ValueError, json.JSONDecodeError):
|
||||
return deny(["INVALID_OR_INCOMPLETE_POLICY_INPUT"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Check project-owned C and header files with clang-format."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
parser.add_argument("--clang-format", required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
root = args.root.resolve()
|
||||
files: list[Path] = []
|
||||
for directory in ("include", "src", "adapters", "samples", "tests"):
|
||||
files.extend(sorted((root / directory).rglob("*.c")))
|
||||
files.extend(sorted((root / directory).rglob("*.h")))
|
||||
|
||||
result = subprocess.run(
|
||||
[args.clang_format, "--dry-run", "--Werror", *map(str, files)],
|
||||
cwd=root,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return result.returncode
|
||||
print(f"format check passed across {len(files)} C/header files")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "Usage: $0 OUTPUT_DIRECTORY" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
OUTPUT_DIR="$1"
|
||||
|
||||
if [ -e "$OUTPUT_DIR" ]; then
|
||||
echo "Output path already exists: $OUTPUT_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! git -C "$ROOT_DIR" -c core.fileMode=false diff --ignore-space-at-eol --quiet \
|
||||
|| ! git -C "$ROOT_DIR" -c core.fileMode=false diff --cached --quiet; then
|
||||
echo "Commit or stash repository changes before creating a public export." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
git -C "$ROOT_DIR" archive --format=tar HEAD | tar -xf - -C "$OUTPUT_DIR"
|
||||
|
||||
# Agent instructions contain private research-session context and are not part
|
||||
# of the distributable library or its safety evidence.
|
||||
find "$OUTPUT_DIR" -type f -name AGENTS.md -delete
|
||||
|
||||
for forbidden in \
|
||||
'.env' '*.pem' '*.key' '*.p12' '*.pfx' '*.db' '*.sqlite' '*.sqlite3' \
|
||||
'*.elf' '*.self' '*.bin' '*.dmp' '*.core' '*.zip' \
|
||||
'secret.key' 'id_rsa' 'id_ed25519'; do
|
||||
if find "$OUTPUT_DIR" -type f -name "$forbidden" -print -quit | grep -q .; then
|
||||
echo "Forbidden file found in public export: $forbidden" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if grep -RIlE --exclude='export-public-source.sh' \
|
||||
'192\.168\.10\.150|NuklearRabbit|C:\\Users\\Jens' "$OUTPUT_DIR" >/dev/null; then
|
||||
echo "Private operator marker found in public export." >&2
|
||||
exit 1
|
||||
fi
|
||||
if find "$OUTPUT_DIR" -type f -size +10M -print -quit | grep -q .; then
|
||||
echo "Unexpected file larger than 10 MiB found in public export." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git -C "$OUTPUT_DIR" init -q
|
||||
# The export is created in a fresh repository, so source paths that happen to
|
||||
# match a diagnostic ignore rule (for example src/core/) must still be added.
|
||||
git -C "$OUTPUT_DIR" add --force .
|
||||
git -C "$OUTPUT_DIR" -c user.name='Chimera GFX release export' \
|
||||
-c user.email='release-export@invalid.example' \
|
||||
commit -q -m "Publish Chimera GFX source"
|
||||
|
||||
EXPECTED_FILES="$(mktemp)"
|
||||
ACTUAL_FILES="$(mktemp)"
|
||||
trap 'rm -f "$EXPECTED_FILES" "$ACTUAL_FILES"' EXIT
|
||||
git -C "$ROOT_DIR" ls-tree -r --name-only HEAD \
|
||||
| grep -Ev '(^|/)AGENTS\.md$' \
|
||||
| LC_ALL=C sort > "$EXPECTED_FILES"
|
||||
git -C "$OUTPUT_DIR" ls-files | LC_ALL=C sort > "$ACTUAL_FILES"
|
||||
if ! diff -u "$EXPECTED_FILES" "$ACTUAL_FILES"; then
|
||||
echo "Public export does not contain the complete tracked source tree." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$OUTPUT_DIR"
|
||||
git ls-files -z | sort -z | xargs -0 sha256sum > PUBLIC-SOURCE-MANIFEST.sha256
|
||||
)
|
||||
git -C "$OUTPUT_DIR" add PUBLIC-SOURCE-MANIFEST.sha256
|
||||
git -C "$OUTPUT_DIR" -c user.name='Chimera GFX release export' \
|
||||
-c user.email='release-export@invalid.example' \
|
||||
commit -q --amend --no-edit
|
||||
git -C "$OUTPUT_DIR" tag public-release-baseline
|
||||
|
||||
echo "Public source export created at $OUTPUT_DIR"
|
||||
echo "Commit: $(git -C "$OUTPUT_DIR" rev-parse HEAD)"
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Generate a deterministic manifest for a locally built, unexecuted artifact."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SDK_COMMIT = "d2e2e585740362976a39fdd5ccf390f199a7bc37"
|
||||
SDL_COMMIT = "0baf4ac49382b537ba449901b5b6d0d189bb1fbb"
|
||||
|
||||
|
||||
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 main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--artifact", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--id", required=True)
|
||||
parser.add_argument("--version", required=True)
|
||||
parser.add_argument("--source-repository", required=True)
|
||||
parser.add_argument("--source-commit", required=True)
|
||||
parser.add_argument("--target", required=True)
|
||||
parser.add_argument("--firmware", default="NONE")
|
||||
parser.add_argument("--profile", required=True)
|
||||
parser.add_argument("--execution-eligible", action="store_true")
|
||||
parser.add_argument("--with-sdl", action="store_true")
|
||||
parser.add_argument("--note", action="append", default=[])
|
||||
args = parser.parse_args()
|
||||
|
||||
artifact = args.artifact.resolve(strict=True)
|
||||
if not artifact.is_file() or artifact.stat().st_size == 0:
|
||||
raise SystemExit("artifact must be a non-empty regular file")
|
||||
if len(args.source_commit) != 40 or any(
|
||||
character not in "0123456789abcdef" for character in args.source_commit
|
||||
):
|
||||
raise SystemExit("source commit must be a lowercase 40-character SHA-1")
|
||||
|
||||
toolchain = {
|
||||
"ps5_payload_sdk_release": "v0.41",
|
||||
"ps5_payload_sdk_commit": SDK_COMMIT,
|
||||
}
|
||||
if args.with_sdl:
|
||||
toolchain["sdl_commit"] = SDL_COMMIT
|
||||
|
||||
document = {
|
||||
"schema_version": 1,
|
||||
"artifact": {
|
||||
"id": args.id,
|
||||
"version": args.version,
|
||||
"filename": artifact.name,
|
||||
"size": artifact.stat().st_size,
|
||||
"sha256": sha256(artifact),
|
||||
"target": args.target,
|
||||
},
|
||||
"source": {
|
||||
"repository": args.source_repository,
|
||||
"commit": args.source_commit,
|
||||
"dirty": False,
|
||||
},
|
||||
"toolchain": toolchain,
|
||||
"firmware_gate": {
|
||||
"embedded_identifier": args.firmware,
|
||||
"allowlisted": args.firmware != "NONE",
|
||||
},
|
||||
"execution": {
|
||||
"execution_eligible": args.execution_eligible,
|
||||
"authorized": False,
|
||||
"transferred": False,
|
||||
"executed": False,
|
||||
},
|
||||
"safety": {
|
||||
"profile": args.profile,
|
||||
"direct_gnm_imports": 0,
|
||||
"notes": args.note,
|
||||
},
|
||||
}
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(
|
||||
json.dumps(document, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Generate the C X-macro list from the read-only JSON manifest."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def render(manifest_path: Path) -> str:
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
symbols = manifest["symbols"]
|
||||
lines = ["/* Generated from manifests/ps5_gnm_symbols.json. Do not edit. */"]
|
||||
lines.extend(f'CHIMERA_GNM_SYMBOL("{entry["name"]}")' for entry in symbols)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--manifest", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--check", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
expected = render(args.manifest)
|
||||
if args.check:
|
||||
if not args.output.exists() or args.output.read_text(encoding="utf-8") != expected:
|
||||
print(f"stale generated file: {args.output}")
|
||||
return 1
|
||||
print(f"generated file is current: {args.output}")
|
||||
return 0
|
||||
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(expected, encoding="utf-8", newline="\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,509 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Read-only, host-only parser for bounded PS5 SIECAF structural metadata.
|
||||
|
||||
This module never decrypts, extracts content, invokes ps5-bar-tool, opens a
|
||||
network connection, or writes an output file. The byte layout is based on:
|
||||
|
||||
* https://www.psdevwiki.com/ps5/Archive.dat
|
||||
* c0w-ar/ps5-bar-tool include/bar_file.h at
|
||||
36d014672bc87577a6e0d750c2cccadc3fae0854
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from contextlib import contextmanager
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import struct
|
||||
from typing import Any, BinaryIO, Iterator
|
||||
import zipfile
|
||||
|
||||
|
||||
MAGIC = b"SIECAF\x00\x00"
|
||||
HEADER = struct.Struct("<8sQiIiI16s12sIQQQ")
|
||||
SEGMENT_META = struct.Struct("<iHHQQQQ12sIQ")
|
||||
SECTION_HASH = struct.Struct("<ii16s24s")
|
||||
ALIGNMENT = 0x10000
|
||||
UINT64_MAX = (1 << 64) - 1
|
||||
SUPPORTED_VERSIONS = frozenset({3, 6})
|
||||
|
||||
|
||||
class SiecafError(ValueError):
|
||||
"""A fail-closed SIECAF structural error."""
|
||||
|
||||
|
||||
def _sha256(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def _checked_add(left: int, right: int, label: str) -> int:
|
||||
result = left + right
|
||||
if left < 0 or right < 0 or result > UINT64_MAX:
|
||||
raise SiecafError(f"{label}: uint64 addition overflow")
|
||||
return result
|
||||
|
||||
|
||||
def _checked_mul(left: int, right: int, label: str) -> int:
|
||||
result = left * right
|
||||
if left < 0 or right < 0 or result > UINT64_MAX:
|
||||
raise SiecafError(f"{label}: uint64 multiplication overflow")
|
||||
return result
|
||||
|
||||
|
||||
def _read_exact(stream: BinaryIO, size: int, label: str) -> bytes:
|
||||
value = stream.read(size)
|
||||
if len(value) != size:
|
||||
raise SiecafError(f"{label}: truncated input")
|
||||
return value
|
||||
|
||||
|
||||
def _canonical_hash(records: list[dict[str, Any]]) -> str:
|
||||
encoded = json.dumps(
|
||||
records, sort_keys=True, separators=(",", ":"), ensure_ascii=True
|
||||
).encode("ascii")
|
||||
return _sha256(encoded)
|
||||
|
||||
|
||||
def inspect_siecaf(
|
||||
stream: BinaryIO, source_size: int, *, source_label: str = "<stream>"
|
||||
) -> dict[str, Any]:
|
||||
"""Parse structural metadata from an already-open, read-only stream."""
|
||||
if source_size < HEADER.size:
|
||||
return {
|
||||
"source": source_label,
|
||||
"source_size": source_size,
|
||||
"classification": "SIECAF_MALFORMED",
|
||||
"errors": ["header: truncated input"],
|
||||
"warnings": [],
|
||||
}
|
||||
stream.seek(0)
|
||||
header_raw = _read_exact(stream, HEADER.size, "header")
|
||||
(
|
||||
magic,
|
||||
unknown_u64,
|
||||
mode,
|
||||
pad1,
|
||||
version,
|
||||
pad2,
|
||||
key,
|
||||
iv12,
|
||||
pad3,
|
||||
segment_count,
|
||||
file_offset,
|
||||
file_size,
|
||||
) = HEADER.unpack(header_raw)
|
||||
header = {
|
||||
"magic_hex": magic.hex(),
|
||||
"magic_ascii": magic.rstrip(b"\x00").decode("ascii", errors="replace"),
|
||||
"unknown_u64": unknown_u64,
|
||||
"mode_i32": mode,
|
||||
"padding_1_u32": pad1,
|
||||
"version_i32": version,
|
||||
"padding_2_u32": pad2,
|
||||
"key_or_unknown_16_redacted": True,
|
||||
"key_or_unknown_16_sha256": _sha256(key),
|
||||
"iv_12_hex": iv12.hex(),
|
||||
"padding_3_u32": pad3,
|
||||
"segment_count": segment_count,
|
||||
"file_offset": file_offset,
|
||||
"file_size": file_size,
|
||||
"raw_sha256": _sha256(header_raw),
|
||||
}
|
||||
if magic != MAGIC:
|
||||
return {
|
||||
"source": source_label,
|
||||
"source_size": source_size,
|
||||
"classification": "SIECAF_MALFORMED",
|
||||
"header": header,
|
||||
"errors": ["magic: expected SIECAF\\0\\0"],
|
||||
"warnings": [],
|
||||
}
|
||||
if version not in SUPPORTED_VERSIONS:
|
||||
return {
|
||||
"source": source_label,
|
||||
"source_size": source_size,
|
||||
"classification": "SIECAF_UNSUPPORTED_VERSION",
|
||||
"header": header,
|
||||
"errors": [f"unsupported SIECAF header version: {version}"],
|
||||
"warnings": [],
|
||||
}
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
if segment_count == 0:
|
||||
errors.append("segment_count: zero")
|
||||
try:
|
||||
metadata_bytes = _checked_mul(
|
||||
segment_count, SEGMENT_META.size, "segment metadata table"
|
||||
)
|
||||
hash_bytes = _checked_mul(
|
||||
segment_count, SECTION_HASH.size, "section hash table"
|
||||
)
|
||||
metadata_end = _checked_add(HEADER.size, metadata_bytes, "metadata end")
|
||||
tables_end = _checked_add(metadata_end, hash_bytes, "tables end")
|
||||
declared_end = _checked_add(file_offset, file_size, "declared file end")
|
||||
except SiecafError as error:
|
||||
return {
|
||||
"source": source_label,
|
||||
"source_size": source_size,
|
||||
"classification": "SIECAF_MALFORMED",
|
||||
"header": header,
|
||||
"errors": [str(error)],
|
||||
"warnings": [],
|
||||
}
|
||||
if tables_end > source_size:
|
||||
errors.append("tables: outside source file")
|
||||
if tables_end > file_offset:
|
||||
errors.append("tables: overlap declared data region")
|
||||
if file_offset % ALIGNMENT != 0:
|
||||
errors.append(f"file_offset: not aligned to {ALIGNMENT}")
|
||||
if declared_end > source_size:
|
||||
errors.append("declared file range: outside source file")
|
||||
elif declared_end < source_size:
|
||||
warnings.append(
|
||||
f"trailing data after declared file range: {source_size - declared_end}"
|
||||
)
|
||||
if errors:
|
||||
return {
|
||||
"source": source_label,
|
||||
"source_size": source_size,
|
||||
"classification": "SIECAF_MALFORMED",
|
||||
"header": header,
|
||||
"tables_end": tables_end,
|
||||
"errors": errors,
|
||||
"warnings": warnings,
|
||||
}
|
||||
|
||||
metadata: list[dict[str, Any]] = []
|
||||
for index in range(segment_count):
|
||||
raw = _read_exact(stream, SEGMENT_META.size, f"segment metadata {index}")
|
||||
(
|
||||
section_id,
|
||||
padding_1,
|
||||
part_number,
|
||||
data_offset,
|
||||
aligned_length,
|
||||
hash_key_id,
|
||||
encryption_key_id,
|
||||
segment_iv12,
|
||||
segment_iv_padding,
|
||||
unaligned_length,
|
||||
) = SEGMENT_META.unpack(raw)
|
||||
iv16 = segment_iv12 + struct.pack("<I", segment_iv_padding)
|
||||
metadata.append(
|
||||
{
|
||||
"table_index": index,
|
||||
"section_id": section_id,
|
||||
"padding_1_u16": padding_1,
|
||||
"part_number": part_number,
|
||||
"data_offset": data_offset,
|
||||
"aligned_length": aligned_length,
|
||||
"unaligned_length": unaligned_length,
|
||||
"hash_key_id_or_algorithm_type": hash_key_id,
|
||||
"encryption_key_id_or_algorithm_version": encryption_key_id,
|
||||
"iv_hex": iv16.hex(),
|
||||
"raw_sha256": _sha256(raw),
|
||||
}
|
||||
)
|
||||
hashes: list[dict[str, Any]] = []
|
||||
for index in range(segment_count):
|
||||
raw = _read_exact(stream, SECTION_HASH.size, f"section hash {index}")
|
||||
section_id, section_type, section_hash, padding = SECTION_HASH.unpack(raw)
|
||||
hashes.append(
|
||||
{
|
||||
"table_index": index,
|
||||
"section_id": section_id,
|
||||
"section_type": section_type,
|
||||
"section_hash_128_hex": section_hash.hex(),
|
||||
"padding_hex": padding.hex(),
|
||||
"raw_sha256": _sha256(raw),
|
||||
}
|
||||
)
|
||||
|
||||
metadata_keys = [(item["section_id"], item["part_number"]) for item in metadata]
|
||||
metadata_ids = [item["section_id"] for item in metadata]
|
||||
hash_ids = [item["section_id"] for item in hashes]
|
||||
duplicate_metadata_ids = sorted(
|
||||
{
|
||||
section_id
|
||||
for section_id, part_number in metadata_keys
|
||||
if metadata_keys.count((section_id, part_number)) > 1
|
||||
}
|
||||
)
|
||||
duplicate_metadata_keys = sorted(
|
||||
{
|
||||
(section_id, part_number)
|
||||
for section_id, part_number in metadata_keys
|
||||
if metadata_keys.count((section_id, part_number)) > 1
|
||||
}
|
||||
)
|
||||
repeated_metadata_ids = sorted(
|
||||
{item for item in metadata_ids if metadata_ids.count(item) > 1}
|
||||
)
|
||||
duplicate_hash_ids = sorted({item for item in hash_ids if hash_ids.count(item) > 1})
|
||||
if duplicate_metadata_ids:
|
||||
errors.append(
|
||||
f"duplicate metadata section ID/part keys: {duplicate_metadata_keys}"
|
||||
)
|
||||
if duplicate_hash_ids:
|
||||
errors.append(f"duplicate hash section IDs: {duplicate_hash_ids}")
|
||||
if set(hash_ids) != set(range(segment_count)):
|
||||
errors.append("hash section IDs do not cover metadata table indexes")
|
||||
|
||||
hash_by_id = {
|
||||
item["section_id"]: item
|
||||
for item in hashes
|
||||
if item["section_id"] not in duplicate_hash_ids
|
||||
}
|
||||
ranges: list[tuple[int, int, int]] = []
|
||||
for item in metadata:
|
||||
section_id = item["section_id"]
|
||||
start = item["data_offset"]
|
||||
length = item["aligned_length"]
|
||||
unaligned = item["unaligned_length"]
|
||||
try:
|
||||
end = _checked_add(start, length, f"section {section_id} end")
|
||||
except SiecafError as error:
|
||||
errors.append(str(error))
|
||||
continue
|
||||
if section_id < 0:
|
||||
errors.append(f"section {section_id}: negative ID")
|
||||
if start % ALIGNMENT != 0:
|
||||
errors.append(f"section {section_id}: unaligned data offset")
|
||||
if length == 0 and unaligned != 0:
|
||||
errors.append(f"section {section_id}: incoherent unaligned length")
|
||||
if length > 0 and length % ALIGNMENT != 0:
|
||||
errors.append(f"section {section_id}: incoherent aligned length")
|
||||
if length > 0 and (unaligned > length or length - unaligned >= ALIGNMENT):
|
||||
errors.append(f"section {section_id}: incoherent unaligned length")
|
||||
if start < file_offset or end > declared_end or end > source_size:
|
||||
errors.append(f"section {section_id}: range outside declared file data")
|
||||
if length > 0:
|
||||
ranges.append((start, end, section_id))
|
||||
hash_record = hash_by_id.get(item["table_index"])
|
||||
item["section_type"] = (
|
||||
hash_record["section_type"] if hash_record is not None else None
|
||||
)
|
||||
item["section_hash_128_hex"] = (
|
||||
hash_record["section_hash_128_hex"] if hash_record is not None else None
|
||||
)
|
||||
|
||||
ranges.sort()
|
||||
overlaps: list[dict[str, int]] = []
|
||||
gaps: list[dict[str, int]] = []
|
||||
previous_end = file_offset
|
||||
previous_id = -1
|
||||
for start, end, section_id in ranges:
|
||||
if start < previous_end:
|
||||
overlaps.append(
|
||||
{
|
||||
"left_section_id": previous_id,
|
||||
"right_section_id": section_id,
|
||||
"overlap_bytes": previous_end - start,
|
||||
}
|
||||
)
|
||||
elif start > previous_end:
|
||||
gaps.append(
|
||||
{
|
||||
"after_section_id": previous_id,
|
||||
"before_section_id": section_id,
|
||||
"gap_bytes": start - previous_end,
|
||||
}
|
||||
)
|
||||
if end > previous_end:
|
||||
previous_end = end
|
||||
previous_id = section_id
|
||||
if overlaps:
|
||||
errors.append(f"overlapping section ranges: {len(overlaps)}")
|
||||
if gaps:
|
||||
warnings.append(f"gaps between section ranges: {len(gaps)}")
|
||||
trailing_data = max(0, source_size - previous_end)
|
||||
if trailing_data:
|
||||
warnings.append(f"trailing data after final section: {trailing_data}")
|
||||
if previous_end < declared_end:
|
||||
warnings.append(
|
||||
f"uncovered bytes inside declared data range: {declared_end - previous_end}"
|
||||
)
|
||||
|
||||
normalized_segments = [
|
||||
{
|
||||
key: item[key]
|
||||
for key in (
|
||||
"section_id",
|
||||
"part_number",
|
||||
"data_offset",
|
||||
"aligned_length",
|
||||
"unaligned_length",
|
||||
"hash_key_id_or_algorithm_type",
|
||||
"encryption_key_id_or_algorithm_version",
|
||||
"iv_hex",
|
||||
)
|
||||
}
|
||||
for item in sorted(
|
||||
metadata,
|
||||
key=lambda value: (
|
||||
value["section_id"],
|
||||
value["part_number"],
|
||||
value["data_offset"],
|
||||
),
|
||||
)
|
||||
]
|
||||
normalized_hashes = [
|
||||
{
|
||||
key: item[key]
|
||||
for key in ("section_id", "section_type", "section_hash_128_hex")
|
||||
}
|
||||
for item in sorted(
|
||||
hashes, key=lambda value: (value["section_id"], value["section_type"])
|
||||
)
|
||||
]
|
||||
normalized_layout = [
|
||||
{
|
||||
key: item[key]
|
||||
for key in (
|
||||
"section_id",
|
||||
"part_number",
|
||||
"data_offset",
|
||||
"aligned_length",
|
||||
"unaligned_length",
|
||||
"hash_key_id_or_algorithm_type",
|
||||
"encryption_key_id_or_algorithm_version",
|
||||
)
|
||||
}
|
||||
for item in normalized_segments
|
||||
]
|
||||
normalized_segment_table_sha256 = _canonical_hash(normalized_segments)
|
||||
normalized_hash_blocks_sha256 = _canonical_hash(normalized_hashes)
|
||||
normalized_layout_sha256 = _canonical_hash(
|
||||
[
|
||||
{
|
||||
"unknown_u64": unknown_u64,
|
||||
"version_i32": version,
|
||||
"segment_count": segment_count,
|
||||
"file_offset": file_offset,
|
||||
"file_size": file_size,
|
||||
},
|
||||
*normalized_layout,
|
||||
]
|
||||
)
|
||||
structural_fingerprint_sha256 = _canonical_hash(
|
||||
[
|
||||
{
|
||||
"header_raw_sha256": header["raw_sha256"],
|
||||
"normalized_segment_table_sha256": (normalized_segment_table_sha256),
|
||||
"normalized_hash_blocks_sha256": normalized_hash_blocks_sha256,
|
||||
}
|
||||
]
|
||||
)
|
||||
return {
|
||||
"source": source_label,
|
||||
"source_size": source_size,
|
||||
"classification": ("SIECAF_MALFORMED" if errors else "SIECAF_VALID_STRUCTURE"),
|
||||
"header": header,
|
||||
"tables_end": tables_end,
|
||||
"table_padding_bytes": file_offset - tables_end,
|
||||
"segments": sorted(metadata, key=lambda item: item["table_index"]),
|
||||
"section_hashes": sorted(hashes, key=lambda item: item["table_index"]),
|
||||
"normalized_segment_table_sha256": normalized_segment_table_sha256,
|
||||
"normalized_hash_blocks_sha256": normalized_hash_blocks_sha256,
|
||||
"normalized_layout_sha256": normalized_layout_sha256,
|
||||
"structural_fingerprint_sha256": structural_fingerprint_sha256,
|
||||
"duplicate_metadata_section_ids": duplicate_metadata_ids,
|
||||
"duplicate_metadata_section_keys": [
|
||||
{"section_id": section_id, "part_number": part_number}
|
||||
for section_id, part_number in duplicate_metadata_keys
|
||||
],
|
||||
"repeated_metadata_section_ids": repeated_metadata_ids,
|
||||
"duplicate_hash_section_ids": duplicate_hash_ids,
|
||||
"overlaps": overlaps,
|
||||
"gaps": gaps,
|
||||
"trailing_data_bytes": trailing_data,
|
||||
"errors": errors,
|
||||
"warnings": warnings,
|
||||
}
|
||||
|
||||
|
||||
def compare_structures(left: dict[str, Any], right: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Classify two already-parsed structural fingerprints."""
|
||||
if (
|
||||
left.get("classification") == "SIECAF_UNSUPPORTED_VERSION"
|
||||
or right.get("classification") == "SIECAF_UNSUPPORTED_VERSION"
|
||||
):
|
||||
classification = "SIECAF_UNSUPPORTED_VERSION"
|
||||
elif (
|
||||
left.get("classification") != "SIECAF_VALID_STRUCTURE"
|
||||
or right.get("classification") != "SIECAF_VALID_STRUCTURE"
|
||||
):
|
||||
classification = "SIECAF_MALFORMED"
|
||||
elif left.get("structural_fingerprint_sha256") == right.get(
|
||||
"structural_fingerprint_sha256"
|
||||
):
|
||||
classification = "SIECAF_STRUCTURAL_EXACT"
|
||||
elif left.get("normalized_layout_sha256") == right.get("normalized_layout_sha256"):
|
||||
classification = "SIECAF_LAYOUT_MATCH_HASHES_DIFFER"
|
||||
else:
|
||||
classification = "SIECAF_LAYOUT_DIFFERENT"
|
||||
return {
|
||||
"classification": classification,
|
||||
"left_structural_fingerprint_sha256": left.get("structural_fingerprint_sha256"),
|
||||
"right_structural_fingerprint_sha256": right.get(
|
||||
"structural_fingerprint_sha256"
|
||||
),
|
||||
"left_layout_sha256": left.get("normalized_layout_sha256"),
|
||||
"right_layout_sha256": right.get("normalized_layout_sha256"),
|
||||
"left_hash_blocks_sha256": left.get("normalized_hash_blocks_sha256"),
|
||||
"right_hash_blocks_sha256": right.get("normalized_hash_blocks_sha256"),
|
||||
}
|
||||
|
||||
|
||||
@contextmanager
|
||||
def open_source(
|
||||
path: Path, zip_entry: str | None
|
||||
) -> Iterator[tuple[BinaryIO, int, str]]:
|
||||
if zip_entry is None:
|
||||
with path.open("rb") as stream:
|
||||
yield stream, path.stat().st_size, str(path)
|
||||
return
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
info = archive.getinfo(zip_entry)
|
||||
with archive.open(info) as stream:
|
||||
yield stream, info.file_size, f"{path}::{zip_entry}"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("path", type=Path)
|
||||
parser.add_argument("--zip-entry")
|
||||
parser.add_argument("--label")
|
||||
parser.add_argument("--pretty", action="store_true")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
with open_source(args.path.resolve(), args.zip_entry) as (
|
||||
stream,
|
||||
size,
|
||||
default_label,
|
||||
):
|
||||
result = inspect_siecaf(
|
||||
stream, size, source_label=args.label or default_label
|
||||
)
|
||||
except (OSError, KeyError, zipfile.BadZipFile, SiecafError) as error:
|
||||
result = {
|
||||
"source": args.label or str(args.path),
|
||||
"classification": "SIECAF_MALFORMED",
|
||||
"errors": [str(error)],
|
||||
"warnings": [],
|
||||
}
|
||||
print(
|
||||
json.dumps(
|
||||
result,
|
||||
sort_keys=True,
|
||||
indent=2 if args.pretty else None,
|
||||
)
|
||||
)
|
||||
return 0 if result["classification"] == "SIECAF_VALID_STRUCTURE" else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Create deterministic, offline-only Phase-0.7 review archives."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ARCHIVE_TIMESTAMP = (2026, 7, 17, 0, 0, 0)
|
||||
ARTIFACTS = {
|
||||
"chimera-elfldr-phase07.elf": {
|
||||
"source": "outputs/phase07/artifacts/chimera-elfldr-phase07-a.elf",
|
||||
"sha256": "63e810982471eb40cae3a20aa9df9a0a02892f420e429874fae4e99aa400b561",
|
||||
"size": 397000,
|
||||
},
|
||||
"chimera-gfx-lifecycle-probe.elf": {
|
||||
"source": "outputs/phase07/artifacts/chimera-gfx-lifecycle-probe.elf",
|
||||
"sha256": "bfb4a5cc768e162fe4c2fddf41c3978e152722918a39085277fd172cb95a7182",
|
||||
"size": 112680,
|
||||
},
|
||||
"chimera-payload-manager-phase07.elf": {
|
||||
"source": "outputs/phase07/artifacts/chimera-pldmgr-phase07-a.elf",
|
||||
"sha256": "8fecf8241a46246eddbd21e8bb4d875f5d76f1f4f4c6a11384df1f131aa5e5b1",
|
||||
"size": 99560,
|
||||
},
|
||||
}
|
||||
STOCK_ELFLDR = {
|
||||
"source": "work/upstream/release-assets/elfldr-ps5-v0.23.elf",
|
||||
"sha256": "092d16ee0ede0c494947efd38d1a17bbd7cc4b022d3858ea898833c188c703e8",
|
||||
"size": 397000,
|
||||
}
|
||||
BLOCKED_SHA256 = (
|
||||
"4be1c17b4964f2b68c39b5145bc4af4619c32512d60269ecf5c39728b390fa63"
|
||||
)
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def verify(path: Path, expected: dict[str, object]) -> None:
|
||||
if not path.is_file():
|
||||
raise RuntimeError(f"missing package input: {path}")
|
||||
if path.stat().st_size != expected["size"]:
|
||||
raise RuntimeError(f"unexpected size: {path}")
|
||||
if sha256(path) != expected["sha256"]:
|
||||
raise RuntimeError(f"unexpected SHA-256: {path}")
|
||||
|
||||
|
||||
def copy(root: Path, relative_source: str, destination: Path) -> None:
|
||||
source = root / relative_source
|
||||
if not source.is_file():
|
||||
raise RuntimeError(f"missing package input: {source}")
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(source, destination)
|
||||
|
||||
|
||||
def write_json(path: Path, document: object) -> None:
|
||||
path.write_text(
|
||||
json.dumps(document, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
|
||||
def write_sums(directory: Path) -> None:
|
||||
entries = []
|
||||
for path in sorted(directory.rglob("*")):
|
||||
if path.is_file() and path.name != "SHA256SUMS.txt":
|
||||
relative = path.relative_to(directory).as_posix()
|
||||
entries.append(f"{sha256(path)} {relative}")
|
||||
(directory / "SHA256SUMS.txt").write_text(
|
||||
"\n".join(entries) + "\n", encoding="utf-8", newline="\n"
|
||||
)
|
||||
|
||||
|
||||
def deterministic_zip(source: Path, destination: Path) -> None:
|
||||
with zipfile.ZipFile(
|
||||
destination, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
|
||||
) as archive:
|
||||
for path in sorted(source.rglob("*")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
relative = (Path(source.name) / path.relative_to(source)).as_posix()
|
||||
info = zipfile.ZipInfo(relative, ARCHIVE_TIMESTAMP)
|
||||
info.compress_type = zipfile.ZIP_DEFLATED
|
||||
info.external_attr = 0o100644 << 16
|
||||
archive.writestr(info, path.read_bytes(), compresslevel=9)
|
||||
|
||||
|
||||
def clean_destination(root: Path, destination: Path) -> None:
|
||||
resolved_root = root.resolve()
|
||||
resolved = destination.resolve()
|
||||
if resolved_root not in resolved.parents:
|
||||
raise RuntimeError(f"refusing destination outside repository: {resolved}")
|
||||
if destination.exists():
|
||||
shutil.rmtree(destination)
|
||||
|
||||
|
||||
def git_head(root: Path) -> str:
|
||||
status = subprocess.run(
|
||||
["git", "status", "--porcelain", "--untracked-files=no"],
|
||||
cwd=root,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if status.stdout.strip():
|
||||
raise RuntimeError("tracked worktree must be clean before packaging")
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
cwd=root,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
root = args.root.resolve()
|
||||
head = git_head(root)
|
||||
packages = root / "outputs" / "phase07" / "packages"
|
||||
installation = packages / "phase07-installation-review"
|
||||
rollback = packages / "phase07-rollback-review"
|
||||
clean_destination(root, installation)
|
||||
clean_destination(root, rollback)
|
||||
packages.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for filename, expected in ARTIFACTS.items():
|
||||
source = root / str(expected["source"])
|
||||
verify(source, expected)
|
||||
copy(root, str(expected["source"]), installation / "artifacts" / filename)
|
||||
|
||||
review_files = [
|
||||
"docs/approvals/phase07-hardened-installation-request.md",
|
||||
"docs/approvals/phase07-lifecycle-transfer-execution-request.md",
|
||||
"docs/runtime/phase-0.7-hardening.md",
|
||||
"docs/runtime/controlled-runtime-policy.md",
|
||||
"manifests/artifact-denylist.json",
|
||||
"manifests/artifacts/chimera-elfldr-phase07-fw-9.60.json",
|
||||
"manifests/artifacts/chimera-gfx-lifecycle-probe-phase07-fw-9.60.json",
|
||||
"manifests/artifacts/chimera-payload-manager-phase07-fw-9.60.json",
|
||||
"manifests/runtime/controlled-ps5-runtime-profile.json",
|
||||
"manifests/runtime/phase-0.7-kernelwrite-proof-matrix.json",
|
||||
"manifests/runtime/phase-0.7-offline-audit.json",
|
||||
"packaging/phase07/README-installation-review.md",
|
||||
]
|
||||
for relative in review_files:
|
||||
copy(root, relative, installation / relative)
|
||||
audit_directory = root / "outputs" / "phase07" / "audit"
|
||||
if not audit_directory.is_dir():
|
||||
raise RuntimeError("full Phase-0.7 audit reports are absent")
|
||||
(installation / "audit").mkdir(parents=True, exist_ok=True)
|
||||
for report in sorted(audit_directory.glob("*")):
|
||||
if report.is_file():
|
||||
shutil.copyfile(report, installation / "audit" / report.name)
|
||||
|
||||
install_manifest = {
|
||||
"artifacts": {
|
||||
name: {
|
||||
"sha256": item["sha256"],
|
||||
"size": item["size"],
|
||||
}
|
||||
for name, item in ARTIFACTS.items()
|
||||
},
|
||||
"blocked_sha256": BLOCKED_SHA256,
|
||||
"decision": "READY_FOR_HARDENED_RUNTIME_DEPLOYMENT",
|
||||
"execution_authorized": False,
|
||||
"firmware": "9.60",
|
||||
"installation_authorized": False,
|
||||
"repository_head": head,
|
||||
"schema_version": 1,
|
||||
}
|
||||
write_json(installation / "PACKAGE-MANIFEST.json", install_manifest)
|
||||
write_sums(installation)
|
||||
|
||||
stock_source = root / str(STOCK_ELFLDR["source"])
|
||||
verify(stock_source, STOCK_ELFLDR)
|
||||
copy(
|
||||
root,
|
||||
str(STOCK_ELFLDR["source"]),
|
||||
rollback / "stock" / "elfldr-ps5-v0.23.elf",
|
||||
)
|
||||
copy(
|
||||
root,
|
||||
"packaging/phase07/README-rollback.md",
|
||||
rollback / "README-rollback.md",
|
||||
)
|
||||
rollback_manifest = {
|
||||
"existing_payload_manager_backup": {
|
||||
"available_offline": False,
|
||||
"required_before_installation": True,
|
||||
"sha256": "518740adbacccb9094fadb07dd424c53ee290f38306449ccc9d6957fdf813c0b",
|
||||
"size": 2050320,
|
||||
},
|
||||
"installation_authorized": False,
|
||||
"repository_head": head,
|
||||
"rollback_authorized": False,
|
||||
"schema_version": 1,
|
||||
"stock_elfldr": {
|
||||
"available_offline": True,
|
||||
"sha256": STOCK_ELFLDR["sha256"],
|
||||
"size": STOCK_ELFLDR["size"],
|
||||
},
|
||||
}
|
||||
write_json(rollback / "ROLLBACK-MANIFEST.json", rollback_manifest)
|
||||
write_sums(rollback)
|
||||
|
||||
installation_zip = packages / "phase07-installation-review.zip"
|
||||
rollback_zip = packages / "phase07-rollback-review.zip"
|
||||
for archive in (installation_zip, rollback_zip):
|
||||
if archive.exists():
|
||||
archive.unlink()
|
||||
deterministic_zip(installation, installation_zip)
|
||||
deterministic_zip(rollback, rollback_zip)
|
||||
package_index = {
|
||||
"archives": {
|
||||
installation_zip.name: {
|
||||
"sha256": sha256(installation_zip),
|
||||
"size": installation_zip.stat().st_size,
|
||||
},
|
||||
rollback_zip.name: {
|
||||
"sha256": sha256(rollback_zip),
|
||||
"size": rollback_zip.stat().st_size,
|
||||
},
|
||||
},
|
||||
"execution_authorized": False,
|
||||
"installation_authorized": False,
|
||||
"repository_head": head,
|
||||
"schema_version": 1,
|
||||
}
|
||||
write_json(packages / "package-index.json", package_index)
|
||||
print(json.dumps(package_index, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,287 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Offline Phase-1.0AA integration for the passive batch contract.
|
||||
|
||||
Only the exact built-in fake adapter and synthetic clock are accepted. This
|
||||
module has no live adapter protocol, network import, address, CLI or real clock.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from phase10w_shsrv_client_policy import SessionPlan
|
||||
from phase10x_inactive_transport import (
|
||||
EvidenceFailure,
|
||||
EvidenceRecord,
|
||||
ExclusiveEvidenceStore,
|
||||
)
|
||||
from phase10z_passive_batch_contract import (
|
||||
PassiveBatch,
|
||||
PassiveContractError,
|
||||
PassiveResultAccumulator,
|
||||
build_passive_batch,
|
||||
)
|
||||
|
||||
|
||||
PHASE10Z_CONTRACT_SHA256 = \
|
||||
"0728c2be7f368e0a7f4b68efe86f6e0c5c2f50704a41d0e1992b0bfec19dde06"
|
||||
MAX_FAKE_EVENTS = 257
|
||||
MAX_FAKE_ADVANCE_SECONDS = 60.0
|
||||
EVENT_DATA = "DATA"
|
||||
EVENT_HARD_DEADLINE = "HARD_DEADLINE"
|
||||
EVENT_REMOTE_EOF = "REMOTE_EOF"
|
||||
EVENT_BLOCKED = "BLOCKED"
|
||||
EVENT_KINDS = {
|
||||
EVENT_DATA, EVENT_HARD_DEADLINE, EVENT_REMOTE_EOF, EVENT_BLOCKED,
|
||||
}
|
||||
|
||||
|
||||
class OfflineIntegrationError(RuntimeError):
|
||||
"""Normalized offline failure without supplied data, target or path."""
|
||||
|
||||
|
||||
class FakeAdapterError(RuntimeError):
|
||||
"""Built-in fake-adapter state failure."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FakeReceiveEvent:
|
||||
"""One caller-supplied synthetic event; never a live receive result."""
|
||||
|
||||
kind: str
|
||||
data: bytes = b""
|
||||
advance_seconds: float = 0.0
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.kind not in EVENT_KINDS:
|
||||
raise FakeAdapterError("fake event kind is invalid")
|
||||
if not isinstance(self.data, bytes):
|
||||
raise FakeAdapterError("fake event data must be bytes")
|
||||
if self.kind == EVENT_DATA and not self.data:
|
||||
raise FakeAdapterError("fake data event is empty")
|
||||
if self.kind != EVENT_DATA and self.data:
|
||||
raise FakeAdapterError("fake control event contains data")
|
||||
value = self.advance_seconds
|
||||
if not isinstance(value, (int, float)) or isinstance(value, bool) or \
|
||||
not math.isfinite(value) or not 0.0 <= value <= \
|
||||
MAX_FAKE_ADVANCE_SECONDS:
|
||||
raise FakeAdapterError("fake time advance is invalid")
|
||||
|
||||
|
||||
class OfflineFakeClock:
|
||||
"""Explicit synthetic monotonic value; never acquires host time."""
|
||||
|
||||
def __init__(self, initial: float = 0.0) -> None:
|
||||
if not isinstance(initial, (int, float)) or isinstance(initial, bool) or \
|
||||
not math.isfinite(initial):
|
||||
raise FakeAdapterError("fake clock initial value is invalid")
|
||||
self._value = float(initial)
|
||||
|
||||
def monotonic(self) -> float:
|
||||
return self._value
|
||||
|
||||
def advance(self, seconds: float) -> None:
|
||||
if not isinstance(seconds, (int, float)) or isinstance(seconds, bool) or \
|
||||
not math.isfinite(seconds) or not 0.0 <= seconds <= \
|
||||
MAX_FAKE_ADVANCE_SECONDS:
|
||||
raise FakeAdapterError("fake clock advance is invalid")
|
||||
self._value += float(seconds)
|
||||
|
||||
|
||||
class OfflineFakeBatchAdapter:
|
||||
"""Closed fake only: one open, one exact batch and scripted events."""
|
||||
|
||||
def __init__(
|
||||
self, clock: OfflineFakeClock, events: tuple[FakeReceiveEvent, ...],
|
||||
) -> None:
|
||||
if type(clock) is not OfflineFakeClock:
|
||||
raise FakeAdapterError("only the exact fake clock is accepted")
|
||||
if not isinstance(events, tuple) or not 1 <= len(events) <= MAX_FAKE_EVENTS:
|
||||
raise FakeAdapterError("fake event sequence is invalid")
|
||||
if any(type(event) is not FakeReceiveEvent for event in events):
|
||||
raise FakeAdapterError("fake event type is invalid")
|
||||
self.clock = clock
|
||||
self._events = list(events)
|
||||
self.state = "NEW"
|
||||
self.open_count = 0
|
||||
self.send_count = 0
|
||||
self.close_count = 0
|
||||
self.sent_sha256: str | None = None
|
||||
self.trace: list[str] = []
|
||||
self.logical_event_buffer_discarded = False
|
||||
|
||||
def open_once(self) -> None:
|
||||
if self.state != "NEW":
|
||||
raise FakeAdapterError("fake adapter cannot open")
|
||||
self.open_count += 1
|
||||
self.trace.append("FAKE_OPEN")
|
||||
self.state = "OPEN"
|
||||
|
||||
def send_one_batch(self, batch: PassiveBatch) -> None:
|
||||
if self.state != "OPEN" or type(batch) is not PassiveBatch:
|
||||
raise FakeAdapterError("fake adapter cannot accept a batch")
|
||||
self.send_count += 1
|
||||
if self.send_count != 1:
|
||||
raise FakeAdapterError("second fake send is forbidden")
|
||||
self.sent_sha256 = hashlib.sha256(batch.payload).hexdigest()
|
||||
self.trace.append("FAKE_SEND_ONE_BATCH")
|
||||
self.state = "SENT"
|
||||
|
||||
def next_event(self) -> FakeReceiveEvent:
|
||||
if self.state not in {"SENT", "RECEIVING"} or not self._events:
|
||||
raise FakeAdapterError("fake adapter has no next event")
|
||||
event = self._events.pop(0)
|
||||
self.clock.advance(event.advance_seconds)
|
||||
self.trace.append(f"FAKE_EVENT_{event.kind}")
|
||||
self.state = "RECEIVING"
|
||||
return event
|
||||
|
||||
def close_once(self) -> None:
|
||||
if self.state not in {"OPEN", "SENT", "RECEIVING"}:
|
||||
raise FakeAdapterError("fake adapter cannot close")
|
||||
self.close_count += 1
|
||||
if self.close_count != 1:
|
||||
raise FakeAdapterError("second fake close is forbidden")
|
||||
self._events.clear()
|
||||
self.logical_event_buffer_discarded = True
|
||||
self.trace.append("FAKE_CLOSE")
|
||||
self.state = "CLOSED"
|
||||
|
||||
|
||||
class OfflineFakeEvidenceStore(ExclusiveEvidenceStore):
|
||||
"""Phase-specific exclusive local evidence; no raw transcript."""
|
||||
|
||||
def create_fake_consumed_receipt(
|
||||
self, plan: SessionPlan, batch: PassiveBatch, start: float,
|
||||
) -> EvidenceRecord:
|
||||
return self._create(f"{plan.run_id}.aa-consumed.json", {
|
||||
"schema_version": 1,
|
||||
"status": "OFFLINE_FAKE_ATTEMPT_CONSUMED_BEFORE_OPEN",
|
||||
"run_id": plan.run_id,
|
||||
"phase10z_contract_sha256": PHASE10Z_CONTRACT_SHA256,
|
||||
"window": batch.window,
|
||||
"batch_sha256": hashlib.sha256(batch.payload).hexdigest(),
|
||||
"batch_size": len(batch.payload),
|
||||
"deadline_seconds": batch.deadline_seconds,
|
||||
"created_fake_monotonic": start,
|
||||
"target_retained": False,
|
||||
"raw_transcript_persisted": False,
|
||||
"retry_allowed": False,
|
||||
"reconnect_allowed": False,
|
||||
"resume_allowed": False,
|
||||
"device_behavior_proven": False,
|
||||
})
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OfflineFakeOutcome:
|
||||
receipt: EvidenceRecord
|
||||
output: EvidenceRecord
|
||||
classification: str
|
||||
exact_identity: bool
|
||||
batch_sha256: str
|
||||
trace: tuple[str, ...]
|
||||
fake_only: bool = True
|
||||
device_behavior_proven: bool = False
|
||||
|
||||
|
||||
def run_offline_fake_batch(
|
||||
plan: SessionPlan,
|
||||
adapter: OfflineFakeBatchAdapter,
|
||||
clock: OfflineFakeClock,
|
||||
evidence: OfflineFakeEvidenceStore,
|
||||
) -> OfflineFakeOutcome:
|
||||
"""Exercise Z end-to-end with exact built-in fakes and local evidence."""
|
||||
if type(adapter) is not OfflineFakeBatchAdapter or \
|
||||
type(clock) is not OfflineFakeClock or \
|
||||
type(evidence) is not OfflineFakeEvidenceStore or \
|
||||
adapter.clock is not clock:
|
||||
raise OfflineIntegrationError("offline fake boundary type is invalid")
|
||||
try:
|
||||
batch = build_passive_batch(plan)
|
||||
except PassiveContractError as error:
|
||||
raise OfflineIntegrationError("passive batch preparation failed") from error
|
||||
start = clock.monotonic()
|
||||
deadline = start + batch.deadline_seconds
|
||||
try:
|
||||
receipt = evidence.create_fake_consumed_receipt(plan, batch, start)
|
||||
except EvidenceFailure as error:
|
||||
raise OfflineIntegrationError("offline receipt creation failed") from error
|
||||
trace = ["RECEIPT_CREATED"]
|
||||
accumulator = PassiveResultAccumulator(batch)
|
||||
sanitized: dict[str, Any] | None = None
|
||||
opened = False
|
||||
failure: OfflineIntegrationError | None = None
|
||||
try:
|
||||
opened = True
|
||||
adapter.open_once()
|
||||
trace.extend(adapter.trace[-1:])
|
||||
if clock.monotonic() >= deadline:
|
||||
raise OfflineIntegrationError("deadline reached before fake send")
|
||||
adapter.send_one_batch(batch)
|
||||
trace.extend(adapter.trace[-1:])
|
||||
for _index in range(MAX_FAKE_EVENTS):
|
||||
event = adapter.next_event()
|
||||
trace.extend(adapter.trace[-1:])
|
||||
now = clock.monotonic()
|
||||
if event.kind == EVENT_DATA:
|
||||
if now >= deadline:
|
||||
raise OfflineIntegrationError("fake data reached deadline")
|
||||
accumulator.feed_supplied_chunk(event.data)
|
||||
continue
|
||||
if event.kind == EVENT_HARD_DEADLINE:
|
||||
if now < deadline:
|
||||
raise OfflineIntegrationError("fake deadline arrived early")
|
||||
sanitized = accumulator.seal_at_hard_deadline(True)
|
||||
break
|
||||
if event.kind == EVENT_REMOTE_EOF:
|
||||
raise OfflineIntegrationError("remote EOF is not completion")
|
||||
if event.kind == EVENT_BLOCKED:
|
||||
raise OfflineIntegrationError("blocked fake receive is invalid")
|
||||
if sanitized is None:
|
||||
raise OfflineIntegrationError("hard deadline result is missing")
|
||||
except Exception as error: # noqa: BLE001 - exact fake boundary normalization
|
||||
failure = OfflineIntegrationError("offline fake integration failed")
|
||||
failure.__cause__ = error
|
||||
finally:
|
||||
if opened:
|
||||
try:
|
||||
adapter.close_once()
|
||||
trace.extend(adapter.trace[-1:])
|
||||
except FakeAdapterError as error:
|
||||
if failure is None:
|
||||
failure = OfflineIntegrationError("offline fake close failed")
|
||||
failure.__cause__ = error
|
||||
if failure is not None:
|
||||
raise failure
|
||||
if sanitized is None or adapter.sent_sha256 is None:
|
||||
raise OfflineIntegrationError("offline fake result is incomplete")
|
||||
sanitized["phase10aa_fake_integration"] = {
|
||||
"offline_fake_only": True,
|
||||
"exact_builtin_adapter_required": True,
|
||||
"exact_builtin_clock_required": True,
|
||||
"receipt_created_before_fake_open": True,
|
||||
"one_fake_open": adapter.open_count == 1,
|
||||
"one_fake_batch_send": adapter.send_count == 1,
|
||||
"one_fake_close": adapter.close_count == 1,
|
||||
"logical_event_buffer_discarded": adapter.logical_event_buffer_discarded,
|
||||
"physical_memory_erasure_proven": False,
|
||||
"network_transport_present": False,
|
||||
"device_behavior_proven": False,
|
||||
}
|
||||
try:
|
||||
output = evidence.create_sanitized_output(plan, receipt, sanitized)
|
||||
except EvidenceFailure as error:
|
||||
raise OfflineIntegrationError("offline sanitized output failed") from error
|
||||
return OfflineFakeOutcome(
|
||||
receipt=receipt,
|
||||
output=output,
|
||||
classification=str(sanitized["classification"]),
|
||||
exact_identity=bool(sanitized["exact_identity"]),
|
||||
batch_sha256=adapter.sent_sha256,
|
||||
trace=tuple(trace),
|
||||
)
|
||||
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Pure offline trace model for a future nonblocking host adapter.
|
||||
|
||||
No socket, selector, address, DNS, OS call, real clock, CLI or file output is
|
||||
present. The model only validates caller-supplied synthetic operation traces.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
|
||||
|
||||
MAX_TRACE_EVENTS = 512
|
||||
MAX_BATCH_BYTES = 1035
|
||||
MAX_RECEIVE_BYTES = 65_536
|
||||
MAX_DEADLINE_SECONDS = 10.0
|
||||
|
||||
RECEIPT_CREATED = "RECEIPT_CREATED"
|
||||
SOCKET_CREATED = "SOCKET_CREATED"
|
||||
SET_NONBLOCKING = "SET_NONBLOCKING"
|
||||
CONNECT_IMMEDIATE = "CONNECT_IMMEDIATE"
|
||||
CONNECT_PENDING = "CONNECT_PENDING"
|
||||
READY_WRITE = "READY_WRITE"
|
||||
SO_ERROR_ZERO = "SO_ERROR_ZERO"
|
||||
SEND_BYTES = "SEND_BYTES"
|
||||
READY_READ = "READY_READ"
|
||||
RECV_BYTES = "RECV_BYTES"
|
||||
RECV_EOF = "RECV_EOF"
|
||||
WAIT_INTERRUPTED = "WAIT_INTERRUPTED"
|
||||
WAIT_TIMEOUT = "WAIT_TIMEOUT"
|
||||
DEADLINE_REACHED = "DEADLINE_REACHED"
|
||||
SANITIZER_ACCEPTED = "SANITIZER_ACCEPTED"
|
||||
LOCAL_CLOSE = "LOCAL_CLOSE"
|
||||
OUTPUT_CREATED = "OUTPUT_CREATED"
|
||||
|
||||
OPERATIONS = {
|
||||
RECEIPT_CREATED, SOCKET_CREATED, SET_NONBLOCKING, CONNECT_IMMEDIATE,
|
||||
CONNECT_PENDING, READY_WRITE, SO_ERROR_ZERO, SEND_BYTES, READY_READ,
|
||||
RECV_BYTES, RECV_EOF, WAIT_INTERRUPTED, WAIT_TIMEOUT, DEADLINE_REACHED,
|
||||
SANITIZER_ACCEPTED, LOCAL_CLOSE, OUTPUT_CREATED,
|
||||
}
|
||||
|
||||
|
||||
class TraceModelError(RuntimeError):
|
||||
"""Fail-closed synthetic trace error."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TraceEvent:
|
||||
operation: str
|
||||
at_seconds: float
|
||||
value: int = 0
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.operation not in OPERATIONS:
|
||||
raise TraceModelError("trace operation is invalid")
|
||||
if not isinstance(self.at_seconds, (int, float)) or isinstance(
|
||||
self.at_seconds, bool) or not math.isfinite(self.at_seconds) or \
|
||||
self.at_seconds < 0:
|
||||
raise TraceModelError("trace time is invalid")
|
||||
if not isinstance(self.value, int) or isinstance(self.value, bool) or \
|
||||
self.value < 0:
|
||||
raise TraceModelError("trace value is invalid")
|
||||
if self.operation not in {SEND_BYTES, RECV_BYTES} and self.value != 0:
|
||||
raise TraceModelError("valueless trace operation has a value")
|
||||
if self.operation in {SEND_BYTES, RECV_BYTES} and self.value == 0:
|
||||
raise TraceModelError("byte operation has no progress")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TraceAssessment:
|
||||
classification: str
|
||||
batch_bytes_sent: int
|
||||
receive_bytes: int
|
||||
receipt_before_create: bool
|
||||
nonblocking_before_connect: bool
|
||||
complete_send_loop: bool
|
||||
deadline_only_completion: bool
|
||||
local_close_observed: bool
|
||||
exact_identity_proven: bool = False
|
||||
device_behavior_proven: bool = False
|
||||
live_transport_present: bool = False
|
||||
|
||||
|
||||
def assess_nonblocking_trace(
|
||||
batch_size: int, deadline_seconds: float, events: tuple[TraceEvent, ...],
|
||||
) -> TraceAssessment:
|
||||
"""Validate one exact synthetic lifecycle; raise on every ambiguity."""
|
||||
if not isinstance(batch_size, int) or isinstance(batch_size, bool) or \
|
||||
not 1 <= batch_size <= MAX_BATCH_BYTES:
|
||||
raise TraceModelError("batch size is invalid")
|
||||
if not isinstance(deadline_seconds, (int, float)) or isinstance(
|
||||
deadline_seconds, bool) or not math.isfinite(deadline_seconds) or \
|
||||
not 0 < deadline_seconds <= MAX_DEADLINE_SECONDS:
|
||||
raise TraceModelError("deadline is invalid")
|
||||
if not isinstance(events, tuple) or not 1 <= len(events) <= MAX_TRACE_EVENTS or \
|
||||
any(type(event) is not TraceEvent for event in events):
|
||||
raise TraceModelError("trace shape is invalid")
|
||||
|
||||
state = "START"
|
||||
last_time = -1.0
|
||||
sent = 0
|
||||
received = 0
|
||||
write_ready = False
|
||||
read_ready = False
|
||||
connected = False
|
||||
deadline_seen = False
|
||||
sanitizer_seen = False
|
||||
close_seen = False
|
||||
|
||||
for event in events:
|
||||
if event.at_seconds < last_time:
|
||||
raise TraceModelError("trace time moved backward")
|
||||
last_time = float(event.at_seconds)
|
||||
operation = event.operation
|
||||
|
||||
if deadline_seen and operation not in {
|
||||
SANITIZER_ACCEPTED, LOCAL_CLOSE, OUTPUT_CREATED}:
|
||||
raise TraceModelError("I/O occurred after deadline")
|
||||
if not deadline_seen and event.at_seconds >= deadline_seconds and \
|
||||
operation != DEADLINE_REACHED:
|
||||
raise TraceModelError("nondeadline operation reached deadline")
|
||||
|
||||
if operation == RECEIPT_CREATED:
|
||||
if state != "START":
|
||||
raise TraceModelError("receipt ordering is invalid")
|
||||
state = "RECEIPT"
|
||||
elif operation == SOCKET_CREATED:
|
||||
if state != "RECEIPT":
|
||||
raise TraceModelError("socket creation precedes receipt")
|
||||
state = "CREATED"
|
||||
elif operation == SET_NONBLOCKING:
|
||||
if state != "CREATED":
|
||||
raise TraceModelError("nonblocking setup ordering is invalid")
|
||||
state = "NONBLOCKING"
|
||||
elif operation == CONNECT_IMMEDIATE:
|
||||
if state != "NONBLOCKING":
|
||||
raise TraceModelError("immediate connect ordering is invalid")
|
||||
connected = True
|
||||
state = "CONNECTED"
|
||||
elif operation == CONNECT_PENDING:
|
||||
if state != "NONBLOCKING":
|
||||
raise TraceModelError("pending connect ordering is invalid")
|
||||
state = "CONNECT_PENDING"
|
||||
elif operation == READY_WRITE:
|
||||
if state == "CONNECT_PENDING":
|
||||
state = "CONNECT_READY"
|
||||
elif connected and not deadline_seen:
|
||||
write_ready = True
|
||||
else:
|
||||
raise TraceModelError("write readiness is unexpected")
|
||||
elif operation == SO_ERROR_ZERO:
|
||||
if state != "CONNECT_READY":
|
||||
raise TraceModelError("SO_ERROR ordering is invalid")
|
||||
connected = True
|
||||
state = "CONNECTED"
|
||||
elif operation == SEND_BYTES:
|
||||
if not connected or not write_ready or sent >= batch_size:
|
||||
raise TraceModelError("send ordering is invalid")
|
||||
if event.value > batch_size - sent:
|
||||
raise TraceModelError("send exceeded exact batch")
|
||||
sent += event.value
|
||||
write_ready = False
|
||||
elif operation == READY_READ:
|
||||
if not connected or sent != batch_size or deadline_seen:
|
||||
raise TraceModelError("read readiness is unexpected")
|
||||
read_ready = True
|
||||
elif operation == RECV_BYTES:
|
||||
if not read_ready:
|
||||
raise TraceModelError("receive occurred without readiness")
|
||||
if received + event.value > MAX_RECEIVE_BYTES:
|
||||
raise TraceModelError("receive bound exceeded")
|
||||
received += event.value
|
||||
read_ready = False
|
||||
elif operation == RECV_EOF:
|
||||
raise TraceModelError("EOF is not a completion event")
|
||||
elif operation in {WAIT_INTERRUPTED, WAIT_TIMEOUT}:
|
||||
if not connected or deadline_seen:
|
||||
raise TraceModelError("wait event ordering is invalid")
|
||||
write_ready = False
|
||||
read_ready = False
|
||||
elif operation == DEADLINE_REACHED:
|
||||
if event.at_seconds < deadline_seconds or not connected or \
|
||||
sent != batch_size or received == 0:
|
||||
raise TraceModelError("deadline preconditions are incomplete")
|
||||
deadline_seen = True
|
||||
write_ready = False
|
||||
read_ready = False
|
||||
elif operation == SANITIZER_ACCEPTED:
|
||||
if not deadline_seen or sanitizer_seen:
|
||||
raise TraceModelError("sanitizer ordering is invalid")
|
||||
sanitizer_seen = True
|
||||
elif operation == LOCAL_CLOSE:
|
||||
if not deadline_seen or not sanitizer_seen or close_seen:
|
||||
raise TraceModelError("local close ordering is invalid")
|
||||
close_seen = True
|
||||
elif operation == OUTPUT_CREATED:
|
||||
if not close_seen or state == "COMPLETE":
|
||||
raise TraceModelError("output ordering is invalid")
|
||||
state = "COMPLETE"
|
||||
else:
|
||||
raise TraceModelError("unhandled trace operation")
|
||||
|
||||
if state != "COMPLETE" or not deadline_seen or not sanitizer_seen or \
|
||||
not close_seen or sent != batch_size:
|
||||
raise TraceModelError("trace is incomplete")
|
||||
return TraceAssessment(
|
||||
classification="OFFLINE_NONBLOCKING_SEQUENCE_FEASIBLE",
|
||||
batch_bytes_sent=sent,
|
||||
receive_bytes=received,
|
||||
receipt_before_create=True,
|
||||
nonblocking_before_connect=True,
|
||||
complete_send_loop=True,
|
||||
deadline_only_completion=True,
|
||||
local_close_observed=True,
|
||||
)
|
||||
@@ -0,0 +1,348 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Dormant Phase-1.0AC adapter driven only by exact built-in fake syscalls.
|
||||
|
||||
The module has no socket, selector, address, DNS, CLI, real clock or file
|
||||
output. It exercises one nonblocking lifecycle with synthetic outcomes and
|
||||
the existing passive batch/result contract.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
|
||||
from phase10z_passive_batch_contract import (
|
||||
PassiveBatch,
|
||||
PassiveContractError,
|
||||
PassiveResultAccumulator,
|
||||
)
|
||||
|
||||
|
||||
MAX_FAKE_STEPS = 1024
|
||||
MAX_STEP_ADVANCE_SECONDS = 60.0
|
||||
MAX_RECEIVE_BYTES = 65536
|
||||
|
||||
CREATE_STREAM = "CREATE_STREAM"
|
||||
SET_NONBLOCKING = "SET_NONBLOCKING"
|
||||
START_CONNECT = "START_CONNECT"
|
||||
WAIT_WRITE = "WAIT_WRITE"
|
||||
GET_SO_ERROR = "GET_SO_ERROR"
|
||||
WRITE_BYTES = "WRITE_BYTES"
|
||||
WAIT_READ = "WAIT_READ"
|
||||
READ_BYTES = "READ_BYTES"
|
||||
|
||||
OK = "OK"
|
||||
IMMEDIATE = "IMMEDIATE"
|
||||
PENDING = "PENDING"
|
||||
READY = "READY"
|
||||
INTERRUPTED = "INTERRUPTED"
|
||||
TIMEOUT = "TIMEOUT"
|
||||
ZERO = "ZERO"
|
||||
NONZERO = "NONZERO"
|
||||
PROGRESS = "PROGRESS"
|
||||
EOF = "EOF"
|
||||
ERROR = "ERROR"
|
||||
|
||||
ALLOWED_RESULTS = {
|
||||
CREATE_STREAM: {OK, ERROR},
|
||||
SET_NONBLOCKING: {OK, ERROR},
|
||||
START_CONNECT: {IMMEDIATE, PENDING, ERROR},
|
||||
WAIT_WRITE: {READY, INTERRUPTED, TIMEOUT, ERROR},
|
||||
GET_SO_ERROR: {ZERO, NONZERO, ERROR},
|
||||
WRITE_BYTES: {PROGRESS, ZERO, ERROR},
|
||||
WAIT_READ: {READY, INTERRUPTED, TIMEOUT, ERROR},
|
||||
READ_BYTES: {PROGRESS, EOF, ERROR},
|
||||
}
|
||||
|
||||
|
||||
class DormantAdapterError(RuntimeError):
|
||||
"""Normalized synthetic adapter failure without supplied bytes."""
|
||||
|
||||
|
||||
class FakeSyscallError(RuntimeError):
|
||||
"""Exact fake-facade state or script failure."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FakeSyscallStep:
|
||||
"""One synthetic syscall outcome; never an operating-system call."""
|
||||
|
||||
operation: str
|
||||
result: str
|
||||
value: int = 0
|
||||
data: bytes = b""
|
||||
advance_seconds: float = 0.0
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.operation not in ALLOWED_RESULTS or \
|
||||
self.result not in ALLOWED_RESULTS[self.operation]:
|
||||
raise FakeSyscallError("fake syscall operation/result is invalid")
|
||||
if not isinstance(self.value, int) or isinstance(self.value, bool) or \
|
||||
self.value < 0:
|
||||
raise FakeSyscallError("fake syscall value is invalid")
|
||||
if not isinstance(self.data, bytes):
|
||||
raise FakeSyscallError("fake syscall data is invalid")
|
||||
advance = self.advance_seconds
|
||||
if not isinstance(advance, (int, float)) or isinstance(advance, bool) or \
|
||||
not math.isfinite(advance) or not 0.0 <= advance <= \
|
||||
MAX_STEP_ADVANCE_SECONDS:
|
||||
raise FakeSyscallError("fake syscall time advance is invalid")
|
||||
if self.operation == WRITE_BYTES and self.result == PROGRESS:
|
||||
if self.value <= 0 or self.data:
|
||||
raise FakeSyscallError("fake write progress is invalid")
|
||||
elif self.operation == READ_BYTES and self.result == PROGRESS:
|
||||
if not self.data or self.value != len(self.data):
|
||||
raise FakeSyscallError("fake read progress is invalid")
|
||||
elif self.value != 0 or self.data:
|
||||
raise FakeSyscallError("fake syscall carries unexpected data")
|
||||
|
||||
|
||||
class OfflineFakeClock:
|
||||
"""Explicit synthetic monotonic value; never reads the host clock."""
|
||||
|
||||
def __init__(self, initial: float = 0.0) -> None:
|
||||
if not isinstance(initial, (int, float)) or isinstance(initial, bool) or \
|
||||
not math.isfinite(initial):
|
||||
raise FakeSyscallError("fake clock initial value is invalid")
|
||||
self._value = float(initial)
|
||||
|
||||
def monotonic(self) -> float:
|
||||
return self._value
|
||||
|
||||
def advance(self, seconds: float) -> None:
|
||||
if not isinstance(seconds, (int, float)) or isinstance(seconds, bool) or \
|
||||
not math.isfinite(seconds) or not 0.0 <= seconds <= \
|
||||
MAX_STEP_ADVANCE_SECONDS:
|
||||
raise FakeSyscallError("fake clock advance is invalid")
|
||||
self._value += float(seconds)
|
||||
|
||||
|
||||
class OfflineFakeSyscallFacade:
|
||||
"""Closed fake facade; it cannot retain a target or create a capability."""
|
||||
|
||||
def __init__(
|
||||
self, clock: OfflineFakeClock, steps: tuple[FakeSyscallStep, ...],
|
||||
close_result: str = OK,
|
||||
) -> None:
|
||||
if type(clock) is not OfflineFakeClock:
|
||||
raise FakeSyscallError("only the exact fake clock is accepted")
|
||||
if not isinstance(steps, tuple) or not 1 <= len(steps) <= MAX_FAKE_STEPS \
|
||||
or any(type(step) is not FakeSyscallStep for step in steps):
|
||||
raise FakeSyscallError("fake syscall script is invalid")
|
||||
if close_result not in {OK, ERROR}:
|
||||
raise FakeSyscallError("fake close result is invalid")
|
||||
self.clock = clock
|
||||
self._steps = list(steps)
|
||||
self._close_result = close_result
|
||||
self.state = "NEW"
|
||||
self.invoke_count = 0
|
||||
self.close_count = 0
|
||||
self.discarded_steps = 0
|
||||
self.trace: list[str] = []
|
||||
|
||||
def invoke(self, operation: str) -> FakeSyscallStep:
|
||||
if self.state == "CLOSED" or not self._steps:
|
||||
raise FakeSyscallError("fake syscall script is exhausted")
|
||||
step = self._steps.pop(0)
|
||||
if step.operation != operation:
|
||||
raise FakeSyscallError("fake syscall ordering is invalid")
|
||||
self.clock.advance(step.advance_seconds)
|
||||
self.invoke_count += 1
|
||||
self.trace.append(f"{operation}:{step.result}")
|
||||
self.state = "ACTIVE"
|
||||
return step
|
||||
|
||||
def close_once(self) -> None:
|
||||
if self.state == "CLOSED" or self.close_count != 0:
|
||||
raise FakeSyscallError("fake facade cannot close")
|
||||
self.close_count = 1
|
||||
self.discarded_steps = len(self._steps)
|
||||
self._steps.clear()
|
||||
self.trace.append(f"LOCAL_CLOSE:{self._close_result}")
|
||||
self.state = "CLOSED"
|
||||
if self._close_result != OK:
|
||||
raise FakeSyscallError("synthetic local close failed")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DormantAdapterOutcome:
|
||||
classification: str
|
||||
result_classification: str
|
||||
exact_identity: bool
|
||||
batch_bytes_sent: int
|
||||
received_bytes: int
|
||||
write_calls: int
|
||||
read_calls: int
|
||||
interrupted_waits: int
|
||||
timed_out_waits: int
|
||||
discarded_steps_after_close: int
|
||||
trace: tuple[str, ...]
|
||||
target_retained: bool = False
|
||||
live_transport_present: bool = False
|
||||
device_behavior_proven: bool = False
|
||||
|
||||
|
||||
def _before_deadline(clock: OfflineFakeClock, deadline: float) -> None:
|
||||
if clock.monotonic() >= deadline:
|
||||
raise DormantAdapterError("synthetic deadline reached")
|
||||
|
||||
|
||||
def _wait_until_ready(
|
||||
facade: OfflineFakeSyscallFacade,
|
||||
clock: OfflineFakeClock,
|
||||
deadline: float,
|
||||
operation: str,
|
||||
) -> tuple[bool, int, int]:
|
||||
interrupted = 0
|
||||
timed_out = 0
|
||||
while True:
|
||||
_before_deadline(clock, deadline)
|
||||
step = facade.invoke(operation)
|
||||
if clock.monotonic() >= deadline:
|
||||
if step.result == READY:
|
||||
raise DormantAdapterError(
|
||||
"synthetic deadline won readiness race")
|
||||
if step.result == INTERRUPTED:
|
||||
return False, interrupted + 1, timed_out
|
||||
if step.result == TIMEOUT:
|
||||
return False, interrupted, timed_out + 1
|
||||
raise DormantAdapterError("synthetic wait failed at deadline")
|
||||
if step.result == READY:
|
||||
return True, interrupted, timed_out
|
||||
if step.result == INTERRUPTED:
|
||||
interrupted += 1
|
||||
continue
|
||||
if step.result == TIMEOUT:
|
||||
timed_out += 1
|
||||
continue
|
||||
raise DormantAdapterError("synthetic readiness failed")
|
||||
|
||||
|
||||
def run_dormant_adapter(
|
||||
batch: PassiveBatch,
|
||||
facade: OfflineFakeSyscallFacade,
|
||||
clock: OfflineFakeClock,
|
||||
receipt_precommitted: bool,
|
||||
) -> DormantAdapterOutcome:
|
||||
"""Run one target-free fake-syscall lifecycle and normalize all failure."""
|
||||
if type(batch) is not PassiveBatch or \
|
||||
type(facade) is not OfflineFakeSyscallFacade or \
|
||||
type(clock) is not OfflineFakeClock or facade.clock is not clock or \
|
||||
receipt_precommitted is not True:
|
||||
raise DormantAdapterError("dormant adapter boundary is invalid")
|
||||
|
||||
start = clock.monotonic()
|
||||
deadline = start + batch.deadline_seconds
|
||||
accumulator: PassiveResultAccumulator
|
||||
try:
|
||||
accumulator = PassiveResultAccumulator(batch)
|
||||
except PassiveContractError as error:
|
||||
raise DormantAdapterError("passive batch is invalid") from error
|
||||
|
||||
sent = 0
|
||||
received = 0
|
||||
write_calls = 0
|
||||
read_calls = 0
|
||||
interrupted_waits = 0
|
||||
timed_out_waits = 0
|
||||
opened = False
|
||||
sanitized = None
|
||||
primary_failure: DormantAdapterError | None = None
|
||||
|
||||
try:
|
||||
_before_deadline(clock, deadline)
|
||||
if facade.invoke(CREATE_STREAM).result != OK:
|
||||
raise DormantAdapterError("synthetic stream creation failed")
|
||||
opened = True
|
||||
_before_deadline(clock, deadline)
|
||||
if facade.invoke(SET_NONBLOCKING).result != OK:
|
||||
raise DormantAdapterError("synthetic nonblocking setup failed")
|
||||
_before_deadline(clock, deadline)
|
||||
connect_result = facade.invoke(START_CONNECT).result
|
||||
if connect_result == PENDING:
|
||||
ready, interrupted, timed_out = _wait_until_ready(
|
||||
facade, clock, deadline, WAIT_WRITE)
|
||||
interrupted_waits += interrupted
|
||||
timed_out_waits += timed_out
|
||||
if not ready:
|
||||
raise DormantAdapterError(
|
||||
"synthetic connect reached deadline")
|
||||
if facade.invoke(GET_SO_ERROR).result != ZERO:
|
||||
raise DormantAdapterError("synthetic pending connect failed")
|
||||
_before_deadline(clock, deadline)
|
||||
elif connect_result != IMMEDIATE:
|
||||
raise DormantAdapterError("synthetic immediate connect failed")
|
||||
|
||||
while sent < len(batch.payload):
|
||||
ready, interrupted, timed_out = _wait_until_ready(
|
||||
facade, clock, deadline, WAIT_WRITE)
|
||||
interrupted_waits += interrupted
|
||||
timed_out_waits += timed_out
|
||||
if not ready:
|
||||
raise DormantAdapterError("synthetic write reached deadline")
|
||||
step = facade.invoke(WRITE_BYTES)
|
||||
write_calls += 1
|
||||
if step.result != PROGRESS or step.value > len(batch.payload) - sent:
|
||||
raise DormantAdapterError("synthetic write made invalid progress")
|
||||
sent += step.value
|
||||
_before_deadline(clock, deadline)
|
||||
|
||||
while clock.monotonic() < deadline:
|
||||
ready, interrupted, timed_out = _wait_until_ready(
|
||||
facade, clock, deadline, WAIT_READ)
|
||||
interrupted_waits += interrupted
|
||||
timed_out_waits += timed_out
|
||||
if not ready:
|
||||
break
|
||||
step = facade.invoke(READ_BYTES)
|
||||
read_calls += 1
|
||||
if step.result == EOF:
|
||||
raise DormantAdapterError("synthetic EOF is not completion")
|
||||
if step.result != PROGRESS or received + len(step.data) > \
|
||||
MAX_RECEIVE_BYTES:
|
||||
raise DormantAdapterError("synthetic read is invalid")
|
||||
if clock.monotonic() >= deadline:
|
||||
raise DormantAdapterError("synthetic data reached deadline")
|
||||
try:
|
||||
accumulator.feed_supplied_chunk(step.data)
|
||||
except PassiveContractError as error:
|
||||
raise DormantAdapterError("passive result rejected input") from error
|
||||
received += len(step.data)
|
||||
|
||||
if clock.monotonic() < deadline or received == 0:
|
||||
raise DormantAdapterError("synthetic deadline result is incomplete")
|
||||
try:
|
||||
sanitized = accumulator.seal_at_hard_deadline(True)
|
||||
except PassiveContractError as error:
|
||||
raise DormantAdapterError("passive deadline result is invalid") from error
|
||||
except Exception as error: # noqa: BLE001 - exact fake boundary normalization
|
||||
primary_failure = DormantAdapterError("dormant fake-syscall run failed")
|
||||
primary_failure.__cause__ = error
|
||||
finally:
|
||||
if opened:
|
||||
try:
|
||||
facade.close_once()
|
||||
except FakeSyscallError as error:
|
||||
if primary_failure is None:
|
||||
primary_failure = DormantAdapterError(
|
||||
"dormant fake-syscall cleanup failed")
|
||||
primary_failure.__cause__ = error
|
||||
|
||||
if primary_failure is not None:
|
||||
raise primary_failure
|
||||
if sanitized is None or facade.close_count != 1:
|
||||
raise DormantAdapterError("dormant fake-syscall result is incomplete")
|
||||
return DormantAdapterOutcome(
|
||||
classification="OFFLINE_DORMANT_FAKE_SYSCALL_ADAPTER_COMPLETE",
|
||||
result_classification=str(sanitized["classification"]),
|
||||
exact_identity=bool(sanitized["exact_identity"]),
|
||||
batch_bytes_sent=sent,
|
||||
received_bytes=received,
|
||||
write_calls=write_calls,
|
||||
read_calls=read_calls,
|
||||
interrupted_waits=interrupted_waits,
|
||||
timed_out_waits=timed_out_waits,
|
||||
discarded_steps_after_close=facade.discarded_steps,
|
||||
trace=("CONSUMED_RECEIPT_PREEXISTS", *facade.trace),
|
||||
)
|
||||
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Offline Phase-1.0AD activation-record contract.
|
||||
|
||||
This module validates data only. It has no socket, DNS, clock, CLI, file
|
||||
output, transport or device capability.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
import ipaddress
|
||||
import re
|
||||
|
||||
|
||||
PHASE = "PHASE_1_0AD_INACTIVE_NUMERIC_TARGET_CONTRACT"
|
||||
PORT = 2323
|
||||
MAX_WINDOW_SECONDS = 300
|
||||
RUN_ID = re.compile(r"^[A-Z0-9][A-Z0-9_-]{7,63}$")
|
||||
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
||||
|
||||
|
||||
class ActivationContractError(ValueError):
|
||||
"""An activation record is incomplete, ambiguous or unsafe."""
|
||||
|
||||
|
||||
def _utc(value: str) -> datetime:
|
||||
if not isinstance(value, str) or not value.endswith("Z"):
|
||||
raise ActivationContractError("timestamp must be UTC with Z suffix")
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value[:-1] + "+00:00")
|
||||
except ValueError as error:
|
||||
raise ActivationContractError("timestamp is invalid") from error
|
||||
if parsed.tzinfo != timezone.utc:
|
||||
raise ActivationContractError("timestamp is not UTC")
|
||||
return parsed
|
||||
|
||||
|
||||
def validate_numeric_target(value: str) -> str:
|
||||
"""Return canonical private IPv4 text without resolving a name."""
|
||||
if not isinstance(value, str) or not value or value != value.strip():
|
||||
raise ActivationContractError("target must be exact numeric text")
|
||||
if ":" in value or any(character.isalpha() for character in value):
|
||||
raise ActivationContractError("only numeric IPv4 targets are allowed")
|
||||
try:
|
||||
address = ipaddress.IPv4Address(value)
|
||||
except ipaddress.AddressValueError as error:
|
||||
raise ActivationContractError("target is not canonical IPv4") from error
|
||||
if str(address) != value or not address.is_private or address.is_loopback \
|
||||
or address.is_link_local or address.is_multicast \
|
||||
or address.is_unspecified:
|
||||
raise ActivationContractError("target is not canonical private IPv4")
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ActivationRecord:
|
||||
phase: str
|
||||
active: bool
|
||||
target_address: str | None
|
||||
target_port: int | None
|
||||
run_id: str | None
|
||||
not_before: str | None
|
||||
expires_at: str | None
|
||||
launcher_sha256: str | None
|
||||
payload_sha256: str | None
|
||||
approval_sha256: str | None
|
||||
one_shot: bool
|
||||
automatic_retry: bool
|
||||
reconnect: bool
|
||||
resume: bool
|
||||
device_write_authorized: bool
|
||||
app_termination_authorized: bool
|
||||
system_remount_authorized: bool
|
||||
|
||||
|
||||
def validate_inactive(record: ActivationRecord) -> None:
|
||||
"""Validate the only tracked Phase-1.0AD state: entirely inactive."""
|
||||
if type(record) is not ActivationRecord or record.phase != PHASE:
|
||||
raise ActivationContractError("activation phase is invalid")
|
||||
if record.active is not False:
|
||||
raise ActivationContractError("tracked activation must be inactive")
|
||||
optional = (
|
||||
record.target_address, record.target_port, record.run_id,
|
||||
record.not_before, record.expires_at, record.launcher_sha256,
|
||||
record.payload_sha256, record.approval_sha256,
|
||||
)
|
||||
if any(value is not None for value in optional):
|
||||
raise ActivationContractError("inactive activation must be target-free")
|
||||
if record.one_shot is not True or record.automatic_retry is not False \
|
||||
or record.reconnect is not False or record.resume is not False:
|
||||
raise ActivationContractError("inactive one-shot policy is invalid")
|
||||
if record.device_write_authorized or record.app_termination_authorized \
|
||||
or record.system_remount_authorized:
|
||||
raise ActivationContractError("inactive device effects must be false")
|
||||
|
||||
|
||||
def validate_candidate(record: ActivationRecord) -> None:
|
||||
"""Validate hypothetical activation data without activating anything."""
|
||||
if type(record) is not ActivationRecord or record.phase != PHASE \
|
||||
or record.active is not True:
|
||||
raise ActivationContractError("candidate is not explicitly active")
|
||||
validate_numeric_target(record.target_address) # type: ignore[arg-type]
|
||||
if record.target_port != PORT:
|
||||
raise ActivationContractError("candidate port is not source-bound")
|
||||
if not isinstance(record.run_id, str) or not RUN_ID.fullmatch(record.run_id):
|
||||
raise ActivationContractError("candidate run id is invalid")
|
||||
if not isinstance(record.launcher_sha256, str) \
|
||||
or not SHA256.fullmatch(record.launcher_sha256):
|
||||
raise ActivationContractError("launcher identity is invalid")
|
||||
if not isinstance(record.payload_sha256, str) \
|
||||
or not SHA256.fullmatch(record.payload_sha256):
|
||||
raise ActivationContractError("payload identity is invalid")
|
||||
if not isinstance(record.approval_sha256, str) \
|
||||
or not SHA256.fullmatch(record.approval_sha256):
|
||||
raise ActivationContractError("approval identity is invalid")
|
||||
start = _utc(record.not_before) # type: ignore[arg-type]
|
||||
end = _utc(record.expires_at) # type: ignore[arg-type]
|
||||
duration = (end - start).total_seconds()
|
||||
if not 0 < duration <= MAX_WINDOW_SECONDS:
|
||||
raise ActivationContractError("candidate window is invalid")
|
||||
if record.one_shot is not True or record.automatic_retry is not False \
|
||||
or record.reconnect is not False or record.resume is not False:
|
||||
raise ActivationContractError("candidate one-shot policy is invalid")
|
||||
if record.device_write_authorized or record.app_termination_authorized \
|
||||
or record.system_remount_authorized:
|
||||
raise ActivationContractError("candidate requests forbidden effects")
|
||||
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Closed host-only model of one bounded BigApp launcher lifecycle.
|
||||
|
||||
The model accepts only exact built-in fake operations. It imports no target
|
||||
headers, socket, process, syscall, clock, CLI or filesystem interface.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
FIXED_TITLE = "PPSA01659"
|
||||
MAX_MAIN_TICKS = 64
|
||||
MAX_FAKE_EVENTS = 64
|
||||
MAX_CLEANUP_EVENTS = 3
|
||||
|
||||
CHECK_NO_BIGAPP = "CHECK_NO_BIGAPP"
|
||||
ATTACH_PARENT = "ATTACH_PARENT"
|
||||
ARM_FORK = "ARM_FORK"
|
||||
CONTINUE_PARENT = "CONTINUE_PARENT"
|
||||
LAUNCH_FIXED_TITLE = "LAUNCH_FIXED_TITLE"
|
||||
AWAIT_UNIQUE_CHILD = "AWAIT_UNIQUE_CHILD"
|
||||
DETACH_PARENT = "DETACH_PARENT"
|
||||
ARM_EXEC = "ARM_EXEC"
|
||||
CONTINUE_CHILD = "CONTINUE_CHILD"
|
||||
AWAIT_EXEC = "AWAIT_EXEC"
|
||||
REPLACE_EXACT_PAYLOAD = "REPLACE_EXACT_PAYLOAD"
|
||||
RESTORE_MUTATIONS = "RESTORE_MUTATIONS"
|
||||
DETACH_CHILD = "DETACH_CHILD"
|
||||
TERMINATE_CHILD = "TERMINATE_CHILD"
|
||||
EMIT_RESULT = "EMIT_RESULT"
|
||||
|
||||
OK = "OK"
|
||||
NONE = "NONE"
|
||||
EXISTS = "EXISTS"
|
||||
CHILD = "CHILD"
|
||||
TIMEOUT = "TIMEOUT"
|
||||
ERROR = "ERROR"
|
||||
|
||||
ALLOWED = {
|
||||
CHECK_NO_BIGAPP: {NONE, EXISTS, ERROR},
|
||||
ATTACH_PARENT: {OK, ERROR}, ARM_FORK: {OK, ERROR},
|
||||
CONTINUE_PARENT: {OK, ERROR}, LAUNCH_FIXED_TITLE: {OK, ERROR},
|
||||
AWAIT_UNIQUE_CHILD: {CHILD, TIMEOUT, ERROR},
|
||||
DETACH_PARENT: {OK, ERROR}, ARM_EXEC: {OK, ERROR},
|
||||
CONTINUE_CHILD: {OK, ERROR}, AWAIT_EXEC: {OK, TIMEOUT, ERROR},
|
||||
REPLACE_EXACT_PAYLOAD: {OK, ERROR}, RESTORE_MUTATIONS: {OK, ERROR},
|
||||
DETACH_CHILD: {OK, ERROR}, TERMINATE_CHILD: {OK, ERROR},
|
||||
EMIT_RESULT: {OK, ERROR},
|
||||
}
|
||||
|
||||
|
||||
class LifecycleModelError(RuntimeError):
|
||||
"""The synthetic lifecycle or its cleanup is invalid."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FakeEvent:
|
||||
operation: str
|
||||
result: str
|
||||
ticks: int = 1
|
||||
child_id: int = 0
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.operation not in ALLOWED or self.result not in ALLOWED[self.operation]:
|
||||
raise LifecycleModelError("fake operation/result is invalid")
|
||||
if not isinstance(self.ticks, int) or isinstance(self.ticks, bool) \
|
||||
or not 1 <= self.ticks <= MAX_MAIN_TICKS:
|
||||
raise LifecycleModelError("fake ticks are invalid")
|
||||
if self.operation == AWAIT_UNIQUE_CHILD and self.result == CHILD:
|
||||
if not isinstance(self.child_id, int) or isinstance(self.child_id, bool) \
|
||||
or self.child_id <= 1:
|
||||
raise LifecycleModelError("fake child identity is invalid")
|
||||
elif self.child_id != 0:
|
||||
raise LifecycleModelError("unexpected fake child identity")
|
||||
|
||||
|
||||
class FakeLifecycleFacade:
|
||||
"""Exact closed script; it cannot execute an operation itself."""
|
||||
|
||||
def __init__(self, events: tuple[FakeEvent, ...]) -> None:
|
||||
if not isinstance(events, tuple) or not 1 <= len(events) <= MAX_FAKE_EVENTS \
|
||||
or any(type(event) is not FakeEvent for event in events):
|
||||
raise LifecycleModelError("fake lifecycle script is invalid")
|
||||
self._events = list(events)
|
||||
self.trace: list[str] = []
|
||||
self.ticks = 0
|
||||
|
||||
def invoke(self, operation: str, cleanup: bool = False) -> FakeEvent:
|
||||
if not self._events:
|
||||
raise LifecycleModelError("fake lifecycle script is exhausted")
|
||||
event = self._events.pop(0)
|
||||
if event.operation != operation:
|
||||
raise LifecycleModelError("fake lifecycle ordering is invalid")
|
||||
self.ticks += event.ticks
|
||||
self.trace.append(f"{operation}:{event.result}")
|
||||
if not cleanup and self.ticks > MAX_MAIN_TICKS:
|
||||
raise LifecycleModelError("bounded main lifecycle expired")
|
||||
return event
|
||||
|
||||
@property
|
||||
def remaining(self) -> int:
|
||||
return len(self._events)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LifecycleOutcome:
|
||||
classification: str
|
||||
success: bool
|
||||
child_id: int
|
||||
parent_detached: bool
|
||||
child_detached: bool
|
||||
child_terminated: bool
|
||||
mutations_restored: bool
|
||||
existing_bigapp_killed: bool
|
||||
fixed_title: str
|
||||
trace: tuple[str, ...]
|
||||
target_code_present: bool = False
|
||||
device_behavior_proven: bool = False
|
||||
|
||||
|
||||
def _require_ok(facade: FakeLifecycleFacade, operation: str) -> None:
|
||||
if facade.invoke(operation).result != OK:
|
||||
raise LifecycleModelError(f"{operation} failed")
|
||||
|
||||
|
||||
def run_lifecycle(facade: FakeLifecycleFacade) -> LifecycleOutcome:
|
||||
"""Model one attempt and exhaustively unwind every acquired state."""
|
||||
if type(facade) is not FakeLifecycleFacade:
|
||||
raise LifecycleModelError("only the exact fake facade is accepted")
|
||||
|
||||
parent_attached = False
|
||||
parent_detached = False
|
||||
child_id = 0
|
||||
child_detached = False
|
||||
child_terminated = False
|
||||
mutations_started = False
|
||||
mutations_restored = False
|
||||
success = False
|
||||
primary_error: Exception | None = None
|
||||
cleanup_events = 0
|
||||
|
||||
try:
|
||||
if facade.invoke(CHECK_NO_BIGAPP).result != NONE:
|
||||
raise LifecycleModelError("pre-existing BigApp blocks launch")
|
||||
_require_ok(facade, ATTACH_PARENT)
|
||||
parent_attached = True
|
||||
_require_ok(facade, ARM_FORK)
|
||||
_require_ok(facade, CONTINUE_PARENT)
|
||||
_require_ok(facade, LAUNCH_FIXED_TITLE)
|
||||
child = facade.invoke(AWAIT_UNIQUE_CHILD)
|
||||
if child.result != CHILD:
|
||||
raise LifecycleModelError("unique child was not observed")
|
||||
child_id = child.child_id
|
||||
_require_ok(facade, DETACH_PARENT)
|
||||
parent_attached = False
|
||||
parent_detached = True
|
||||
_require_ok(facade, ARM_EXEC)
|
||||
_require_ok(facade, CONTINUE_CHILD)
|
||||
_require_ok(facade, AWAIT_EXEC)
|
||||
replace = facade.invoke(REPLACE_EXACT_PAYLOAD)
|
||||
mutations_started = True
|
||||
if replace.result != OK:
|
||||
raise LifecycleModelError("exact payload replacement failed")
|
||||
_require_ok(facade, RESTORE_MUTATIONS)
|
||||
mutations_started = False
|
||||
mutations_restored = True
|
||||
_require_ok(facade, DETACH_CHILD)
|
||||
child_detached = True
|
||||
_require_ok(facade, EMIT_RESULT)
|
||||
success = True
|
||||
except Exception as error: # exact fake boundary normalization
|
||||
primary_error = error
|
||||
finally:
|
||||
try:
|
||||
if parent_attached:
|
||||
cleanup_events += 1
|
||||
if facade.invoke(DETACH_PARENT, cleanup=True).result != OK:
|
||||
raise LifecycleModelError("parent cleanup failed")
|
||||
parent_attached = False
|
||||
parent_detached = True
|
||||
if mutations_started:
|
||||
cleanup_events += 1
|
||||
if facade.invoke(RESTORE_MUTATIONS, cleanup=True).result != OK:
|
||||
raise LifecycleModelError("mutation restoration failed")
|
||||
mutations_started = False
|
||||
mutations_restored = True
|
||||
if child_id and not child_detached:
|
||||
cleanup_events += 1
|
||||
if facade.invoke(TERMINATE_CHILD, cleanup=True).result != OK:
|
||||
raise LifecycleModelError("new child cleanup failed")
|
||||
child_terminated = True
|
||||
if cleanup_events > MAX_CLEANUP_EVENTS:
|
||||
raise LifecycleModelError("cleanup operation bound exceeded")
|
||||
except Exception as cleanup_error:
|
||||
raise LifecycleModelError("lifecycle cleanup is incomplete") from cleanup_error
|
||||
|
||||
if facade.remaining:
|
||||
raise LifecycleModelError("fake lifecycle has unused operations")
|
||||
if primary_error is not None:
|
||||
return LifecycleOutcome(
|
||||
"OFFLINE_BIGAPP_LIFECYCLE_FAILED_CLEANLY", False, child_id,
|
||||
parent_detached, child_detached, child_terminated,
|
||||
mutations_restored, False, FIXED_TITLE, tuple(facade.trace))
|
||||
if not success or not parent_detached or not child_detached \
|
||||
or not mutations_restored or child_terminated:
|
||||
raise LifecycleModelError("successful lifecycle invariants failed")
|
||||
return LifecycleOutcome(
|
||||
"OFFLINE_BIGAPP_LIFECYCLE_MODEL_COMPLETE", True, child_id,
|
||||
parent_detached, child_detached, child_terminated,
|
||||
mutations_restored, False, FIXED_TITLE, tuple(facade.trace))
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Bytes-only bounded ELF64 admission contract for a future launcher.
|
||||
|
||||
No path, file, process, target, network or execution interface is present.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import re
|
||||
import struct
|
||||
|
||||
|
||||
MAX_FILE_SIZE = 2 * 1024 * 1024
|
||||
MAX_PROGRAM_HEADERS = 32
|
||||
MAX_SECTION_HEADERS = 256
|
||||
MAX_LOAD_SEGMENTS = 8
|
||||
MAX_TOTAL_LOAD_MEMORY = 64 * 1024 * 1024
|
||||
MAX_LOAD_SPAN = 128 * 1024 * 1024
|
||||
MAX_ALIGNMENT = 2 * 1024 * 1024
|
||||
|
||||
ELF_HEADER = struct.Struct("<16sHHIQQQIHHHHHH")
|
||||
PROGRAM_HEADER = struct.Struct("<IIQQQQQQ")
|
||||
SECTION_HEADER_SIZE = 64
|
||||
PT_LOAD = 1
|
||||
PT_INTERP = 3
|
||||
ET_DYN = 3
|
||||
EM_X86_64 = 62
|
||||
PF_X = 1
|
||||
PF_W = 2
|
||||
PF_R = 4
|
||||
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
||||
|
||||
|
||||
class ElfContractError(ValueError):
|
||||
"""The supplied bytes are not an admitted bounded payload."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LoadSegment:
|
||||
flags: int
|
||||
offset: int
|
||||
virtual_address: int
|
||||
file_size: int
|
||||
memory_size: int
|
||||
alignment: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ElfAssessment:
|
||||
size: int
|
||||
sha256: str
|
||||
entry: int
|
||||
program_header_count: int
|
||||
load_segments: tuple[LoadSegment, ...]
|
||||
total_load_memory: int
|
||||
load_span: int
|
||||
pie: bool = True
|
||||
machine_x86_64: bool = True
|
||||
writable_executable_segment: bool = False
|
||||
interpreter_present: bool = False
|
||||
execution_performed: bool = False
|
||||
|
||||
|
||||
def _bounded_range(offset: int, size: int, limit: int, label: str) -> None:
|
||||
if offset < 0 or size < 0 or offset > limit or size > limit - offset:
|
||||
raise ElfContractError(f"{label} range is outside supplied bytes")
|
||||
|
||||
|
||||
def _power_of_two(value: int) -> bool:
|
||||
return value > 0 and value & (value - 1) == 0
|
||||
|
||||
|
||||
def assess_elf(payload: bytes, expected_sha256: str) -> ElfAssessment:
|
||||
"""Validate exact supplied bytes without opening or executing anything."""
|
||||
if not isinstance(payload, bytes) or not ELF_HEADER.size <= len(payload) <= MAX_FILE_SIZE:
|
||||
raise ElfContractError("payload size is outside the admission bound")
|
||||
if not isinstance(expected_sha256, str) or not SHA256.fullmatch(expected_sha256):
|
||||
raise ElfContractError("expected SHA-256 is invalid")
|
||||
digest = hashlib.sha256(payload).hexdigest()
|
||||
if digest != expected_sha256:
|
||||
raise ElfContractError("payload SHA-256 does not match")
|
||||
|
||||
(ident, elf_type, machine, version, entry, phoff, shoff, flags,
|
||||
ehsize, phentsize, phnum, shentsize, shnum, shstrndx) = ELF_HEADER.unpack_from(payload)
|
||||
del flags, shstrndx
|
||||
if ident[:4] != b"\x7fELF" or ident[4] != 2 or ident[5] != 1 \
|
||||
or ident[6] != 1 or ident[7] not in {0, 9}:
|
||||
raise ElfContractError("ELF identity is unsupported")
|
||||
if elf_type != ET_DYN or machine != EM_X86_64 or version != 1:
|
||||
raise ElfContractError("ELF type, machine or version is unsupported")
|
||||
if ehsize != ELF_HEADER.size or phentsize != PROGRAM_HEADER.size \
|
||||
or not 1 <= phnum <= MAX_PROGRAM_HEADERS:
|
||||
raise ElfContractError("ELF header sizing is invalid")
|
||||
if phoff < ELF_HEADER.size:
|
||||
raise ElfContractError("program header table overlaps ELF header")
|
||||
_bounded_range(phoff, phnum * phentsize, len(payload), "program header table")
|
||||
if shnum == 0:
|
||||
if shoff != 0 or shentsize not in {0, SECTION_HEADER_SIZE}:
|
||||
raise ElfContractError("absent section table is inconsistent")
|
||||
else:
|
||||
if not 1 <= shnum <= MAX_SECTION_HEADERS or shentsize != SECTION_HEADER_SIZE:
|
||||
raise ElfContractError("section header sizing is invalid")
|
||||
_bounded_range(shoff, shnum * shentsize, len(payload), "section header table")
|
||||
|
||||
loads: list[LoadSegment] = []
|
||||
interpreter = False
|
||||
for index in range(phnum):
|
||||
values = PROGRAM_HEADER.unpack_from(payload, phoff + index * phentsize)
|
||||
p_type, p_flags, p_offset, p_vaddr, _p_paddr, p_filesz, p_memsz, p_align = values
|
||||
if p_type == PT_INTERP:
|
||||
interpreter = True
|
||||
if p_type != PT_LOAD:
|
||||
continue
|
||||
if len(loads) >= MAX_LOAD_SEGMENTS or p_memsz == 0 or p_filesz > p_memsz:
|
||||
raise ElfContractError("load segment count or sizing is invalid")
|
||||
_bounded_range(p_offset, p_filesz, len(payload), "load segment file")
|
||||
if p_vaddr > (1 << 64) - 1 - p_memsz:
|
||||
raise ElfContractError("load segment address overflows")
|
||||
if p_flags & ~(PF_R | PF_W | PF_X) or p_flags & PF_W and p_flags & PF_X:
|
||||
raise ElfContractError("load segment permissions are invalid")
|
||||
if not _power_of_two(p_align) or p_align > MAX_ALIGNMENT \
|
||||
or p_offset % p_align != p_vaddr % p_align:
|
||||
raise ElfContractError("load segment alignment is invalid")
|
||||
loads.append(LoadSegment(p_flags, p_offset, p_vaddr, p_filesz,
|
||||
p_memsz, p_align))
|
||||
if interpreter:
|
||||
raise ElfContractError("interpreter segment is forbidden")
|
||||
if not loads:
|
||||
raise ElfContractError("ELF has no load segments")
|
||||
|
||||
ordered = sorted(loads, key=lambda item: item.virtual_address)
|
||||
for previous, current in zip(ordered, ordered[1:]):
|
||||
if previous.virtual_address + previous.memory_size > current.virtual_address:
|
||||
raise ElfContractError("load segment virtual ranges overlap")
|
||||
total_memory = sum(item.memory_size for item in ordered)
|
||||
span = ordered[-1].virtual_address + ordered[-1].memory_size - ordered[0].virtual_address
|
||||
if total_memory > MAX_TOTAL_LOAD_MEMORY or span > MAX_LOAD_SPAN:
|
||||
raise ElfContractError("load memory budget is exceeded")
|
||||
executable = [item for item in ordered if item.flags & PF_X]
|
||||
if not executable or not any(item.virtual_address <= entry <
|
||||
item.virtual_address + item.memory_size
|
||||
for item in executable):
|
||||
raise ElfContractError("entry is not in an executable load segment")
|
||||
return ElfAssessment(len(payload), digest, entry, phnum, tuple(ordered),
|
||||
total_memory, span)
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Bytes-only dynamic/relocation contract layered on Phase 1.0AG."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import struct
|
||||
|
||||
from phase10ag_bounded_elf import (
|
||||
ELF_HEADER, ElfContractError, PF_W, assess_elf,
|
||||
)
|
||||
|
||||
|
||||
SECTION_HEADER = struct.Struct("<IIQQQQIIQQ")
|
||||
DYNAMIC_ENTRY = struct.Struct("<qQ")
|
||||
RELA_ENTRY = struct.Struct("<QQq")
|
||||
SHT_STRTAB = 3
|
||||
SHT_RELA = 4
|
||||
SHT_DYNAMIC = 6
|
||||
DT_NULL = 0
|
||||
DT_NEEDED = 1
|
||||
R_X86_64_GLOB_DAT = 6
|
||||
R_X86_64_RELATIVE = 8
|
||||
MAX_DYNAMIC_ENTRIES = 256
|
||||
MAX_RELOCATION_SECTIONS = 4
|
||||
MAX_RELOCATIONS = 4096
|
||||
MAX_NEEDED = 16
|
||||
MAX_NEEDED_NAME = 128
|
||||
|
||||
ALLOWED_MODULES = frozenset({
|
||||
"libSceAudioOut.sprx", "libSceLibcInternal.sprx", "libScePad.sprx",
|
||||
"libSceSystemService.sprx", "libSceUserService.sprx",
|
||||
"libSceVideoOut.sprx", "libkernel_web.sprx",
|
||||
})
|
||||
|
||||
|
||||
class DynamicContractError(ValueError):
|
||||
"""Dynamic metadata exceeds or violates the admitted subset."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DynamicAssessment:
|
||||
needed: tuple[str, ...]
|
||||
relative_count: int
|
||||
glob_dat_count: int
|
||||
total_relocations: int
|
||||
relocation_sections: int
|
||||
loader_applied_types: tuple[int, ...] = (R_X86_64_RELATIVE,)
|
||||
crt_applied_types: tuple[int, ...] = (R_X86_64_GLOB_DAT,)
|
||||
unknown_relocation_types: tuple[int, ...] = ()
|
||||
target_mapping_performed: bool = False
|
||||
runtime_module_loading_performed: bool = False
|
||||
|
||||
|
||||
def _range(offset: int, size: int, limit: int, label: str) -> None:
|
||||
if offset < 0 or size < 0 or offset > limit or size > limit - offset:
|
||||
raise DynamicContractError(f"{label} is outside supplied bytes")
|
||||
|
||||
|
||||
def _sections(payload: bytes) -> list[tuple[int, ...]]:
|
||||
header = ELF_HEADER.unpack_from(payload)
|
||||
shoff, shentsize, shnum = header[6], header[11], header[12]
|
||||
if shnum == 0 or shentsize != SECTION_HEADER.size:
|
||||
raise DynamicContractError("section table is required")
|
||||
_range(shoff, shnum * shentsize, len(payload), "section table")
|
||||
return [SECTION_HEADER.unpack_from(payload, shoff + index * shentsize)
|
||||
for index in range(shnum)]
|
||||
|
||||
|
||||
def _needed(payload: bytes, sections: list[tuple[int, ...]]) -> tuple[str, ...]:
|
||||
dynamic_sections = [section for section in sections if section[1] == SHT_DYNAMIC]
|
||||
if len(dynamic_sections) != 1:
|
||||
raise DynamicContractError("exactly one dynamic section is required")
|
||||
section = dynamic_sections[0]
|
||||
offset, size, link, entsize = section[4], section[5], section[6], section[9]
|
||||
if entsize != DYNAMIC_ENTRY.size or size % entsize or size // entsize > MAX_DYNAMIC_ENTRIES:
|
||||
raise DynamicContractError("dynamic table sizing is invalid")
|
||||
_range(offset, size, len(payload), "dynamic table")
|
||||
if link >= len(sections) or sections[link][1] != SHT_STRTAB:
|
||||
raise DynamicContractError("dynamic string table link is invalid")
|
||||
strings = sections[link]
|
||||
string_offset, string_size = strings[4], strings[5]
|
||||
_range(string_offset, string_size, len(payload), "dynamic string table")
|
||||
string_data = payload[string_offset:string_offset + string_size]
|
||||
names: list[str] = []
|
||||
terminated = False
|
||||
for position in range(offset, offset + size, entsize):
|
||||
tag, value = DYNAMIC_ENTRY.unpack_from(payload, position)
|
||||
if terminated:
|
||||
if tag != DT_NULL or value != 0:
|
||||
raise DynamicContractError("nonzero dynamic data follows DT_NULL")
|
||||
continue
|
||||
if tag == DT_NULL:
|
||||
terminated = True
|
||||
continue
|
||||
if tag != DT_NEEDED:
|
||||
continue
|
||||
if len(names) >= MAX_NEEDED or value >= len(string_data):
|
||||
raise DynamicContractError("DT_NEEDED count or offset is invalid")
|
||||
end = string_data.find(b"\0", value, min(len(string_data), value + MAX_NEEDED_NAME + 1))
|
||||
if end < 0:
|
||||
raise DynamicContractError("DT_NEEDED name is unterminated or too long")
|
||||
try:
|
||||
name = string_data[value:end].decode("ascii")
|
||||
except UnicodeDecodeError as error:
|
||||
raise DynamicContractError("DT_NEEDED name is not ASCII") from error
|
||||
if name not in ALLOWED_MODULES or name in names:
|
||||
raise DynamicContractError("DT_NEEDED module is unknown or duplicated")
|
||||
names.append(name)
|
||||
if not terminated:
|
||||
raise DynamicContractError("dynamic table has no DT_NULL terminator")
|
||||
return tuple(names)
|
||||
|
||||
|
||||
def assess_dynamic(payload: bytes, expected_sha256: str,
|
||||
expected_needed: tuple[str, ...]) -> DynamicAssessment:
|
||||
"""Validate exact dynamic dependencies and the bounded relocation split."""
|
||||
try:
|
||||
elf = assess_elf(payload, expected_sha256)
|
||||
except ElfContractError as error:
|
||||
raise DynamicContractError("base ELF admission failed") from error
|
||||
if not isinstance(expected_needed, tuple) or not 1 <= len(expected_needed) <= MAX_NEEDED \
|
||||
or any(not isinstance(item, str) for item in expected_needed) \
|
||||
or len(set(expected_needed)) != len(expected_needed) \
|
||||
or any(item not in ALLOWED_MODULES for item in expected_needed):
|
||||
raise DynamicContractError("expected module inventory is invalid")
|
||||
sections = _sections(payload)
|
||||
needed = _needed(payload, sections)
|
||||
if needed != expected_needed:
|
||||
raise DynamicContractError("DT_NEEDED inventory or order differs")
|
||||
|
||||
writable = [(item.virtual_address, item.virtual_address + item.memory_size)
|
||||
for item in elf.load_segments if item.flags & PF_W]
|
||||
image = [(item.virtual_address, item.virtual_address + item.memory_size)
|
||||
for item in elf.load_segments]
|
||||
relative = 0
|
||||
glob_dat = 0
|
||||
total = 0
|
||||
relocation_sections = 0
|
||||
for section in sections:
|
||||
if section[1] != SHT_RELA:
|
||||
continue
|
||||
relocation_sections += 1
|
||||
if relocation_sections > MAX_RELOCATION_SECTIONS:
|
||||
raise DynamicContractError("too many relocation sections")
|
||||
offset, size, entsize = section[4], section[5], section[9]
|
||||
if entsize != RELA_ENTRY.size or size % entsize:
|
||||
raise DynamicContractError("relocation table sizing is invalid")
|
||||
_range(offset, size, len(payload), "relocation table")
|
||||
for position in range(offset, offset + size, entsize):
|
||||
total += 1
|
||||
if total > MAX_RELOCATIONS:
|
||||
raise DynamicContractError("relocation count is exceeded")
|
||||
target, info, addend = RELA_ENTRY.unpack_from(payload, position)
|
||||
relocation_type = info & 0xffffffff
|
||||
symbol = info >> 32
|
||||
if target % 8 or not any(start <= target and target + 8 <= end
|
||||
for start, end in writable):
|
||||
raise DynamicContractError("relocation target is not aligned RW memory")
|
||||
if relocation_type == R_X86_64_RELATIVE:
|
||||
if symbol != 0 or addend < 0 or not any(start <= addend < end
|
||||
for start, end in image):
|
||||
raise DynamicContractError("relative relocation is invalid")
|
||||
relative += 1
|
||||
elif relocation_type == R_X86_64_GLOB_DAT:
|
||||
if symbol == 0 or addend != 0:
|
||||
raise DynamicContractError("GLOB_DAT relocation is invalid")
|
||||
glob_dat += 1
|
||||
else:
|
||||
raise DynamicContractError("relocation type is outside the contract")
|
||||
if relocation_sections == 0 or relative == 0:
|
||||
raise DynamicContractError("relative relocation closure is absent")
|
||||
return DynamicAssessment(needed, relative, glob_dat, total,
|
||||
relocation_sections)
|
||||
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Host-only transactional mapping/protection model for admitted ELF loads."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from phase10ag_bounded_elf import LoadSegment, PF_R, PF_W, PF_X
|
||||
|
||||
|
||||
PAGE_SIZE = 16 * 1024
|
||||
MAX_SEGMENTS = 8
|
||||
MAX_RELATIVE_RELOCATIONS = 4096
|
||||
MAX_EVENTS = 64
|
||||
MAX_MAIN_TICKS = 128
|
||||
MAX_REGION_SIZE = 128 * 1024 * 1024
|
||||
|
||||
RESERVE_CHILD = "RESERVE_CHILD"
|
||||
CREATE_MIRROR = "CREATE_MIRROR"
|
||||
COPY_FILE_BYTES = "COPY_FILE_BYTES"
|
||||
ZERO_BSS = "ZERO_BSS"
|
||||
APPLY_RELATIVE = "APPLY_RELATIVE"
|
||||
COPY_MIRROR_TO_CHILD = "COPY_MIRROR_TO_CHILD"
|
||||
SET_FINAL_PROTECTION = "SET_FINAL_PROTECTION"
|
||||
SYNC_IMAGE = "SYNC_IMAGE"
|
||||
RELEASE_MIRROR = "RELEASE_MIRROR"
|
||||
UNMAP_CHILD = "UNMAP_CHILD"
|
||||
OK = "OK"
|
||||
ERROR = "ERROR"
|
||||
|
||||
OPERATIONS = {
|
||||
RESERVE_CHILD, CREATE_MIRROR, COPY_FILE_BYTES, ZERO_BSS,
|
||||
APPLY_RELATIVE, COPY_MIRROR_TO_CHILD, SET_FINAL_PROTECTION,
|
||||
SYNC_IMAGE, RELEASE_MIRROR, UNMAP_CHILD,
|
||||
}
|
||||
|
||||
|
||||
class MappingModelError(RuntimeError):
|
||||
"""The mapping transaction or its rollback is incomplete."""
|
||||
|
||||
|
||||
def _round_down(value: int) -> int:
|
||||
return value & ~(PAGE_SIZE - 1)
|
||||
|
||||
|
||||
def _round_up(value: int) -> int:
|
||||
if value > (1 << 64) - PAGE_SIZE:
|
||||
raise MappingModelError("mapping address cannot be rounded")
|
||||
return (value + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MappingPlan:
|
||||
segments: tuple[LoadSegment, ...]
|
||||
relative_relocations: int
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not isinstance(self.segments, tuple) or not 1 <= len(self.segments) <= MAX_SEGMENTS \
|
||||
or any(type(item) is not LoadSegment for item in self.segments):
|
||||
raise MappingModelError("mapping segments are invalid")
|
||||
if tuple(sorted(self.segments, key=lambda item: item.virtual_address)) != self.segments:
|
||||
raise MappingModelError("mapping segments are not ordered")
|
||||
if not isinstance(self.relative_relocations, int) \
|
||||
or isinstance(self.relative_relocations, bool) \
|
||||
or not 1 <= self.relative_relocations <= MAX_RELATIVE_RELOCATIONS:
|
||||
raise MappingModelError("relative relocation count is invalid")
|
||||
protected: list[tuple[int, int]] = []
|
||||
for segment in self.segments:
|
||||
if segment.memory_size <= 0 or segment.file_size < 0 \
|
||||
or segment.file_size > segment.memory_size \
|
||||
or segment.flags & ~(PF_R | PF_W | PF_X) \
|
||||
or segment.flags & PF_W and segment.flags & PF_X:
|
||||
raise MappingModelError("mapping segment is unsafe")
|
||||
start = _round_down(segment.virtual_address)
|
||||
end = _round_up(segment.virtual_address + segment.memory_size)
|
||||
if protected and protected[-1][1] > start:
|
||||
raise MappingModelError("page-rounded protections overlap")
|
||||
protected.append((start, end))
|
||||
if self.region_size > MAX_REGION_SIZE:
|
||||
raise MappingModelError("mapping region exceeds budget")
|
||||
|
||||
@property
|
||||
def region_start(self) -> int:
|
||||
return _round_down(self.segments[0].virtual_address)
|
||||
|
||||
@property
|
||||
def region_size(self) -> int:
|
||||
end = _round_up(self.segments[-1].virtual_address +
|
||||
self.segments[-1].memory_size)
|
||||
return end - self.region_start
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FakeMappingEvent:
|
||||
operation: str
|
||||
result: str
|
||||
segment_index: int = -1
|
||||
value: int = 0
|
||||
ticks: int = 1
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.operation not in OPERATIONS or self.result not in {OK, ERROR}:
|
||||
raise MappingModelError("fake mapping operation/result is invalid")
|
||||
if not isinstance(self.segment_index, int) or isinstance(self.segment_index, bool) \
|
||||
or self.segment_index < -1:
|
||||
raise MappingModelError("fake segment index is invalid")
|
||||
if not isinstance(self.value, int) or isinstance(self.value, bool) or self.value < 0:
|
||||
raise MappingModelError("fake mapping value is invalid")
|
||||
if not isinstance(self.ticks, int) or isinstance(self.ticks, bool) \
|
||||
or not 1 <= self.ticks <= MAX_MAIN_TICKS:
|
||||
raise MappingModelError("fake mapping ticks are invalid")
|
||||
indexed = self.operation in {COPY_FILE_BYTES, ZERO_BSS, SET_FINAL_PROTECTION}
|
||||
if indexed != (self.segment_index >= 0):
|
||||
raise MappingModelError("fake mapping segment binding is invalid")
|
||||
|
||||
|
||||
class FakeMappingFacade:
|
||||
"""Exact scripted facade with no memory or process capability."""
|
||||
|
||||
def __init__(self, events: tuple[FakeMappingEvent, ...]) -> None:
|
||||
if not isinstance(events, tuple) or not 1 <= len(events) <= MAX_EVENTS \
|
||||
or any(type(event) is not FakeMappingEvent for event in events):
|
||||
raise MappingModelError("fake mapping script is invalid")
|
||||
self._events = list(events)
|
||||
self.trace: list[str] = []
|
||||
self.ticks = 0
|
||||
|
||||
def invoke(self, operation: str, segment_index: int = -1,
|
||||
value: int = 0, cleanup: bool = False) -> FakeMappingEvent:
|
||||
if not self._events:
|
||||
raise MappingModelError("fake mapping script is exhausted")
|
||||
event = self._events.pop(0)
|
||||
if event.operation != operation or event.segment_index != segment_index \
|
||||
or event.value != value:
|
||||
raise MappingModelError("fake mapping ordering or binding differs")
|
||||
if not cleanup and self.ticks + event.ticks > MAX_MAIN_TICKS:
|
||||
self.trace.append(f"DEADLINE_BEFORE:{operation}")
|
||||
raise MappingModelError("mapping main tick budget expired before operation")
|
||||
self.ticks += event.ticks
|
||||
self.trace.append(f"{operation}:{segment_index}:{value}:{event.result}")
|
||||
return event
|
||||
|
||||
@property
|
||||
def remaining(self) -> int:
|
||||
return len(self._events)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MappingOutcome:
|
||||
classification: str
|
||||
success: bool
|
||||
region_size: int
|
||||
copied_file_bytes: int
|
||||
zeroed_bss_bytes: int
|
||||
relative_relocations: int
|
||||
final_protections: tuple[int, ...]
|
||||
mirror_released: bool
|
||||
child_region_retained: bool
|
||||
child_region_unmapped: bool
|
||||
target_mapping_performed: bool = False
|
||||
device_behavior_proven: bool = False
|
||||
|
||||
|
||||
def _ok(facade: FakeMappingFacade, operation: str, segment_index: int = -1,
|
||||
value: int = 0) -> None:
|
||||
if facade.invoke(operation, segment_index, value).result != OK:
|
||||
raise MappingModelError(f"{operation} failed")
|
||||
|
||||
|
||||
def run_mapping(plan: MappingPlan, facade: FakeMappingFacade) -> MappingOutcome:
|
||||
"""Run one synthetic mapping transaction with full-region rollback."""
|
||||
if type(plan) is not MappingPlan or type(facade) is not FakeMappingFacade:
|
||||
raise MappingModelError("mapping model boundary is invalid")
|
||||
child_reserved = False
|
||||
mirror_active = False
|
||||
mirror_released = False
|
||||
child_unmapped = False
|
||||
committed = False
|
||||
copied = 0
|
||||
zeroed = 0
|
||||
protections: list[int] = []
|
||||
primary_error: Exception | None = None
|
||||
|
||||
try:
|
||||
_ok(facade, RESERVE_CHILD, value=plan.region_size)
|
||||
child_reserved = True
|
||||
_ok(facade, CREATE_MIRROR, value=plan.region_size)
|
||||
mirror_active = True
|
||||
for index, segment in enumerate(plan.segments):
|
||||
if segment.file_size:
|
||||
_ok(facade, COPY_FILE_BYTES, index, segment.file_size)
|
||||
copied += segment.file_size
|
||||
bss = segment.memory_size - segment.file_size
|
||||
if bss:
|
||||
_ok(facade, ZERO_BSS, index, bss)
|
||||
zeroed += bss
|
||||
_ok(facade, APPLY_RELATIVE, value=plan.relative_relocations)
|
||||
_ok(facade, COPY_MIRROR_TO_CHILD, value=plan.region_size)
|
||||
for index, segment in enumerate(plan.segments):
|
||||
_ok(facade, SET_FINAL_PROTECTION, index, segment.flags)
|
||||
protections.append(segment.flags)
|
||||
_ok(facade, SYNC_IMAGE, value=plan.region_size)
|
||||
_ok(facade, RELEASE_MIRROR)
|
||||
mirror_active = False
|
||||
mirror_released = True
|
||||
committed = True
|
||||
except Exception as error:
|
||||
primary_error = error
|
||||
finally:
|
||||
try:
|
||||
if mirror_active:
|
||||
if facade.invoke(RELEASE_MIRROR, cleanup=True).result != OK:
|
||||
raise MappingModelError("mirror cleanup failed")
|
||||
mirror_active = False
|
||||
mirror_released = True
|
||||
if child_reserved and not committed:
|
||||
if facade.invoke(UNMAP_CHILD, value=plan.region_size,
|
||||
cleanup=True).result != OK:
|
||||
raise MappingModelError("child mapping rollback failed")
|
||||
child_unmapped = True
|
||||
child_reserved = False
|
||||
except Exception as cleanup_error:
|
||||
raise MappingModelError("mapping rollback is incomplete") from cleanup_error
|
||||
|
||||
if facade.remaining:
|
||||
raise MappingModelError("fake mapping script has unused operations")
|
||||
if primary_error is not None:
|
||||
return MappingOutcome(
|
||||
"OFFLINE_MAPPING_TRANSACTION_ROLLED_BACK", False, plan.region_size,
|
||||
copied, zeroed, plan.relative_relocations, tuple(protections),
|
||||
mirror_released, False, child_unmapped)
|
||||
if not committed or not mirror_released or child_unmapped \
|
||||
or tuple(protections) != tuple(item.flags for item in plan.segments):
|
||||
raise MappingModelError("successful mapping invariants failed")
|
||||
return MappingOutcome(
|
||||
"OFFLINE_MAPPING_TRANSACTION_MODEL_COMPLETE", True, plan.region_size,
|
||||
copied, zeroed, plan.relative_relocations, tuple(protections), True,
|
||||
True, False)
|
||||
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Host-only ownership model for hybrid BigApp/JIT mapping composition."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
MAX_EXECUTABLE_SEGMENTS = 8
|
||||
MAX_EVENTS = 256
|
||||
MAX_TICKS = 256
|
||||
|
||||
CREATE_CHILD = "CREATE_CHILD"
|
||||
RESERVE_REGION = "RESERVE_REGION"
|
||||
CREATE_MIRROR = "CREATE_MIRROR"
|
||||
CREATE_JIT_MASTER = "CREATE_JIT_MASTER"
|
||||
MAP_EXECUTABLE = "MAP_EXECUTABLE"
|
||||
CREATE_JIT_ALIAS = "CREATE_JIT_ALIAS"
|
||||
MAP_HOST_ALIAS = "MAP_HOST_ALIAS"
|
||||
MAP_REMOTE_ALIAS = "MAP_REMOTE_ALIAS"
|
||||
COPY_ALIAS = "COPY_ALIAS"
|
||||
UNMAP_REMOTE_ALIAS = "UNMAP_REMOTE_ALIAS"
|
||||
UNMAP_HOST_ALIAS = "UNMAP_HOST_ALIAS"
|
||||
CLOSE_JIT_ALIAS = "CLOSE_JIT_ALIAS"
|
||||
CLOSE_JIT_MASTER = "CLOSE_JIT_MASTER"
|
||||
FINALIZE_IMAGE = "FINALIZE_IMAGE"
|
||||
RELEASE_MIRROR = "RELEASE_MIRROR"
|
||||
UNMAP_REGION = "UNMAP_REGION"
|
||||
KILL_AND_REAP_CHILD = "KILL_AND_REAP_CHILD"
|
||||
OK = "OK"
|
||||
ERROR = "ERROR"
|
||||
|
||||
INDEXED = {
|
||||
CREATE_JIT_MASTER, MAP_EXECUTABLE, CREATE_JIT_ALIAS, MAP_HOST_ALIAS,
|
||||
MAP_REMOTE_ALIAS, COPY_ALIAS, UNMAP_REMOTE_ALIAS, UNMAP_HOST_ALIAS,
|
||||
CLOSE_JIT_ALIAS, CLOSE_JIT_MASTER,
|
||||
}
|
||||
OPERATIONS = INDEXED | {
|
||||
CREATE_CHILD, RESERVE_REGION, CREATE_MIRROR, FINALIZE_IMAGE,
|
||||
RELEASE_MIRROR, UNMAP_REGION, KILL_AND_REAP_CHILD,
|
||||
}
|
||||
|
||||
|
||||
class CompositionError(RuntimeError):
|
||||
"""The synthetic ownership transaction cannot close safely."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CompositionPlan:
|
||||
executable_segments: int
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not isinstance(self.executable_segments, int) \
|
||||
or isinstance(self.executable_segments, bool) \
|
||||
or not 1 <= self.executable_segments <= MAX_EXECUTABLE_SEGMENTS:
|
||||
raise CompositionError("executable segment count is invalid")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FakeEvent:
|
||||
operation: str
|
||||
result: str
|
||||
segment: int = -1
|
||||
ticks: int = 1
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.operation not in OPERATIONS or self.result not in {OK, ERROR}:
|
||||
raise CompositionError("event operation/result is invalid")
|
||||
if (self.operation in INDEXED) != (self.segment >= 0):
|
||||
raise CompositionError("event segment binding is invalid")
|
||||
if not isinstance(self.segment, int) or isinstance(self.segment, bool):
|
||||
raise CompositionError("event segment is invalid")
|
||||
if not isinstance(self.ticks, int) or isinstance(self.ticks, bool) \
|
||||
or not 1 <= self.ticks <= MAX_TICKS:
|
||||
raise CompositionError("event ticks are invalid")
|
||||
|
||||
|
||||
class FakeFacade:
|
||||
"""Exact fake script with no process, mapping, clock or device access."""
|
||||
|
||||
def __init__(self, events: tuple[FakeEvent, ...]) -> None:
|
||||
if not isinstance(events, tuple) or not 1 <= len(events) <= MAX_EVENTS \
|
||||
or any(type(event) is not FakeEvent for event in events):
|
||||
raise CompositionError("fake composition script is invalid")
|
||||
self._events = list(events)
|
||||
self.ticks = 0
|
||||
|
||||
def invoke(self, operation: str, segment: int = -1,
|
||||
cleanup: bool = False) -> str:
|
||||
if not self._events:
|
||||
raise CompositionError("fake composition script is exhausted")
|
||||
event = self._events.pop(0)
|
||||
if event.operation != operation or event.segment != segment:
|
||||
raise CompositionError("fake composition ordering differs")
|
||||
if not cleanup and self.ticks + event.ticks > MAX_TICKS:
|
||||
raise CompositionError("composition deadline precedes operation")
|
||||
self.ticks += event.ticks
|
||||
return event.result
|
||||
|
||||
@property
|
||||
def remaining(self) -> int:
|
||||
return len(self._events)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CompositionOutcome:
|
||||
classification: str
|
||||
success: bool
|
||||
child_alive: bool
|
||||
image_retained: bool
|
||||
resources_open: int
|
||||
fail_closed_termination: bool
|
||||
target_action_performed: bool = False
|
||||
firmware_behavior_proven: bool = False
|
||||
|
||||
|
||||
def _require(facade: FakeFacade, operation: str, segment: int = -1) -> None:
|
||||
if facade.invoke(operation, segment) != OK:
|
||||
raise CompositionError(f"{operation} failed")
|
||||
|
||||
|
||||
def run_composition(plan: CompositionPlan, facade: FakeFacade) -> CompositionOutcome:
|
||||
"""Compose synthetic resources; cleanup failure requires child termination."""
|
||||
if type(plan) is not CompositionPlan or type(facade) is not FakeFacade:
|
||||
raise CompositionError("composition boundary is invalid")
|
||||
child = region = mirror = committed = False
|
||||
masters: set[int] = set()
|
||||
aliases: set[int] = set()
|
||||
host_aliases: set[int] = set()
|
||||
remote_aliases: set[int] = set()
|
||||
primary_failed = cleanup_failed = terminated = False
|
||||
|
||||
try:
|
||||
_require(facade, CREATE_CHILD)
|
||||
child = True
|
||||
_require(facade, RESERVE_REGION)
|
||||
region = True
|
||||
_require(facade, CREATE_MIRROR)
|
||||
mirror = True
|
||||
for segment in range(plan.executable_segments):
|
||||
_require(facade, CREATE_JIT_MASTER, segment)
|
||||
masters.add(segment)
|
||||
_require(facade, MAP_EXECUTABLE, segment)
|
||||
_require(facade, CREATE_JIT_ALIAS, segment)
|
||||
aliases.add(segment)
|
||||
_require(facade, MAP_HOST_ALIAS, segment)
|
||||
host_aliases.add(segment)
|
||||
_require(facade, MAP_REMOTE_ALIAS, segment)
|
||||
remote_aliases.add(segment)
|
||||
_require(facade, COPY_ALIAS, segment)
|
||||
_require(facade, UNMAP_REMOTE_ALIAS, segment)
|
||||
remote_aliases.remove(segment)
|
||||
_require(facade, UNMAP_HOST_ALIAS, segment)
|
||||
host_aliases.remove(segment)
|
||||
_require(facade, CLOSE_JIT_ALIAS, segment)
|
||||
aliases.remove(segment)
|
||||
_require(facade, CLOSE_JIT_MASTER, segment)
|
||||
masters.remove(segment)
|
||||
_require(facade, FINALIZE_IMAGE)
|
||||
_require(facade, RELEASE_MIRROR)
|
||||
mirror = False
|
||||
committed = True
|
||||
except Exception:
|
||||
primary_failed = True
|
||||
|
||||
if primary_failed:
|
||||
for resources, operation in (
|
||||
(remote_aliases, UNMAP_REMOTE_ALIAS),
|
||||
(host_aliases, UNMAP_HOST_ALIAS),
|
||||
(aliases, CLOSE_JIT_ALIAS),
|
||||
(masters, CLOSE_JIT_MASTER),
|
||||
):
|
||||
for segment in sorted(resources, reverse=True):
|
||||
if facade.invoke(operation, segment, cleanup=True) != OK:
|
||||
cleanup_failed = True
|
||||
else:
|
||||
resources.remove(segment)
|
||||
if mirror:
|
||||
if facade.invoke(RELEASE_MIRROR, cleanup=True) != OK:
|
||||
cleanup_failed = True
|
||||
else:
|
||||
mirror = False
|
||||
if region:
|
||||
if facade.invoke(UNMAP_REGION, cleanup=True) != OK:
|
||||
cleanup_failed = True
|
||||
else:
|
||||
region = False
|
||||
if child:
|
||||
if facade.invoke(KILL_AND_REAP_CHILD, cleanup=True) != OK:
|
||||
raise CompositionError("fail-closed child termination failed")
|
||||
child = False
|
||||
terminated = True
|
||||
masters.clear()
|
||||
aliases.clear()
|
||||
host_aliases.clear()
|
||||
remote_aliases.clear()
|
||||
mirror = region = False
|
||||
|
||||
if facade.remaining:
|
||||
raise CompositionError("fake composition script has unused operations")
|
||||
open_count = (int(mirror) + int(region) + len(masters) + len(aliases) +
|
||||
len(host_aliases) + len(remote_aliases))
|
||||
if primary_failed:
|
||||
if child or open_count:
|
||||
raise CompositionError("failed composition retained ownership")
|
||||
classification = ("OFFLINE_FAIL_CLOSED_AFTER_CLEANUP_FAILURE" if cleanup_failed
|
||||
else "OFFLINE_COMPOSITION_ROLLED_BACK")
|
||||
return CompositionOutcome(classification, False, False, False, 0,
|
||||
terminated)
|
||||
if not committed or not child or not region or mirror or open_count != 1:
|
||||
raise CompositionError("successful composition invariants failed")
|
||||
return CompositionOutcome("OFFLINE_HYBRID_COMPOSITION_COMPLETE", True,
|
||||
True, True, 0, False)
|
||||
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Capability-free exact-progress copy and credential restoration model."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
MAX_COPY_SIZE = 128 * 1024 * 1024
|
||||
MAX_CHUNKS = 4096
|
||||
MAX_EVENTS = MAX_CHUNKS + 16
|
||||
MAX_TICKS = 8192
|
||||
MAX_U64 = (1 << 64) - 1
|
||||
|
||||
BACKUP_AUTHID = "BACKUP_AUTHID"
|
||||
BACKUP_CAPS = "BACKUP_CAPS"
|
||||
SET_PRIV_AUTHID = "SET_PRIV_AUTHID"
|
||||
SET_PRIV_CAPS = "SET_PRIV_CAPS"
|
||||
COPY_CHUNK = "COPY_CHUNK"
|
||||
RESTORE_CAPS = "RESTORE_CAPS"
|
||||
RESTORE_AUTHID = "RESTORE_AUTHID"
|
||||
KILL_AND_REAP_CHILD = "KILL_AND_REAP_CHILD"
|
||||
TERMINATE_SERVICE = "TERMINATE_SERVICE"
|
||||
OK = "OK"
|
||||
ERROR = "ERROR"
|
||||
MORE = "MORE"
|
||||
COMPLETE = "COMPLETE"
|
||||
|
||||
OPERATIONS = {
|
||||
BACKUP_AUTHID, BACKUP_CAPS, SET_PRIV_AUTHID, SET_PRIV_CAPS, COPY_CHUNK,
|
||||
RESTORE_CAPS, RESTORE_AUTHID, KILL_AND_REAP_CHILD, TERMINATE_SERVICE,
|
||||
}
|
||||
|
||||
|
||||
class CopyModelError(RuntimeError):
|
||||
"""The synthetic copy contract cannot reach a bounded safe state."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CopyPlan:
|
||||
source: int
|
||||
destination: int
|
||||
length: int
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
for value in (self.source, self.destination, self.length):
|
||||
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
||||
raise CopyModelError("copy plan value is invalid")
|
||||
if not 1 <= self.length <= MAX_COPY_SIZE:
|
||||
raise CopyModelError("copy length is invalid")
|
||||
if self.source > MAX_U64 - self.length \
|
||||
or self.destination > MAX_U64 - self.length:
|
||||
raise CopyModelError("copy range overflows")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FakeCopyEvent:
|
||||
operation: str
|
||||
result: str
|
||||
progress: int = 0
|
||||
remote_status: str = ""
|
||||
ticks: int = 1
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.operation not in OPERATIONS or self.result not in {OK, ERROR}:
|
||||
raise CopyModelError("copy event operation/result is invalid")
|
||||
if not isinstance(self.progress, int) or isinstance(self.progress, bool) \
|
||||
or self.progress < 0:
|
||||
raise CopyModelError("copy progress is invalid")
|
||||
if self.operation == COPY_CHUNK:
|
||||
if self.remote_status not in {MORE, COMPLETE}:
|
||||
raise CopyModelError("copy status is invalid")
|
||||
elif self.progress or self.remote_status:
|
||||
raise CopyModelError("non-copy event carries copy result")
|
||||
if not isinstance(self.ticks, int) or isinstance(self.ticks, bool) \
|
||||
or not 1 <= self.ticks <= MAX_TICKS:
|
||||
raise CopyModelError("copy event ticks are invalid")
|
||||
|
||||
|
||||
class FakeCopyFacade:
|
||||
"""Exact fake script with no credential, process, clock or memory access."""
|
||||
|
||||
def __init__(self, events: tuple[FakeCopyEvent, ...]) -> None:
|
||||
if not isinstance(events, tuple) or not 1 <= len(events) <= MAX_EVENTS \
|
||||
or any(type(item) is not FakeCopyEvent for item in events):
|
||||
raise CopyModelError("copy event script is invalid")
|
||||
self._events = list(events)
|
||||
self.ticks = 0
|
||||
|
||||
def invoke(self, operation: str, cleanup: bool = False) -> FakeCopyEvent:
|
||||
if not self._events:
|
||||
raise CopyModelError("copy event script is exhausted")
|
||||
event = self._events.pop(0)
|
||||
if event.operation != operation:
|
||||
raise CopyModelError("copy event ordering differs")
|
||||
if not cleanup and self.ticks + event.ticks > MAX_TICKS:
|
||||
raise CopyModelError("copy deadline precedes operation")
|
||||
self.ticks += event.ticks
|
||||
return event
|
||||
|
||||
@property
|
||||
def remaining(self) -> int:
|
||||
return len(self._events)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CopyOutcome:
|
||||
classification: str
|
||||
success: bool
|
||||
copied: int
|
||||
restore_failure_bits: int
|
||||
child_alive: bool
|
||||
service_available: bool
|
||||
target_copy_performed: bool = False
|
||||
firmware_behavior_proven: bool = False
|
||||
|
||||
|
||||
def run_copy(plan: CopyPlan, facade: FakeCopyFacade) -> CopyOutcome:
|
||||
"""Run one bounded synthetic copy and attempt every required restore."""
|
||||
if type(plan) is not CopyPlan or type(facade) is not FakeCopyFacade:
|
||||
raise CopyModelError("copy model boundary is invalid")
|
||||
auth_changed = caps_changed = False
|
||||
copied = chunks = restore_failures = 0
|
||||
failed = False
|
||||
|
||||
try:
|
||||
if facade.invoke(BACKUP_AUTHID).result != OK:
|
||||
failed = True
|
||||
elif facade.invoke(BACKUP_CAPS).result != OK:
|
||||
failed = True
|
||||
elif facade.invoke(SET_PRIV_AUTHID).result != OK:
|
||||
failed = True
|
||||
else:
|
||||
auth_changed = True
|
||||
if facade.invoke(SET_PRIV_CAPS).result != OK:
|
||||
failed = True
|
||||
else:
|
||||
caps_changed = True
|
||||
while copied < plan.length:
|
||||
if chunks >= MAX_CHUNKS:
|
||||
failed = True
|
||||
break
|
||||
event = facade.invoke(COPY_CHUNK)
|
||||
chunks += 1
|
||||
remaining = plan.length - copied
|
||||
if event.result != OK or event.progress == 0 \
|
||||
or event.progress > remaining:
|
||||
failed = True
|
||||
break
|
||||
copied += event.progress
|
||||
if event.remote_status == COMPLETE:
|
||||
if copied != plan.length:
|
||||
failed = True
|
||||
break
|
||||
if copied == plan.length:
|
||||
failed = True
|
||||
break
|
||||
if copied != plan.length:
|
||||
failed = True
|
||||
except CopyModelError:
|
||||
failed = True
|
||||
|
||||
if caps_changed:
|
||||
if facade.invoke(RESTORE_CAPS, cleanup=True).result != OK:
|
||||
restore_failures |= 1
|
||||
caps_changed = False
|
||||
if auth_changed:
|
||||
if facade.invoke(RESTORE_AUTHID, cleanup=True).result != OK:
|
||||
restore_failures |= 2
|
||||
auth_changed = False
|
||||
|
||||
child_alive = True
|
||||
service_available = True
|
||||
if (failed and copied) or restore_failures:
|
||||
if facade.invoke(KILL_AND_REAP_CHILD, cleanup=True).result != OK:
|
||||
raise CopyModelError("child termination failed")
|
||||
child_alive = False
|
||||
if restore_failures:
|
||||
if facade.invoke(TERMINATE_SERVICE, cleanup=True).result != OK:
|
||||
raise CopyModelError("compromised service termination failed")
|
||||
service_available = False
|
||||
if facade.remaining:
|
||||
raise CopyModelError("copy event script has unused operations")
|
||||
|
||||
success = not failed and copied == plan.length and restore_failures == 0
|
||||
if success:
|
||||
return CopyOutcome("OFFLINE_EXACT_COPY_COMPLETE", True, copied, 0,
|
||||
True, True)
|
||||
if restore_failures:
|
||||
classification = "OFFLINE_RESTORE_FAILURE_CONTAINED"
|
||||
elif copied:
|
||||
classification = "OFFLINE_PARTIAL_COPY_CONTAINED"
|
||||
else:
|
||||
classification = "OFFLINE_COPY_REJECTED_BEFORE_MUTATION"
|
||||
return CopyOutcome(classification, False, copied, restore_failures,
|
||||
child_alive, service_available)
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Host-only supervisor/worker preemption architecture model."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
MAX_COPY_SIZE = 128 * 1024 * 1024
|
||||
MAX_TICKS = 256
|
||||
MAX_EVENTS = 16
|
||||
|
||||
CREATE_WORKER = "CREATE_WORKER"
|
||||
VERIFY_WORKER = "VERIFY_WORKER"
|
||||
START_COPY = "START_COPY"
|
||||
RECEIVE_RESULT = "RECEIVE_RESULT"
|
||||
DEADLINE = "DEADLINE"
|
||||
TERMINATE_WORKER = "TERMINATE_WORKER"
|
||||
REAP_WORKER = "REAP_WORKER"
|
||||
TERMINATE_CHILD = "TERMINATE_CHILD"
|
||||
REAP_CHILD = "REAP_CHILD"
|
||||
OK = "OK"
|
||||
ERROR = "ERROR"
|
||||
SUCCESS = "SUCCESS"
|
||||
COPY_ERROR = "COPY_ERROR"
|
||||
RESTORE_ERROR = "RESTORE_ERROR"
|
||||
|
||||
OPERATIONS = {
|
||||
CREATE_WORKER, VERIFY_WORKER, START_COPY, RECEIVE_RESULT, DEADLINE,
|
||||
TERMINATE_WORKER, REAP_WORKER, TERMINATE_CHILD, REAP_CHILD,
|
||||
}
|
||||
|
||||
|
||||
class SupervisorError(RuntimeError):
|
||||
"""The synthetic supervisor cannot prove containment."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkerPlan:
|
||||
worker_id: int
|
||||
child_id: int
|
||||
copy_length: int
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
for value in (self.worker_id, self.child_id, self.copy_length):
|
||||
if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
|
||||
raise SupervisorError("worker plan value is invalid")
|
||||
if self.worker_id == self.child_id or self.copy_length > MAX_COPY_SIZE:
|
||||
raise SupervisorError("worker plan identity or size is invalid")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkerResult:
|
||||
worker_id: int
|
||||
status: str
|
||||
copied: int
|
||||
restore_failure_bits: int
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not isinstance(self.worker_id, int) or isinstance(self.worker_id, bool) \
|
||||
or self.worker_id <= 0 or self.status not in {
|
||||
SUCCESS, COPY_ERROR, RESTORE_ERROR}:
|
||||
raise SupervisorError("worker result identity/status is invalid")
|
||||
if not isinstance(self.copied, int) or isinstance(self.copied, bool) \
|
||||
or self.copied < 0 or not isinstance(self.restore_failure_bits, int) \
|
||||
or isinstance(self.restore_failure_bits, bool) \
|
||||
or not 0 <= self.restore_failure_bits <= 3:
|
||||
raise SupervisorError("worker result fields are invalid")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FakeSupervisorEvent:
|
||||
operation: str
|
||||
result: str
|
||||
worker_result: WorkerResult | None = None
|
||||
ticks: int = 1
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.operation not in OPERATIONS or self.result not in {OK, ERROR}:
|
||||
raise SupervisorError("supervisor event is invalid")
|
||||
if (self.operation == RECEIVE_RESULT) != (self.worker_result is not None):
|
||||
raise SupervisorError("worker result binding is invalid")
|
||||
if not isinstance(self.ticks, int) or isinstance(self.ticks, bool) \
|
||||
or not 1 <= self.ticks <= MAX_TICKS:
|
||||
raise SupervisorError("supervisor event ticks are invalid")
|
||||
|
||||
|
||||
class FakeSupervisorFacade:
|
||||
"""Exact fake operations with no process, signal, clock or IPC access."""
|
||||
|
||||
def __init__(self, events: tuple[FakeSupervisorEvent, ...]) -> None:
|
||||
if not isinstance(events, tuple) or not 1 <= len(events) <= MAX_EVENTS \
|
||||
or any(type(item) is not FakeSupervisorEvent for item in events):
|
||||
raise SupervisorError("supervisor script is invalid")
|
||||
self._events = list(events)
|
||||
self.ticks = 0
|
||||
|
||||
def invoke(self, operation: str, terminal: bool = False) -> FakeSupervisorEvent:
|
||||
if not self._events:
|
||||
raise SupervisorError("supervisor script is exhausted")
|
||||
event = self._events.pop(0)
|
||||
if event.operation != operation:
|
||||
raise SupervisorError("supervisor event ordering differs")
|
||||
if not terminal and self.ticks + event.ticks > MAX_TICKS:
|
||||
raise SupervisorError("supervisor deadline precedes operation")
|
||||
self.ticks += event.ticks
|
||||
return event
|
||||
|
||||
@property
|
||||
def remaining(self) -> int:
|
||||
return len(self._events)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SupervisorOutcome:
|
||||
classification: str
|
||||
success: bool
|
||||
service_alive: bool
|
||||
worker_alive: bool
|
||||
child_alive: bool
|
||||
copied: int
|
||||
automatic_restart: bool = False
|
||||
target_action_performed: bool = False
|
||||
firmware_behavior_proven: bool = False
|
||||
|
||||
|
||||
def _terminal(facade: FakeSupervisorFacade, operation: str) -> None:
|
||||
if facade.invoke(operation, terminal=True).result != OK:
|
||||
raise SupervisorError(f"terminal operation failed: {operation}")
|
||||
|
||||
|
||||
def run_supervisor(plan: WorkerPlan, facade: FakeSupervisorFacade,
|
||||
deadline_first: bool = False) -> SupervisorOutcome:
|
||||
"""Run one synthetic, non-restarting worker attempt."""
|
||||
if type(plan) is not WorkerPlan or type(facade) is not FakeSupervisorFacade \
|
||||
or type(deadline_first) is not bool:
|
||||
raise SupervisorError("supervisor model boundary is invalid")
|
||||
worker = started = False
|
||||
copied = 0
|
||||
failed = deadline = False
|
||||
|
||||
if facade.invoke(CREATE_WORKER).result != OK:
|
||||
failed = True
|
||||
else:
|
||||
worker = True
|
||||
if facade.invoke(VERIFY_WORKER).result != OK:
|
||||
failed = True
|
||||
elif facade.invoke(START_COPY).result != OK:
|
||||
started = True
|
||||
failed = True
|
||||
else:
|
||||
started = True
|
||||
if deadline_first:
|
||||
if facade.invoke(DEADLINE).result != OK:
|
||||
raise SupervisorError("deadline event failed")
|
||||
deadline = failed = True
|
||||
else:
|
||||
event = facade.invoke(RECEIVE_RESULT)
|
||||
result = event.worker_result
|
||||
if event.result != OK or result is None:
|
||||
failed = True
|
||||
else:
|
||||
copied = result.copied
|
||||
valid = (result.worker_id == plan.worker_id and
|
||||
result.status == SUCCESS and
|
||||
result.copied == plan.copy_length and
|
||||
result.restore_failure_bits == 0)
|
||||
failed = not valid
|
||||
|
||||
if worker:
|
||||
if failed:
|
||||
_terminal(facade, TERMINATE_WORKER)
|
||||
_terminal(facade, REAP_WORKER)
|
||||
worker = False
|
||||
if failed and started:
|
||||
_terminal(facade, TERMINATE_CHILD)
|
||||
_terminal(facade, REAP_CHILD)
|
||||
if facade.remaining:
|
||||
raise SupervisorError("supervisor script has unused operations")
|
||||
|
||||
if not failed:
|
||||
return SupervisorOutcome("OFFLINE_WORKER_RESULT_ACCEPTED", True, True,
|
||||
False, True, copied)
|
||||
classification = ("OFFLINE_DEADLINE_CONTAINED" if deadline else
|
||||
"OFFLINE_WORKER_FAILURE_CONTAINED")
|
||||
return SupervisorOutcome(classification, False, True, False,
|
||||
not started, copied)
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Bytes-only fixed worker result record and generation identity contract."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import struct
|
||||
|
||||
|
||||
MAGIC = b"CHW10AQ1"
|
||||
VERSION = 1
|
||||
RECORD_SIZE = 128
|
||||
HASHED_SIZE = 96
|
||||
MAX_COPY_SIZE = 128 * 1024 * 1024
|
||||
STATUS_SUCCESS = 0
|
||||
STATUS_COPY_ERROR = 1
|
||||
STATUS_RESTORE_ERROR = 2
|
||||
STATUS_DEADLINE = 3
|
||||
VALID_STATUSES = {STATUS_SUCCESS, STATUS_COPY_ERROR,
|
||||
STATUS_RESTORE_ERROR, STATUS_DEADLINE}
|
||||
PREFIX = struct.Struct("<8sHHIIIQQQII16s16s8s")
|
||||
|
||||
|
||||
class WorkerRecordError(ValueError):
|
||||
"""The fixed worker record is malformed or not precommitted."""
|
||||
|
||||
|
||||
def _token(value: bytes, name: str) -> bytes:
|
||||
if type(value) is not bytes or len(value) != 16 or not any(value):
|
||||
raise WorkerRecordError(f"{name} must be a nonzero 16-byte token")
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkerPrecommit:
|
||||
attempt_id: bytes
|
||||
nonce: bytes
|
||||
worker_pid: int
|
||||
child_pid: int
|
||||
generation: int
|
||||
requested: int
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_token(self.attempt_id, "attempt ID")
|
||||
_token(self.nonce, "worker nonce")
|
||||
for value in (self.worker_pid, self.child_pid, self.generation,
|
||||
self.requested):
|
||||
if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
|
||||
raise WorkerRecordError("precommit integer is invalid")
|
||||
if self.worker_pid == self.child_pid or self.worker_pid > 0xffffffff \
|
||||
or self.child_pid > 0xffffffff or self.generation > 0xffffffffffffffff \
|
||||
or self.requested > MAX_COPY_SIZE:
|
||||
raise WorkerRecordError("precommit identity or size is invalid")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkerRecord:
|
||||
status: int
|
||||
worker_pid: int
|
||||
child_pid: int
|
||||
generation: int
|
||||
requested: int
|
||||
copied: int
|
||||
restore_failure_bits: int
|
||||
flags: int
|
||||
attempt_id: bytes
|
||||
nonce: bytes
|
||||
|
||||
@property
|
||||
def successful(self) -> bool:
|
||||
return (self.status == STATUS_SUCCESS and self.copied == self.requested
|
||||
and self.restore_failure_bits == 0 and self.flags == 0)
|
||||
|
||||
|
||||
def encode_record(precommit: WorkerPrecommit, status: int, copied: int,
|
||||
restore_failure_bits: int = 0, flags: int = 0) -> bytes:
|
||||
"""Encode synthetic bytes; this function has no transport capability."""
|
||||
if type(precommit) is not WorkerPrecommit or status not in VALID_STATUSES:
|
||||
raise WorkerRecordError("record input is invalid")
|
||||
for value in (copied, restore_failure_bits, flags):
|
||||
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
||||
raise WorkerRecordError("record numeric field is invalid")
|
||||
if copied > precommit.requested or restore_failure_bits > 3 \
|
||||
or flags > 0xffffffff:
|
||||
raise WorkerRecordError("record progress, restore bits or flags are invalid")
|
||||
if status == STATUS_SUCCESS and (copied != precommit.requested
|
||||
or restore_failure_bits or flags):
|
||||
raise WorkerRecordError("success record is not exact")
|
||||
prefix = PREFIX.pack(MAGIC, VERSION, RECORD_SIZE, status,
|
||||
precommit.worker_pid, precommit.child_pid,
|
||||
precommit.generation, precommit.requested, copied,
|
||||
restore_failure_bits, flags, precommit.attempt_id,
|
||||
precommit.nonce, bytes(8))
|
||||
if len(prefix) != HASHED_SIZE:
|
||||
raise WorkerRecordError("internal record layout differs")
|
||||
return prefix + hashlib.sha256(prefix).digest()
|
||||
|
||||
|
||||
def parse_record(raw: bytes, expected: WorkerPrecommit) -> WorkerRecord:
|
||||
"""Parse exactly one complete record and enforce the full precommit."""
|
||||
if type(raw) is not bytes or len(raw) != RECORD_SIZE \
|
||||
or type(expected) is not WorkerPrecommit:
|
||||
raise WorkerRecordError("record boundary is invalid")
|
||||
prefix, digest = raw[:HASHED_SIZE], raw[HASHED_SIZE:]
|
||||
if not hashlib.sha256(prefix).digest() == digest:
|
||||
raise WorkerRecordError("record digest differs")
|
||||
(magic, version, size, status, worker_pid, child_pid, generation,
|
||||
requested, copied, restore_bits, flags, attempt_id, nonce,
|
||||
reserved) = PREFIX.unpack(prefix)
|
||||
if magic != MAGIC or version != VERSION or size != RECORD_SIZE \
|
||||
or status not in VALID_STATUSES or any(reserved):
|
||||
raise WorkerRecordError("record header is invalid")
|
||||
if (worker_pid, child_pid, generation, requested, attempt_id, nonce) != (
|
||||
expected.worker_pid, expected.child_pid, expected.generation,
|
||||
expected.requested, expected.attempt_id, expected.nonce):
|
||||
raise WorkerRecordError("record identity differs from precommit")
|
||||
if copied > requested or restore_bits > 3 or flags != 0:
|
||||
raise WorkerRecordError("record result fields are invalid")
|
||||
record = WorkerRecord(status, worker_pid, child_pid, generation, requested,
|
||||
copied, restore_bits, flags, attempt_id, nonce)
|
||||
if status == STATUS_SUCCESS and not record.successful:
|
||||
raise WorkerRecordError("success record is incomplete")
|
||||
if status == STATUS_RESTORE_ERROR and restore_bits == 0:
|
||||
raise WorkerRecordError("restore error has no failure bits")
|
||||
return record
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Transport-free partial-read/deadline model for one worker result record."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from phase10aq_worker_result_record import (
|
||||
RECORD_SIZE, WorkerPrecommit, WorkerRecord, WorkerRecordError, parse_record,
|
||||
)
|
||||
|
||||
|
||||
MAX_CHUNKS = 128
|
||||
MAX_TICKS = 256
|
||||
DATA = "DATA"
|
||||
EOF = "EOF"
|
||||
DEADLINE = "DEADLINE"
|
||||
EVENTS = {DATA, EOF, DEADLINE}
|
||||
|
||||
|
||||
class ChannelModelError(RuntimeError):
|
||||
"""The synthetic channel script or boundary is invalid."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FakeReadEvent:
|
||||
kind: str
|
||||
data: bytes = b""
|
||||
ticks: int = 1
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.kind not in EVENTS or type(self.data) is not bytes:
|
||||
raise ChannelModelError("read event is invalid")
|
||||
if (self.kind == DATA) != bool(self.data) or len(self.data) > RECORD_SIZE:
|
||||
raise ChannelModelError("read event payload is invalid")
|
||||
if not isinstance(self.ticks, int) or isinstance(self.ticks, bool) \
|
||||
or not 1 <= self.ticks <= MAX_TICKS:
|
||||
raise ChannelModelError("read event ticks are invalid")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChannelPlan:
|
||||
precommit: WorkerPrecommit
|
||||
writer_pid: int
|
||||
writer_generation: int
|
||||
writer_nonce: bytes
|
||||
exclusive_writer: bool = True
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if type(self.precommit) is not WorkerPrecommit \
|
||||
or self.writer_pid != self.precommit.worker_pid \
|
||||
or self.writer_generation != self.precommit.generation \
|
||||
or self.writer_nonce != self.precommit.nonce \
|
||||
or self.exclusive_writer is not True:
|
||||
raise ChannelModelError("exclusive writer precommit differs")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChannelOutcome:
|
||||
classification: str
|
||||
accepted: bool
|
||||
buffered: int
|
||||
record: WorkerRecord | None
|
||||
containment_required: bool
|
||||
eof_is_success: bool = False
|
||||
live_transport_present: bool = False
|
||||
|
||||
|
||||
def receive_record(plan: ChannelPlan,
|
||||
events: tuple[FakeReadEvent, ...]) -> ChannelOutcome:
|
||||
"""Assemble one exact record from supplied bytes; no I/O is performed."""
|
||||
if type(plan) is not ChannelPlan or not isinstance(events, tuple) \
|
||||
or not 1 <= len(events) <= MAX_CHUNKS \
|
||||
or any(type(item) is not FakeReadEvent for item in events):
|
||||
raise ChannelModelError("channel model boundary is invalid")
|
||||
buffer = bytearray()
|
||||
ticks = 0
|
||||
for index, event in enumerate(events):
|
||||
if ticks + event.ticks > MAX_TICKS:
|
||||
return ChannelOutcome("OFFLINE_CHANNEL_DEADLINE", False,
|
||||
len(buffer), None, True)
|
||||
ticks += event.ticks
|
||||
if event.kind == DEADLINE:
|
||||
return ChannelOutcome("OFFLINE_CHANNEL_DEADLINE", False,
|
||||
len(buffer), None, True)
|
||||
if event.kind == EOF:
|
||||
return ChannelOutcome("OFFLINE_EOF_BEFORE_COMPLETE_RECORD", False,
|
||||
len(buffer), None, True)
|
||||
if len(buffer) + len(event.data) > RECORD_SIZE:
|
||||
return ChannelOutcome("OFFLINE_CHANNEL_OVERFLOW", False,
|
||||
len(buffer), None, True)
|
||||
buffer.extend(event.data)
|
||||
if len(buffer) == RECORD_SIZE:
|
||||
if index != len(events) - 1:
|
||||
raise ChannelModelError("events remain after record boundary")
|
||||
try:
|
||||
record = parse_record(bytes(buffer), plan.precommit)
|
||||
except WorkerRecordError:
|
||||
return ChannelOutcome("OFFLINE_RECORD_REJECTED", False,
|
||||
RECORD_SIZE, None, True)
|
||||
return ChannelOutcome("OFFLINE_EXACT_RECORD_ACCEPTED", True,
|
||||
RECORD_SIZE, record, False)
|
||||
return ChannelOutcome("OFFLINE_INCOMPLETE_WITHOUT_DEADLINE", False,
|
||||
len(buffer), None, True)
|
||||
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Host-only RFFDG descriptor ownership and absolute deadline model."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from phase10aq_worker_result_record import (
|
||||
RECORD_SIZE, WorkerPrecommit, WorkerRecordError, parse_record,
|
||||
)
|
||||
|
||||
|
||||
MAX_TICKS = 256
|
||||
MAX_READ_EVENTS = 256
|
||||
CREATE_PIPE = "CREATE_PIPE"
|
||||
SET_PARENT_READ_NONBLOCK = "SET_PARENT_READ_NONBLOCK"
|
||||
SPAWN_RFFDG = "SPAWN_RFFDG"
|
||||
PARENT_CLOSE_WRITE = "PARENT_CLOSE_WRITE"
|
||||
CHILD_CLOSE_READ = "CHILD_CLOSE_READ"
|
||||
CHILD_CLOSE_WRITE = "CHILD_CLOSE_WRITE"
|
||||
PARENT_CLOSE_READ = "PARENT_CLOSE_READ"
|
||||
TERMINATE_CHILD = "TERMINATE_CHILD"
|
||||
REAP_CHILD = "REAP_CHILD"
|
||||
OK = "OK"
|
||||
ERROR = "ERROR"
|
||||
DATA = "DATA"
|
||||
EINTR = "EINTR"
|
||||
WOULD_BLOCK = "WOULD_BLOCK"
|
||||
EOF = "EOF"
|
||||
|
||||
OPERATIONS = {
|
||||
CREATE_PIPE, SET_PARENT_READ_NONBLOCK, SPAWN_RFFDG,
|
||||
PARENT_CLOSE_WRITE, CHILD_CLOSE_READ, CHILD_CLOSE_WRITE,
|
||||
PARENT_CLOSE_READ, TERMINATE_CHILD, REAP_CHILD,
|
||||
}
|
||||
READ_KINDS = {DATA, EINTR, WOULD_BLOCK, EOF}
|
||||
|
||||
|
||||
class FdModelError(RuntimeError):
|
||||
"""The synthetic descriptor transaction cannot close safely."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FakeFdEvent:
|
||||
operation: str
|
||||
result: str
|
||||
ticks: int = 1
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.operation not in OPERATIONS or self.result not in {OK, ERROR}:
|
||||
raise FdModelError("FD event is invalid")
|
||||
if not isinstance(self.ticks, int) or isinstance(self.ticks, bool) \
|
||||
or not 1 <= self.ticks <= MAX_TICKS:
|
||||
raise FdModelError("FD event ticks are invalid")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FakeRead:
|
||||
kind: str
|
||||
data: bytes = b""
|
||||
ticks: int = 1
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.kind not in READ_KINDS or type(self.data) is not bytes:
|
||||
raise FdModelError("read event is invalid")
|
||||
if (self.kind == DATA) != bool(self.data) or len(self.data) > RECORD_SIZE:
|
||||
raise FdModelError("read payload is invalid")
|
||||
if not isinstance(self.ticks, int) or isinstance(self.ticks, bool) \
|
||||
or not 1 <= self.ticks <= MAX_TICKS:
|
||||
raise FdModelError("read ticks are invalid")
|
||||
|
||||
|
||||
class FakeFdFacade:
|
||||
"""Exact scripted FD facade with no OS, process, clock or I/O imports."""
|
||||
|
||||
def __init__(self, events: tuple[FakeFdEvent, ...]) -> None:
|
||||
if not isinstance(events, tuple) or not events \
|
||||
or any(type(item) is not FakeFdEvent for item in events):
|
||||
raise FdModelError("FD script is invalid")
|
||||
self._events = list(events)
|
||||
self.ticks = 0
|
||||
|
||||
def invoke(self, operation: str, cleanup: bool = False) -> str:
|
||||
if not self._events:
|
||||
raise FdModelError("FD script is exhausted")
|
||||
event = self._events.pop(0)
|
||||
if event.operation != operation:
|
||||
raise FdModelError("FD operation ordering differs")
|
||||
if not cleanup and self.ticks + event.ticks > MAX_TICKS:
|
||||
raise FdModelError("absolute deadline precedes FD operation")
|
||||
self.ticks += event.ticks
|
||||
return event.result
|
||||
|
||||
@property
|
||||
def remaining(self) -> int:
|
||||
return len(self._events)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FdOutcome:
|
||||
classification: str
|
||||
success: bool
|
||||
buffered: int
|
||||
eintr_count: int
|
||||
parent_fds_open: int
|
||||
child_fds_open: int
|
||||
worker_alive: bool
|
||||
containment_required: bool
|
||||
rffdg_used: bool = True
|
||||
live_fd_present: bool = False
|
||||
|
||||
|
||||
def _close(facade: FakeFdFacade, operation: str) -> None:
|
||||
if facade.invoke(operation, cleanup=True) != OK:
|
||||
raise FdModelError(f"terminal cleanup failed: {operation}")
|
||||
|
||||
|
||||
def run_fd_transaction(precommit: WorkerPrecommit, facade: FakeFdFacade,
|
||||
reads: tuple[FakeRead, ...]) -> FdOutcome:
|
||||
"""Model exact descriptor ownership and one absolute deadline."""
|
||||
if type(precommit) is not WorkerPrecommit or type(facade) is not FakeFdFacade \
|
||||
or not isinstance(reads, tuple) or not 1 <= len(reads) <= MAX_READ_EVENTS \
|
||||
or any(type(item) is not FakeRead for item in reads):
|
||||
raise FdModelError("FD model boundary is invalid")
|
||||
parent_read = parent_write = child_read = child_write = worker = False
|
||||
failed = False
|
||||
buffer = bytearray()
|
||||
eintr_count = 0
|
||||
|
||||
try:
|
||||
if facade.invoke(CREATE_PIPE) != OK:
|
||||
failed = True
|
||||
else:
|
||||
parent_read = parent_write = True
|
||||
if facade.invoke(SET_PARENT_READ_NONBLOCK) != OK:
|
||||
failed = True
|
||||
elif facade.invoke(SPAWN_RFFDG) != OK:
|
||||
failed = True
|
||||
else:
|
||||
worker = child_read = child_write = True
|
||||
if facade.invoke(PARENT_CLOSE_WRITE) != OK:
|
||||
failed = True
|
||||
else:
|
||||
parent_write = False
|
||||
if facade.invoke(CHILD_CLOSE_READ) != OK:
|
||||
failed = True
|
||||
else:
|
||||
child_read = False
|
||||
for index, read in enumerate(reads):
|
||||
if facade.ticks + read.ticks > MAX_TICKS:
|
||||
failed = True
|
||||
break
|
||||
facade.ticks += read.ticks
|
||||
if read.kind == EINTR:
|
||||
eintr_count += 1
|
||||
continue
|
||||
if read.kind == WOULD_BLOCK:
|
||||
continue
|
||||
if read.kind == EOF:
|
||||
failed = True
|
||||
break
|
||||
if len(buffer) + len(read.data) > RECORD_SIZE:
|
||||
failed = True
|
||||
break
|
||||
buffer.extend(read.data)
|
||||
if len(buffer) == RECORD_SIZE:
|
||||
if index != len(reads) - 1:
|
||||
failed = True
|
||||
break
|
||||
try:
|
||||
parse_record(bytes(buffer), precommit)
|
||||
except WorkerRecordError:
|
||||
failed = True
|
||||
break
|
||||
if len(buffer) != RECORD_SIZE:
|
||||
failed = True
|
||||
except FdModelError:
|
||||
failed = True
|
||||
|
||||
if worker:
|
||||
if failed:
|
||||
_close(facade, TERMINATE_CHILD)
|
||||
if child_read:
|
||||
_close(facade, CHILD_CLOSE_READ)
|
||||
child_read = False
|
||||
if child_write:
|
||||
_close(facade, CHILD_CLOSE_WRITE)
|
||||
child_write = False
|
||||
_close(facade, REAP_CHILD)
|
||||
worker = False
|
||||
if parent_write:
|
||||
_close(facade, PARENT_CLOSE_WRITE)
|
||||
parent_write = False
|
||||
if parent_read:
|
||||
_close(facade, PARENT_CLOSE_READ)
|
||||
parent_read = False
|
||||
if facade.remaining:
|
||||
raise FdModelError("FD script has unused operations")
|
||||
|
||||
if failed:
|
||||
return FdOutcome("OFFLINE_FD_TRANSACTION_CONTAINED", False,
|
||||
len(buffer), eintr_count, 0, 0, False, True)
|
||||
return FdOutcome("OFFLINE_FD_TRANSACTION_COMPLETE", True,
|
||||
RECORD_SIZE, eintr_count, 0, 0, False, False)
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Target-free launch-context A/B canary contract.
|
||||
|
||||
This module validates immutable synthetic data only. It has no target source,
|
||||
artifact, socket, process, clock, filesystem output or device capability.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import re
|
||||
|
||||
|
||||
PHASE = "PHASE_1_0AV_TARGET_FREE_LAUNCH_CONTEXT_CANARY"
|
||||
FIRMWARE = "9.60"
|
||||
PROTOCOL_MAGIC = "CHD10AV1"
|
||||
RAW_ELFLDR = "RAW_ELFLDR"
|
||||
BIGAPP_CANDIDATE = "BIGAPP_CANDIDATE"
|
||||
ARM_KINDS = (RAW_ELFLDR, BIGAPP_CANDIDATE)
|
||||
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
||||
RUN_ID = re.compile(r"^CHIMERA_AV_[A-Z0-9_-]{8,48}$")
|
||||
RETURN_MIN = -(1 << 31)
|
||||
RETURN_MAX = (1 << 31) - 1
|
||||
|
||||
|
||||
class CanaryContractError(ValueError):
|
||||
"""The offline canary contract is incomplete, ambiguous or unsafe."""
|
||||
|
||||
|
||||
def _exact_hash(value: str) -> bool:
|
||||
return isinstance(value, str) and SHA256.fullmatch(value) is not None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CanaryArm:
|
||||
kind: str
|
||||
launcher_sha256: str
|
||||
run_id: str
|
||||
approval_sha256: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CanaryPairPlan:
|
||||
phase: str
|
||||
firmware: str
|
||||
protocol_magic: str
|
||||
payload_sha256: str
|
||||
arms: tuple[CanaryArm, CanaryArm]
|
||||
one_shot_each: bool
|
||||
automatic_retry: bool
|
||||
reconnect: bool
|
||||
resume: bool
|
||||
installation: bool
|
||||
autoload: bool
|
||||
device_write_authorized: bool
|
||||
app_termination_authorized: bool
|
||||
result_reception_authorized: bool
|
||||
activation_authorized: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CanaryObservation:
|
||||
kind: str
|
||||
launcher_sha256: str
|
||||
payload_sha256: str
|
||||
run_id: str
|
||||
protocol_magic: str
|
||||
d04_seen: bool
|
||||
d04_result: int
|
||||
d04_sequence: int
|
||||
submit_seen: bool
|
||||
submit_result: int
|
||||
submit_errno: int
|
||||
submit_sequence: int
|
||||
terminal_seen: bool
|
||||
terminal_sequence: int
|
||||
cleanup_complete: bool
|
||||
visible_output_observed: bool
|
||||
retry_count: int
|
||||
persistent_write_count: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CanaryClassification:
|
||||
status: str
|
||||
pair_comparable: bool
|
||||
submit_return_differs: bool
|
||||
launch_context_candidate: bool
|
||||
root_cause_proven: bool = False
|
||||
visible_output_proven: bool = False
|
||||
firmware_behavior_proven: bool = False
|
||||
device_action_authorized: bool = False
|
||||
|
||||
|
||||
def validate_plan(plan: CanaryPairPlan) -> None:
|
||||
"""Validate a hypothetical pair without activating either arm."""
|
||||
if type(plan) is not CanaryPairPlan or plan.phase != PHASE \
|
||||
or plan.firmware != FIRMWARE or plan.protocol_magic != PROTOCOL_MAGIC:
|
||||
raise CanaryContractError("pair identity is invalid")
|
||||
if not _exact_hash(plan.payload_sha256) or type(plan.arms) is not tuple \
|
||||
or len(plan.arms) != 2 \
|
||||
or any(type(arm) is not CanaryArm for arm in plan.arms):
|
||||
raise CanaryContractError("pair binding is invalid")
|
||||
if tuple(arm.kind for arm in plan.arms) != ARM_KINDS:
|
||||
raise CanaryContractError("arm order or kind is invalid")
|
||||
for arm in plan.arms:
|
||||
if not _exact_hash(arm.launcher_sha256) \
|
||||
or not _exact_hash(arm.approval_sha256) \
|
||||
or not isinstance(arm.run_id, str) \
|
||||
or RUN_ID.fullmatch(arm.run_id) is None:
|
||||
raise CanaryContractError("arm identity is invalid")
|
||||
if plan.arms[0].launcher_sha256 == plan.arms[1].launcher_sha256 \
|
||||
or plan.arms[0].run_id == plan.arms[1].run_id \
|
||||
or plan.arms[0].approval_sha256 == plan.arms[1].approval_sha256:
|
||||
raise CanaryContractError("arms are not independently authorized")
|
||||
if plan.one_shot_each is not True or plan.automatic_retry is not False \
|
||||
or plan.reconnect is not False or plan.resume is not False \
|
||||
or plan.installation is not False or plan.autoload is not False:
|
||||
raise CanaryContractError("pair one-shot policy is invalid")
|
||||
if plan.device_write_authorized or plan.app_termination_authorized \
|
||||
or plan.result_reception_authorized or plan.activation_authorized:
|
||||
raise CanaryContractError("offline plan grants device authority")
|
||||
|
||||
|
||||
def _validate_observation(plan: CanaryPairPlan, arm: CanaryArm,
|
||||
observation: CanaryObservation) -> bool:
|
||||
if type(observation) is not CanaryObservation \
|
||||
or observation.kind != arm.kind \
|
||||
or observation.launcher_sha256 != arm.launcher_sha256 \
|
||||
or observation.payload_sha256 != plan.payload_sha256 \
|
||||
or observation.run_id != arm.run_id \
|
||||
or observation.protocol_magic != PROTOCOL_MAGIC:
|
||||
raise CanaryContractError("observation identity differs")
|
||||
booleans = (observation.d04_seen, observation.submit_seen,
|
||||
observation.terminal_seen, observation.cleanup_complete,
|
||||
observation.visible_output_observed)
|
||||
if any(type(value) is not bool for value in booleans):
|
||||
raise CanaryContractError("observation boolean field is invalid")
|
||||
numeric = (observation.d04_result, observation.submit_result,
|
||||
observation.submit_errno)
|
||||
if any(type(value) is not int or not RETURN_MIN <= value <= RETURN_MAX
|
||||
for value in numeric):
|
||||
raise CanaryContractError("observation numeric field is invalid")
|
||||
counts = (observation.submit_sequence, observation.d04_sequence,
|
||||
observation.terminal_sequence,
|
||||
observation.retry_count, observation.persistent_write_count)
|
||||
if any(type(value) is not int or value < 0 for value in counts):
|
||||
raise CanaryContractError("observation count is invalid")
|
||||
if observation.retry_count != 0 or observation.persistent_write_count != 0:
|
||||
raise CanaryContractError("observation reports forbidden effects")
|
||||
if observation.visible_output_observed:
|
||||
raise CanaryContractError("unscoped visible-output claim is forbidden")
|
||||
complete = observation.d04_seen and observation.submit_seen \
|
||||
and observation.terminal_seen and observation.cleanup_complete \
|
||||
and observation.submit_sequence < observation.d04_sequence \
|
||||
and observation.d04_sequence < observation.terminal_sequence
|
||||
return complete
|
||||
|
||||
|
||||
def classify_pair(plan: CanaryPairPlan,
|
||||
observations: tuple[CanaryObservation, CanaryObservation]
|
||||
) -> CanaryClassification:
|
||||
"""Classify supplied synthetic results without interpreting VideoOut ABI."""
|
||||
validate_plan(plan)
|
||||
if type(observations) is not tuple or len(observations) != 2:
|
||||
raise CanaryContractError("observation pair is invalid")
|
||||
complete = tuple(_validate_observation(plan, arm, observation)
|
||||
for arm, observation in zip(plan.arms, observations))
|
||||
if not all(complete):
|
||||
return CanaryClassification("INCOMPLETE_NO_CAUSAL_COMPARISON",
|
||||
False, False, False)
|
||||
differs = observations[0].submit_result != observations[1].submit_result
|
||||
if not differs:
|
||||
return CanaryClassification("NO_SUBMIT_RETURN_DIFFERENCE",
|
||||
True, False, False)
|
||||
return CanaryClassification(
|
||||
"LAUNCH_CONTEXT_SUBMIT_RETURN_DIFFERENCE_CANDIDATE_ONLY",
|
||||
True, True, True)
|
||||
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Host-only CHD10AV1 frame and cleanup-terminal reference model."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
|
||||
MAGIC = b"CHD10AV1"
|
||||
VERSION = 1
|
||||
FRAME_SIZE = 64
|
||||
D04 = 4
|
||||
D07 = 7
|
||||
D12 = 12
|
||||
D14 = 30
|
||||
KIND_RAW = 1
|
||||
KIND_PAIR = 2
|
||||
RAW0_VALID = 1 << 0
|
||||
RAW1_VALID = 1 << 1
|
||||
TERMINAL = 1 << 2
|
||||
ALLOWED_FLAGS = RAW0_VALID | RAW1_VALID | TERMINAL
|
||||
S15_COMPLETE = 15
|
||||
UINT32_MAX = (1 << 32) - 1
|
||||
INT32_MIN = -(1 << 31)
|
||||
INT32_MAX = (1 << 31) - 1
|
||||
|
||||
|
||||
class CanaryProtocolError(ValueError):
|
||||
"""A synthetic AV frame or cleanup state is invalid."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Frame:
|
||||
sequence: int
|
||||
stage: int
|
||||
kind: int
|
||||
flags: int
|
||||
raw0: int
|
||||
raw1: int
|
||||
result: int
|
||||
aux0: int
|
||||
aux1: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CleanupSnapshot:
|
||||
rarch_main_returned: bool
|
||||
d04_emitted: bool
|
||||
phase: int
|
||||
initialized_mask: int
|
||||
cleaned_mask: int
|
||||
cleanup_order_errors: int
|
||||
cleanup_failure_count: int
|
||||
rarch_main_result: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TraceResult:
|
||||
complete: bool
|
||||
submit_result: int
|
||||
sdl_result: int
|
||||
rarch_main_result: int
|
||||
cleaned_mask: int
|
||||
visible_output_proven: bool = False
|
||||
firmware_behavior_proven: bool = False
|
||||
device_action_authorized: bool = False
|
||||
|
||||
|
||||
def _u32(value: int, label: str) -> int:
|
||||
if type(value) is not int or not 0 <= value <= UINT32_MAX:
|
||||
raise CanaryProtocolError(f"{label} is not uint32")
|
||||
return value
|
||||
|
||||
|
||||
def _i32(value: int, label: str) -> int:
|
||||
if type(value) is not int or not INT32_MIN <= value <= INT32_MAX:
|
||||
raise CanaryProtocolError(f"{label} is not int32")
|
||||
return value
|
||||
|
||||
|
||||
def encode_frame(frame: Frame) -> bytes:
|
||||
"""Encode one exact frame; AV has exactly one possible terminal stage."""
|
||||
if type(frame) is not Frame:
|
||||
raise CanaryProtocolError("frame type is invalid")
|
||||
_u32(frame.sequence, "sequence")
|
||||
if frame.sequence == 0 or type(frame.stage) is not int \
|
||||
or not 0 <= frame.stage <= D14 or frame.kind not in {KIND_RAW, KIND_PAIR}:
|
||||
raise CanaryProtocolError("frame identity is invalid")
|
||||
if type(frame.flags) is not int or frame.flags & ~ALLOWED_FLAGS:
|
||||
raise CanaryProtocolError("frame flags are invalid")
|
||||
if frame.stage == D14:
|
||||
if frame.kind != KIND_PAIR or frame.flags != (RAW0_VALID | RAW1_VALID | TERMINAL):
|
||||
raise CanaryProtocolError("D14 is not the exact terminal frame")
|
||||
elif frame.flags & TERMINAL:
|
||||
raise CanaryProtocolError("only D14 may be terminal")
|
||||
for value, label in ((frame.raw0, "raw0"), (frame.raw1, "raw1"),
|
||||
(frame.result, "result")):
|
||||
_i32(value, label)
|
||||
_u32(frame.aux0, "aux0")
|
||||
_u32(frame.aux1, "aux1")
|
||||
output = bytearray(FRAME_SIZE)
|
||||
output[:8] = MAGIC
|
||||
struct.pack_into(">HHIBBHiiiII", output, 8, VERSION, FRAME_SIZE,
|
||||
frame.sequence, frame.stage, frame.kind, frame.flags,
|
||||
frame.raw0, frame.raw1, frame.result,
|
||||
frame.aux0, frame.aux1)
|
||||
struct.pack_into(">I", output, 60, zlib.crc32(output[:60]) & UINT32_MAX)
|
||||
return bytes(output)
|
||||
|
||||
|
||||
def parse_frame(raw: bytes) -> Frame:
|
||||
if type(raw) is not bytes or len(raw) != FRAME_SIZE or raw[:8] != MAGIC:
|
||||
raise CanaryProtocolError("frame envelope is invalid")
|
||||
if any(raw[40:60]):
|
||||
raise CanaryProtocolError("reserved frame bytes are nonzero")
|
||||
expected = zlib.crc32(raw[:60]) & UINT32_MAX
|
||||
if struct.unpack_from(">I", raw, 60)[0] != expected:
|
||||
raise CanaryProtocolError("frame CRC differs")
|
||||
version, size, sequence, stage, kind, flags, raw0, raw1, result, aux0, aux1 = \
|
||||
struct.unpack_from(">HHIBBHiiiII", raw, 8)
|
||||
if version != VERSION or size != FRAME_SIZE:
|
||||
raise CanaryProtocolError("frame version or size differs")
|
||||
frame = Frame(sequence, stage, kind, flags, raw0, raw1, result, aux0, aux1)
|
||||
if encode_frame(frame) != raw:
|
||||
raise CanaryProtocolError("frame is not canonical")
|
||||
return frame
|
||||
|
||||
|
||||
def build_cleanup_terminal(sequence: int, snapshot: CleanupSnapshot) -> bytes | None:
|
||||
"""Return D14 only when the modeled lifecycle is completely closed."""
|
||||
if type(snapshot) is not CleanupSnapshot:
|
||||
raise CanaryProtocolError("cleanup snapshot type is invalid")
|
||||
booleans = (snapshot.rarch_main_returned, snapshot.d04_emitted)
|
||||
if any(type(value) is not bool for value in booleans):
|
||||
raise CanaryProtocolError("cleanup booleans are invalid")
|
||||
for value, label in ((snapshot.phase, "phase"),
|
||||
(snapshot.initialized_mask, "initialized mask"),
|
||||
(snapshot.cleaned_mask, "cleaned mask"),
|
||||
(snapshot.cleanup_order_errors, "cleanup order errors"),
|
||||
(snapshot.cleanup_failure_count, "cleanup failure count")):
|
||||
_u32(value, label)
|
||||
_i32(snapshot.rarch_main_result, "rarch_main result")
|
||||
complete = snapshot.rarch_main_returned and snapshot.d04_emitted \
|
||||
and snapshot.phase == S15_COMPLETE and snapshot.initialized_mask == 0 \
|
||||
and snapshot.cleanup_order_errors == 0 \
|
||||
and snapshot.cleanup_failure_count == 0
|
||||
if not complete:
|
||||
return None
|
||||
return encode_frame(Frame(sequence, D14, KIND_PAIR,
|
||||
RAW0_VALID | RAW1_VALID | TERMINAL,
|
||||
snapshot.initialized_mask, snapshot.cleaned_mask,
|
||||
snapshot.rarch_main_result,
|
||||
snapshot.cleanup_order_errors,
|
||||
snapshot.cleanup_failure_count))
|
||||
|
||||
|
||||
def validate_trace(raw_frames: tuple[bytes, ...]) -> TraceResult:
|
||||
"""Require one ordered D07/D04/D14 path and no bytes after terminal."""
|
||||
if type(raw_frames) is not tuple or not raw_frames:
|
||||
raise CanaryProtocolError("trace is empty or not immutable")
|
||||
frames = tuple(parse_frame(raw) for raw in raw_frames)
|
||||
if any(right.sequence <= left.sequence for left, right in zip(frames, frames[1:])):
|
||||
raise CanaryProtocolError("frame sequence is not strictly increasing")
|
||||
terminal_indexes = [index for index, frame in enumerate(frames)
|
||||
if frame.flags & TERMINAL]
|
||||
if terminal_indexes != [len(frames) - 1]:
|
||||
raise CanaryProtocolError("terminal is absent, duplicated or not final")
|
||||
for stage in (D07, D04, D14):
|
||||
if sum(frame.stage == stage for frame in frames) != 1:
|
||||
raise CanaryProtocolError("required stage is absent or duplicated")
|
||||
if sum(frame.stage == D12 for frame in frames) > 1:
|
||||
raise CanaryProtocolError("D12 is duplicated")
|
||||
positions = {frame.stage: index for index, frame in enumerate(frames)
|
||||
if frame.stage in {D07, D04, D14}}
|
||||
if not positions[D07] < positions[D04] < positions[D14]:
|
||||
raise CanaryProtocolError("submit/D04/D14 order differs")
|
||||
terminal = frames[-1]
|
||||
if terminal.raw0 != 0 or terminal.aux0 != 0 or terminal.aux1 != 0:
|
||||
raise CanaryProtocolError("D14 cleanup predicate differs")
|
||||
submit = frames[positions[D07]]
|
||||
sdl = frames[positions[D04]]
|
||||
if any(frame.kind != KIND_RAW or frame.flags != RAW0_VALID
|
||||
for frame in (submit, sdl)):
|
||||
raise CanaryProtocolError("D07 or D04 semantics differ")
|
||||
return TraceResult(True, submit.raw0, sdl.raw0, terminal.result,
|
||||
terminal.raw1)
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Pure, inactive Phase-1.0DC BigApp comparison-gate contract."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
import re
|
||||
|
||||
|
||||
PHASE = "PHASE_1_0DC_INACTIVE_BIGAPP_COMPARISON_GATE"
|
||||
FIRMWARE = "9.60"
|
||||
TITLE_ID = "PPSA01659"
|
||||
PAYLOAD_NAME = "retroarch_ps5_launch_canary.elf"
|
||||
PAYLOAD_SIZE = 1845240
|
||||
PAYLOAD_SHA256 = "8dadce9d9faaef21ea129a3d216c768eea9a3ca9bf8ecb8d852e376b58a9bf95"
|
||||
PROTOCOL_MAGIC = "CHD10AV1"
|
||||
PROTOCOL_TERMINAL = "D14"
|
||||
MAX_WINDOW_SECONDS = 300
|
||||
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
||||
RUN_ID = re.compile(r"^[A-Z0-9][A-Z0-9_-]{7,63}$")
|
||||
|
||||
|
||||
class BigAppGateError(ValueError):
|
||||
"""Candidate data is incomplete, ambiguous, or unsafe."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BigAppGateRecord:
|
||||
phase: str
|
||||
active: bool
|
||||
firmware: str | None
|
||||
title_id: str | None
|
||||
run_id: str | None
|
||||
not_before: str | None
|
||||
expires_at: str | None
|
||||
launcher_name: str | None
|
||||
launcher_size: int | None
|
||||
launcher_sha256: str | None
|
||||
payload_name: str | None
|
||||
payload_size: int | None
|
||||
payload_sha256: str | None
|
||||
approval_sha256: str | None
|
||||
result_magic: str | None
|
||||
result_terminal: str | None
|
||||
no_running_bigapp_attested: bool
|
||||
kernel_ptrace_effects_accepted: bool
|
||||
app_termination_authorized: bool
|
||||
persistent_write_authorized: bool
|
||||
system_remount_authorized: bool
|
||||
installation_authorized: bool
|
||||
autoload_authorized: bool
|
||||
automatic_retry: bool
|
||||
reconnect: bool
|
||||
fallback_title: bool
|
||||
bounded_parent_detach_proven: bool
|
||||
bounded_child_cleanup_proven: bool
|
||||
bounded_result_channel_proven: bool
|
||||
|
||||
|
||||
def _utc(value: str | None) -> datetime:
|
||||
if not isinstance(value, str) or not value.endswith("Z"):
|
||||
raise BigAppGateError("timestamp must be exact UTC")
|
||||
try:
|
||||
result = datetime.fromisoformat(value[:-1] + "+00:00")
|
||||
except ValueError as error:
|
||||
raise BigAppGateError("timestamp is invalid") from error
|
||||
if result.tzinfo != timezone.utc:
|
||||
raise BigAppGateError("timestamp is not UTC")
|
||||
return result
|
||||
|
||||
|
||||
def validate_inactive(record: BigAppGateRecord) -> None:
|
||||
"""Require the tracked record to contain no live identity or authority."""
|
||||
if type(record) is not BigAppGateRecord or record.phase != PHASE or record.active:
|
||||
raise BigAppGateError("tracked gate is not inert")
|
||||
identities = (
|
||||
record.firmware, record.title_id, record.run_id, record.not_before,
|
||||
record.expires_at, record.launcher_name, record.launcher_size,
|
||||
record.launcher_sha256, record.payload_name, record.payload_size,
|
||||
record.payload_sha256, record.approval_sha256, record.result_magic,
|
||||
record.result_terminal,
|
||||
)
|
||||
if any(value is not None for value in identities):
|
||||
raise BigAppGateError("inactive gate contains an identity")
|
||||
if any((record.no_running_bigapp_attested,
|
||||
record.kernel_ptrace_effects_accepted,
|
||||
record.app_termination_authorized,
|
||||
record.persistent_write_authorized,
|
||||
record.system_remount_authorized,
|
||||
record.installation_authorized, record.autoload_authorized,
|
||||
record.automatic_retry, record.reconnect, record.fallback_title,
|
||||
record.bounded_parent_detach_proven,
|
||||
record.bounded_child_cleanup_proven,
|
||||
record.bounded_result_channel_proven)):
|
||||
raise BigAppGateError("inactive gate contains authority or proof")
|
||||
|
||||
|
||||
def validate_candidate(record: BigAppGateRecord) -> None:
|
||||
"""Validate hypothetical data; this function grants no authorization."""
|
||||
if type(record) is not BigAppGateRecord or record.phase != PHASE or not record.active:
|
||||
raise BigAppGateError("candidate is not explicitly active")
|
||||
if record.firmware != FIRMWARE or record.title_id != TITLE_ID:
|
||||
raise BigAppGateError("firmware or fixed title mismatch")
|
||||
if not isinstance(record.run_id, str) or not RUN_ID.fullmatch(record.run_id):
|
||||
raise BigAppGateError("run identity is invalid")
|
||||
start, end = _utc(record.not_before), _utc(record.expires_at)
|
||||
if not 0 < (end - start).total_seconds() <= MAX_WINDOW_SECONDS:
|
||||
raise BigAppGateError("activation window is invalid")
|
||||
if record.launcher_name != "chimera_bigapp_canary_launcher.elf" or \
|
||||
not isinstance(record.launcher_size, int) or isinstance(record.launcher_size, bool) or \
|
||||
not 1 <= record.launcher_size <= 1048576 or \
|
||||
not isinstance(record.launcher_sha256, str) or not SHA256.fullmatch(record.launcher_sha256):
|
||||
raise BigAppGateError("launcher identity is invalid")
|
||||
if (record.payload_name, record.payload_size, record.payload_sha256) != \
|
||||
(PAYLOAD_NAME, PAYLOAD_SIZE, PAYLOAD_SHA256):
|
||||
raise BigAppGateError("payload identity is not the unchanged CZ artifact")
|
||||
if not isinstance(record.approval_sha256, str) or not SHA256.fullmatch(record.approval_sha256):
|
||||
raise BigAppGateError("separate approval identity is invalid")
|
||||
if (record.result_magic, record.result_terminal) != \
|
||||
(PROTOCOL_MAGIC, PROTOCOL_TERMINAL):
|
||||
raise BigAppGateError("result protocol is invalid")
|
||||
required = (record.no_running_bigapp_attested,
|
||||
record.kernel_ptrace_effects_accepted,
|
||||
record.bounded_parent_detach_proven,
|
||||
record.bounded_child_cleanup_proven,
|
||||
record.bounded_result_channel_proven)
|
||||
if any(value is not True for value in required):
|
||||
raise BigAppGateError("required attestation or bounded proof is absent")
|
||||
forbidden = (record.app_termination_authorized,
|
||||
record.persistent_write_authorized,
|
||||
record.system_remount_authorized,
|
||||
record.installation_authorized, record.autoload_authorized,
|
||||
record.automatic_retry, record.reconnect,
|
||||
record.fallback_title)
|
||||
if any(value is not False for value in forbidden):
|
||||
raise BigAppGateError("candidate permits a forbidden effect")
|
||||
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Pure, inactive title-presence observer contract for Phase 1.0DF."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import re
|
||||
|
||||
|
||||
PHASE = "PHASE_1_0DF_INACTIVE_TITLE_PRESENCE_OBSERVER"
|
||||
FIRMWARE = "9.60"
|
||||
TITLE_ID = "PPSA01659"
|
||||
MAX_RESULT_BYTES = 4096
|
||||
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
||||
|
||||
|
||||
class ObserverError(ValueError):
|
||||
"""An observer plan or result is ambiguous or unsafe."""
|
||||
|
||||
|
||||
class Outcome(Enum):
|
||||
PRESENT = "PRESENT"
|
||||
ABSENT = "ABSENT"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ObserverPlan:
|
||||
phase: str
|
||||
active: bool
|
||||
firmware: str | None
|
||||
title_id: str | None
|
||||
method: str | None
|
||||
query_contract_sha256: str | None
|
||||
exact_literal_path: str | None
|
||||
path_provenance_sha256: str | None
|
||||
request_bytes_sha256: str | None
|
||||
maximum_result_bytes: int | None
|
||||
one_request: bool
|
||||
read_only: bool
|
||||
shell_present: bool
|
||||
directory_enumeration: bool
|
||||
title_launch: bool
|
||||
app_termination: bool
|
||||
device_write: bool
|
||||
retry: bool
|
||||
reconnect: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SyntheticResult:
|
||||
firmware: str
|
||||
title_id: str
|
||||
method: str
|
||||
request_bytes_sha256: str
|
||||
complete: bool
|
||||
result_bytes: int
|
||||
explicit_present: bool
|
||||
explicit_absent: bool
|
||||
error_code: int | None
|
||||
|
||||
|
||||
def validate_inactive(plan: ObserverPlan) -> None:
|
||||
"""Require the tracked plan to be empty and incapable of a request."""
|
||||
if type(plan) is not ObserverPlan or plan.phase != PHASE or plan.active:
|
||||
raise ObserverError("tracked observer is not inactive")
|
||||
optional = (plan.firmware, plan.title_id, plan.method,
|
||||
plan.query_contract_sha256, plan.exact_literal_path,
|
||||
plan.path_provenance_sha256, plan.request_bytes_sha256,
|
||||
plan.maximum_result_bytes)
|
||||
if any(value is not None for value in optional):
|
||||
raise ObserverError("inactive observer contains request data")
|
||||
if any((plan.one_request, plan.read_only, plan.shell_present,
|
||||
plan.directory_enumeration, plan.title_launch,
|
||||
plan.app_termination, plan.device_write, plan.retry,
|
||||
plan.reconnect)):
|
||||
raise ObserverError("inactive observer contains capability")
|
||||
|
||||
|
||||
def validate_candidate(plan: ObserverPlan) -> None:
|
||||
"""Validate hypothetical data without creating or authorizing a request."""
|
||||
if type(plan) is not ObserverPlan or plan.phase != PHASE or not plan.active:
|
||||
raise ObserverError("candidate is not active data")
|
||||
if plan.firmware != FIRMWARE or plan.title_id != TITLE_ID:
|
||||
raise ObserverError("firmware or title mismatch")
|
||||
if plan.method not in {"SOURCE_BOUND_QUERY", "EXACT_PATH_METADATA"}:
|
||||
raise ObserverError("observer method is not allowlisted")
|
||||
if not isinstance(plan.query_contract_sha256, str) or \
|
||||
not SHA256.fullmatch(plan.query_contract_sha256):
|
||||
raise ObserverError("query contract identity is absent")
|
||||
if not isinstance(plan.request_bytes_sha256, str) or \
|
||||
not SHA256.fullmatch(plan.request_bytes_sha256):
|
||||
raise ObserverError("request bytes are not bound")
|
||||
if plan.maximum_result_bytes != MAX_RESULT_BYTES:
|
||||
raise ObserverError("result bound is not exact")
|
||||
if plan.method == "SOURCE_BOUND_QUERY":
|
||||
if plan.exact_literal_path is not None or \
|
||||
plan.path_provenance_sha256 is not None:
|
||||
raise ObserverError("query method must not carry a path")
|
||||
else:
|
||||
if not isinstance(plan.exact_literal_path, str) or \
|
||||
not plan.exact_literal_path.startswith("/") or \
|
||||
".." in plan.exact_literal_path or \
|
||||
not isinstance(plan.path_provenance_sha256, str) or \
|
||||
not SHA256.fullmatch(plan.path_provenance_sha256):
|
||||
raise ObserverError("exact path provenance is absent")
|
||||
if plan.one_request is not True or plan.read_only is not True:
|
||||
raise ObserverError("one read-only request is not exact")
|
||||
if any((plan.shell_present, plan.directory_enumeration,
|
||||
plan.title_launch, plan.app_termination, plan.device_write,
|
||||
plan.retry, plan.reconnect)):
|
||||
raise ObserverError("candidate contains a forbidden capability")
|
||||
|
||||
|
||||
def classify_synthetic(plan: ObserverPlan, result: SyntheticResult) -> Outcome:
|
||||
"""Classify supplied bytes; errors and incomplete data remain UNKNOWN."""
|
||||
validate_candidate(plan)
|
||||
if type(result) is not SyntheticResult or \
|
||||
(result.firmware, result.title_id, result.method,
|
||||
result.request_bytes_sha256) != \
|
||||
(plan.firmware, plan.title_id, plan.method,
|
||||
plan.request_bytes_sha256):
|
||||
raise ObserverError("result binding mismatch")
|
||||
if result.result_bytes < 0 or result.result_bytes > MAX_RESULT_BYTES:
|
||||
raise ObserverError("result bound exceeded")
|
||||
if result.explicit_present and result.explicit_absent:
|
||||
raise ObserverError("contradictory result")
|
||||
if not result.complete or result.error_code is not None:
|
||||
return Outcome.UNKNOWN
|
||||
if result.explicit_present:
|
||||
return Outcome.PRESENT
|
||||
if result.explicit_absent:
|
||||
return Outcome.ABSENT
|
||||
return Outcome.UNKNOWN
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Hash-bound, offline-only appinfo snapshot query for Phase 1.0DH."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sqlite3
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
PHASE = "PHASE_1_0DH_HASH_BOUND_SNAPSHOT_QUERY"
|
||||
TITLE_ID = "PPSA01659"
|
||||
MAX_SNAPSHOT_BYTES = 64 * 1024 * 1024
|
||||
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
||||
|
||||
|
||||
class SnapshotError(ValueError):
|
||||
"""The supplied snapshot or query contract is invalid."""
|
||||
|
||||
|
||||
class Outcome(Enum):
|
||||
PRESENT = "PRESENT"
|
||||
ABSENT = "ABSENT"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SnapshotBinding:
|
||||
phase: str
|
||||
title_id: str
|
||||
size: int
|
||||
sha256: str
|
||||
|
||||
|
||||
def _digest(path: Path) -> tuple[int, str]:
|
||||
total = 0
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while block := stream.read(64 * 1024):
|
||||
total += len(block)
|
||||
if total > MAX_SNAPSHOT_BYTES:
|
||||
raise SnapshotError("snapshot exceeds size ceiling")
|
||||
digest.update(block)
|
||||
return total, digest.hexdigest()
|
||||
|
||||
|
||||
def validate_binding(binding: SnapshotBinding) -> None:
|
||||
if type(binding) is not SnapshotBinding or binding.phase != PHASE:
|
||||
raise SnapshotError("phase binding mismatch")
|
||||
if binding.title_id != TITLE_ID:
|
||||
raise SnapshotError("title binding mismatch")
|
||||
if not isinstance(binding.size, int) or isinstance(binding.size, bool) or \
|
||||
binding.size <= 0 or binding.size > MAX_SNAPSHOT_BYTES:
|
||||
raise SnapshotError("snapshot size is invalid")
|
||||
if not isinstance(binding.sha256, str) or not SHA256.fullmatch(binding.sha256):
|
||||
raise SnapshotError("snapshot hash is invalid")
|
||||
|
||||
|
||||
def query_snapshot(path: Path, binding: SnapshotBinding) -> Outcome:
|
||||
"""Query a byte-exact local snapshot without creating SQLite sidecars."""
|
||||
validate_binding(binding)
|
||||
path = path.resolve(strict=True)
|
||||
if not path.is_file() or _digest(path) != (binding.size, binding.sha256):
|
||||
raise SnapshotError("snapshot identity mismatch")
|
||||
if path.read_bytes()[:16] != b"SQLite format 3\x00":
|
||||
raise SnapshotError("snapshot is not SQLite 3")
|
||||
|
||||
uri = f"file:{quote(path.as_posix(), safe='/:')}?mode=ro&immutable=1"
|
||||
try:
|
||||
connection = sqlite3.connect(uri, uri=True)
|
||||
try:
|
||||
connection.execute("PRAGMA query_only = ON")
|
||||
columns = {
|
||||
row[1] for row in connection.execute(
|
||||
"PRAGMA table_info(tbl_appinfo)")
|
||||
}
|
||||
if "titleId" not in columns:
|
||||
return Outcome.UNKNOWN
|
||||
rows = connection.execute(
|
||||
"SELECT 1 FROM tbl_appinfo WHERE titleId = ? LIMIT 2",
|
||||
(TITLE_ID,),
|
||||
).fetchall()
|
||||
if len(rows) == 1:
|
||||
return Outcome.PRESENT
|
||||
if len(rows) == 0:
|
||||
return Outcome.ABSENT
|
||||
return Outcome.UNKNOWN
|
||||
finally:
|
||||
connection.close()
|
||||
except sqlite3.Error:
|
||||
return Outcome.UNKNOWN
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Offline parser for the bounded Phase-1.0DM snapshot stream."""
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import hashlib,struct
|
||||
|
||||
MAGIC=b"CHS10DM1";FRAME=struct.Struct("<8sIIiiQQQQq");MAX_BYTES=64*1024*1024
|
||||
BEGIN=1;END=2;ERROR=3
|
||||
class ProtocolError(ValueError):pass
|
||||
@dataclass(frozen=True)
|
||||
class SnapshotResult:
|
||||
data:bytes
|
||||
sha256:str
|
||||
size:int
|
||||
device:int
|
||||
inode:int
|
||||
mtime:int
|
||||
|
||||
def _frame(data:bytes):
|
||||
if len(data)!=64:raise ProtocolError("frame size")
|
||||
values=FRAME.unpack(data)
|
||||
if values[0]!=MAGIC or values[1]!=1:raise ProtocolError("frame identity")
|
||||
return values
|
||||
|
||||
def parse_stream(raw:bytes)->SnapshotResult:
|
||||
if not isinstance(raw,bytes) or len(raw)<128:raise ProtocolError("stream truncated")
|
||||
begin=_frame(raw[:64]);kind,status,saved,size,sent,dev,ino,mtime=begin[2:]
|
||||
if kind==ERROR:raise ProtocolError(f"target error {status}")
|
||||
if kind!=BEGIN or status!=0 or saved!=0 or sent!=0 or not 0<size<=MAX_BYTES:raise ProtocolError("begin invalid")
|
||||
if len(raw)!=64+size+64:raise ProtocolError("stream length")
|
||||
payload=raw[64:64+size];end=_frame(raw[64+size:]);ekind,estatus,esaved,esize,esent,edev,eino,emtime=end[2:]
|
||||
if ekind!=END or estatus!=0 or esaved!=0 or esize!=size or esent!=size or (edev,eino,emtime)!=(dev,ino,mtime):raise ProtocolError("terminal invalid")
|
||||
if payload[:16]!=b"SQLite format 3\x00":raise ProtocolError("not sqlite")
|
||||
return SnapshotResult(payload,hashlib.sha256(payload).hexdigest(),size,dev,ino,mtime)
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Network-free streaming receiver for one Phase-1.0DM result."""
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import hashlib,os
|
||||
from pathlib import Path
|
||||
from phase10dm_snapshot_protocol import BEGIN,END,FRAME,MAGIC,MAX_BYTES,ProtocolError,_frame
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReceivedSnapshot:
|
||||
path:Path;size:int;sha256:str;device:int;inode:int;mtime:int
|
||||
|
||||
class SnapshotReceiver:
|
||||
def __init__(self,path:Path):
|
||||
if not isinstance(path,Path) or path.exists():raise ProtocolError("exclusive output required")
|
||||
self.path=path;self.stream=path.open("xb");self.buffer=bytearray();self.state="HEADER";self.expected=None;self.written=0;self.digest=hashlib.sha256();self.begin=None
|
||||
def feed(self,chunk:bytes):
|
||||
if self.state not in {"HEADER","PAYLOAD","TERMINAL"} or not isinstance(chunk,bytes):raise ProtocolError("receiver state")
|
||||
self.buffer.extend(chunk)
|
||||
if self.state=="HEADER" and len(self.buffer)>=64:
|
||||
self.begin=_frame(bytes(self.buffer[:64]));del self.buffer[:64];kind,status,saved,size,sent,*_=self.begin[2:]
|
||||
if kind!=BEGIN or status or saved or sent or not 0<size<=MAX_BYTES:self.abort();raise ProtocolError("begin invalid")
|
||||
self.expected=size;self.state="PAYLOAD"
|
||||
if self.state=="PAYLOAD":
|
||||
take=min(len(self.buffer),self.expected-self.written)
|
||||
if take:
|
||||
data=bytes(self.buffer[:take]);del self.buffer[:take];self.stream.write(data);self.digest.update(data);self.written+=take
|
||||
if self.written==self.expected:self.state="TERMINAL"
|
||||
if self.state=="TERMINAL" and len(self.buffer)>64:self.abort();raise ProtocolError("trailing bytes")
|
||||
def finish(self)->ReceivedSnapshot:
|
||||
if self.state!="TERMINAL" or len(self.buffer)!=64 or self.begin is None:self.abort();raise ProtocolError("stream incomplete")
|
||||
end=_frame(bytes(self.buffer));kind,status,saved,size,sent,dev,ino,mtime=end[2:];_,_,_,bsize,_,bdev,bino,bmtime=self.begin[2:]
|
||||
if kind!=END or status or saved or size!=bsize or sent!=bsize or (dev,ino,mtime)!=(bdev,bino,bmtime):self.abort();raise ProtocolError("terminal invalid")
|
||||
self.stream.flush();os.fsync(self.stream.fileno());self.stream.close();self.state="SEALED"
|
||||
with self.path.open("rb") as check:
|
||||
if check.read(16)!=b"SQLite format 3\x00":raise ProtocolError("not sqlite")
|
||||
return ReceivedSnapshot(self.path,self.written,self.digest.hexdigest(),dev,ino,mtime)
|
||||
def abort(self):
|
||||
if not self.stream.closed:self.stream.flush();self.stream.close()
|
||||
self.state="ABORTED"
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Manifest-gated one-shot raw-elfldr snapshot runner."""
|
||||
from __future__ import annotations
|
||||
import hashlib, ipaddress, json, os, socket, time
|
||||
from pathlib import Path
|
||||
from phase10dn_snapshot_receiver import SnapshotReceiver
|
||||
|
||||
ARTIFACT_SIZE=109896
|
||||
ARTIFACT_SHA256="147b5bede0f0b5b7d2be903bc72ff0d0541a2cdc28eae7d86b6bf95e1978ebdf"
|
||||
PORT=9021
|
||||
CONNECT_TIMEOUT=3.0
|
||||
TOTAL_TIMEOUT=30.0
|
||||
MAX_WIRE=67108992
|
||||
FIELDS={"active","run_id","target","port","artifact_size","artifact_sha256","snapshot_path","receipt_path","not_before","not_after","one_connection","one_transfer","one_execution","result_receive","target_file_read","device_write","installation","autoload","retry","reconnect"}
|
||||
|
||||
class RunnerError(RuntimeError):
|
||||
pass
|
||||
|
||||
def _load(path: Path) -> dict:
|
||||
value=json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value,dict): raise RunnerError("record is not an object")
|
||||
return value
|
||||
|
||||
def validate_records(activation: dict,approval: dict,now: float)->dict:
|
||||
if set(activation)!=FIELDS or set(approval)!=FIELDS: raise RunnerError("record fields differ")
|
||||
if activation!=approval: raise RunnerError("activation and approval differ")
|
||||
if activation["active"] is not True: raise RunnerError("activation is inactive")
|
||||
if not activation["not_before"]<=now<=activation["not_after"]: raise RunnerError("approval window inactive")
|
||||
if activation["port"]!=PORT or activation["artifact_size"]!=ARTIFACT_SIZE or activation["artifact_sha256"]!=ARTIFACT_SHA256: raise RunnerError("artifact route mismatch")
|
||||
if any(activation[k] is not True for k in ("one_connection","one_transfer","one_execution","result_receive","target_file_read")): raise RunnerError("required authority absent")
|
||||
if any(activation[k] is not False for k in ("device_write","installation","autoload","retry","reconnect")): raise RunnerError("forbidden authority present")
|
||||
ipaddress.IPv4Address(activation["target"])
|
||||
for key in ("snapshot_path","receipt_path"):
|
||||
path=Path(activation[key])
|
||||
if not path.is_absolute() or path.exists(): raise RunnerError("exclusive absolute evidence path required")
|
||||
return activation
|
||||
|
||||
def _consume(plan: dict)->None:
|
||||
path=Path(plan["receipt_path"])
|
||||
with path.open("x",encoding="utf-8",newline="\n") as out:
|
||||
json.dump({"run_id":plan["run_id"],"artifact_sha256":ARTIFACT_SHA256,"consumed_before_socket":True},out,sort_keys=True)
|
||||
out.write("\n");out.flush();os.fsync(out.fileno())
|
||||
|
||||
def run(artifact:Path,activation_path:Path,approval_path:Path,socket_factory=socket.socket,clock=time.monotonic,wall_clock=time.time):
|
||||
plan=validate_records(_load(activation_path),_load(approval_path),wall_clock())
|
||||
raw=artifact.read_bytes()
|
||||
if len(raw)!=ARTIFACT_SIZE or hashlib.sha256(raw).hexdigest()!=ARTIFACT_SHA256: raise RunnerError("artifact identity mismatch")
|
||||
_consume(plan)
|
||||
receiver=SnapshotReceiver(Path(plan["snapshot_path"]));sock=None;received=0;deadline=clock()+TOTAL_TIMEOUT
|
||||
try:
|
||||
sock=socket_factory(socket.AF_INET,socket.SOCK_STREAM);sock.settimeout(CONNECT_TIMEOUT);sock.connect((plan["target"],PORT));sock.sendall(raw);sock.shutdown(socket.SHUT_WR)
|
||||
while True:
|
||||
remain=deadline-clock()
|
||||
if remain<=0: raise RunnerError("result deadline")
|
||||
sock.settimeout(remain);chunk=sock.recv(min(65536,MAX_WIRE-received))
|
||||
if not chunk: break
|
||||
received+=len(chunk)
|
||||
if received>MAX_WIRE: raise RunnerError("wire bound")
|
||||
receiver.feed(chunk)
|
||||
return receiver.finish()
|
||||
except Exception:
|
||||
receiver.abort();raise
|
||||
finally:
|
||||
if sock is not None: sock.close()
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Strict parser for the bounded Phase-1.0DQ inventory stream."""
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import struct
|
||||
|
||||
MAGIC=b"CHI10DQ1";FRAME=struct.Struct("<8sIIiiIIQ24s");ENTRY=struct.Struct("<256sIIIIQQq24s")
|
||||
HEADER=1;TERMINAL=2;ERROR=3;MAX_ENTRIES=256;MAX_WIRE=64+MAX_ENTRIES*320+64
|
||||
class ProtocolError(ValueError):pass
|
||||
@dataclass(frozen=True)
|
||||
class InventoryEntry:name:str;file_type:int;mode:int;size:int;inode:int;mtime:int
|
||||
def _frame(raw:bytes):
|
||||
if len(raw)!=FRAME.size:raise ProtocolError("frame size")
|
||||
value=FRAME.unpack(raw)
|
||||
if value[0]!=MAGIC or value[1]!=1 or value[8]!=bytes(24):raise ProtocolError("frame identity")
|
||||
return value
|
||||
def parse_stream(raw:bytes)->tuple[InventoryEntry,...]:
|
||||
if not isinstance(raw,bytes) or not 128<=len(raw)<=MAX_WIRE or (len(raw)-128)%ENTRY.size:raise ProtocolError("stream length")
|
||||
first=_frame(raw[:64]);kind,status,saved,count,reserved,transferred=first[2:8]
|
||||
if kind==ERROR:raise ProtocolError(f"target error {status}:{saved}")
|
||||
if kind!=HEADER or status or saved or count or reserved or transferred:raise ProtocolError("header invalid")
|
||||
entries=[]
|
||||
for offset in range(64,len(raw)-64,ENTRY.size):
|
||||
name_raw,name_length,file_type,mode,reserved,size,inode,mtime,padding=ENTRY.unpack(raw[offset:offset+ENTRY.size])
|
||||
if not 0<name_length<256 or reserved or padding!=bytes(24) or any(name_raw[name_length:]):raise ProtocolError("entry encoding")
|
||||
try:name=name_raw[:name_length].decode("utf-8")
|
||||
except UnicodeDecodeError as exc:raise ProtocolError("entry utf8") from exc
|
||||
if name in {".",".."} or "/" in name or "\\" in name or "\x00" in name:raise ProtocolError("entry name")
|
||||
entries.append(InventoryEntry(name,file_type,mode,size,inode,mtime))
|
||||
terminal=_frame(raw[-64:]);kind,status,saved,count,reserved,transferred=terminal[2:8]
|
||||
if kind!=TERMINAL or status or saved or reserved or count!=len(entries) or transferred!=len(entries)*ENTRY.size:raise ProtocolError("terminal invalid")
|
||||
return tuple(entries)
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Inactive manifest-gated one-shot runner for Phase-1.0DQ."""
|
||||
from __future__ import annotations
|
||||
import hashlib,ipaddress,json,os,socket,time
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
from phase10dq_inventory_protocol import MAX_WIRE,parse_stream
|
||||
|
||||
ARTIFACT_SIZE=110032;ARTIFACT_SHA256="914fce06a490ad048fdd0a85ae117858e8904b47c72054bf12fbfebc213a6db8";PORT=9021
|
||||
FIELDS={"active","run_id","target","port","artifact_size","artifact_sha256","output_path","receipt_path","not_before","not_after","one_connection","one_transfer","one_execution","result_receive","directory_inventory","possible_atime_effect_acknowledged","device_file_content_read","persistent_device_write","installation","autoload","retry","reconnect"}
|
||||
class RunnerError(RuntimeError):pass
|
||||
def _load(path:Path):
|
||||
value=json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value,dict):raise RunnerError("record object required")
|
||||
return value
|
||||
def validate_records(activation:dict,approval:dict,now:float):
|
||||
if set(activation)!=FIELDS or set(approval)!=FIELDS or activation!=approval:raise RunnerError("record mismatch")
|
||||
if activation["active"] is not True or not activation["not_before"]<=now<=activation["not_after"]:raise RunnerError("inactive window")
|
||||
if activation["port"]!=PORT or activation["artifact_size"]!=ARTIFACT_SIZE or activation["artifact_sha256"]!=ARTIFACT_SHA256:raise RunnerError("route mismatch")
|
||||
if any(activation[k] is not True for k in ("one_connection","one_transfer","one_execution","result_receive","directory_inventory","possible_atime_effect_acknowledged")):raise RunnerError("authority absent")
|
||||
if any(activation[k] is not False for k in ("device_file_content_read","persistent_device_write","installation","autoload","retry","reconnect")):raise RunnerError("forbidden authority")
|
||||
ipaddress.IPv4Address(activation["target"])
|
||||
for key in ("output_path","receipt_path"):
|
||||
p=Path(activation[key])
|
||||
if not p.is_absolute() or p.exists():raise RunnerError("exclusive path required")
|
||||
return activation
|
||||
def _exclusive_json(path:Path,value:dict):
|
||||
with path.open("x",encoding="utf-8",newline="\n") as out:json.dump(value,out,sort_keys=True);out.write("\n");out.flush();os.fsync(out.fileno())
|
||||
def run(artifact:Path,activation_path:Path,approval_path:Path,socket_factory=socket.socket,clock=time.monotonic,wall_clock=time.time):
|
||||
plan=validate_records(_load(activation_path),_load(approval_path),wall_clock());raw=artifact.read_bytes()
|
||||
if len(raw)!=ARTIFACT_SIZE or hashlib.sha256(raw).hexdigest()!=ARTIFACT_SHA256:raise RunnerError("artifact identity")
|
||||
_exclusive_json(Path(plan["receipt_path"]),{"artifact_sha256":ARTIFACT_SHA256,"consumed_before_socket":True,"run_id":plan["run_id"]})
|
||||
sock=None;wire=bytearray();deadline=clock()+20.0
|
||||
try:
|
||||
sock=socket_factory(socket.AF_INET,socket.SOCK_STREAM);sock.settimeout(3.0);sock.connect((plan["target"],PORT));sock.sendall(raw);sock.shutdown(socket.SHUT_WR)
|
||||
while True:
|
||||
remaining=deadline-clock()
|
||||
if remaining<=0:raise RunnerError("result deadline")
|
||||
sock.settimeout(remaining);chunk=sock.recv(min(4096,MAX_WIRE-len(wire)))
|
||||
if not chunk:break
|
||||
wire.extend(chunk)
|
||||
if len(wire)==MAX_WIRE:break
|
||||
entries=parse_stream(bytes(wire));_exclusive_json(Path(plan["output_path"]),{"entries":[asdict(item) for item in entries],"entry_count":len(entries),"path":"/user/app/FAKE00000","run_id":plan["run_id"]});return entries
|
||||
finally:
|
||||
for index in range(len(wire)):wire[index]=0
|
||||
if sock is not None:sock.close()
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import annotations
|
||||
import struct
|
||||
MAGIC=b"CHM10DS1";FRAME=struct.Struct("<8sIIiiIIQ24s");NAMES=("app.pbm","app.json","app.xml","app.crc");MAX_FILE=4096;MAX_WIRE=16768
|
||||
class ProtocolError(ValueError):pass
|
||||
def frame(raw):
|
||||
if len(raw)!=64:raise ProtocolError("frame size")
|
||||
v=FRAME.unpack(raw)
|
||||
if v[0]!=MAGIC or v[1]!=1 or v[8]!=bytes(24):raise ProtocolError("frame identity")
|
||||
return v
|
||||
def parse_stream(raw:bytes)->dict[str,bytes]:
|
||||
if not isinstance(raw,bytes) or not 128<=len(raw)<=MAX_WIRE:raise ProtocolError("stream length")
|
||||
first=frame(raw[:64]);kind,status,saved,index,length,total=first[2:8]
|
||||
if kind==4:raise ProtocolError(f"target error {status}:{saved}")
|
||||
if (kind,status,saved,index,length,total)!=(1,0,0,4,0,0):raise ProtocolError("header invalid")
|
||||
offset=64;files={};transferred=0
|
||||
for expected,name in enumerate(NAMES):
|
||||
if offset+64>len(raw):raise ProtocolError("file frame truncated")
|
||||
value=frame(raw[offset:offset+64]);offset+=64;kind,status,saved,index,length,total=value[2:8]
|
||||
if kind==4:raise ProtocolError(f"target error {status}:{saved}")
|
||||
if kind!=2 or status or saved or index!=expected or length>MAX_FILE or total!=transferred:raise ProtocolError("file frame invalid")
|
||||
if offset+length>len(raw):raise ProtocolError("file truncated")
|
||||
files[name]=raw[offset:offset+length];offset+=length;transferred+=length
|
||||
if offset+64!=len(raw):raise ProtocolError("terminal position")
|
||||
value=frame(raw[offset:]);kind,status,saved,index,length,total=value[2:8]
|
||||
if (kind,status,saved,index,length,total)!=(3,0,0,4,0,transferred):raise ProtocolError("terminal invalid")
|
||||
return files
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import annotations
|
||||
import base64,hashlib,ipaddress,json,os,socket,time
|
||||
from pathlib import Path
|
||||
from phase10ds_metadata_protocol import MAX_WIRE,parse_stream
|
||||
ARTIFACT_SIZE=109928;ARTIFACT_SHA256="077307b98e44f566fa1db82b08cd5e71bd56bd9792826fc7254965fa768c0dc7";PORT=9021
|
||||
FIELDS={"active","run_id","target","port","artifact_size","artifact_sha256","output_path","receipt_path","not_before","not_after","one_connection","one_transfer","one_execution","result_receive","four_exact_metadata_reads","possible_atime_effect_acknowledged","app_pkg_read","backup_read","persistent_device_write","installation","autoload","retry","reconnect"}
|
||||
class RunnerError(RuntimeError):pass
|
||||
def load(path):
|
||||
v=json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(v,dict):raise RunnerError("record object")
|
||||
return v
|
||||
def validate(a,b,now):
|
||||
if set(a)!=FIELDS or set(b)!=FIELDS or a!=b:raise RunnerError("record mismatch")
|
||||
if a["active"] is not True or not a["not_before"]<=now<=a["not_after"]:raise RunnerError("inactive")
|
||||
if a["port"]!=PORT or a["artifact_size"]!=ARTIFACT_SIZE or a["artifact_sha256"]!=ARTIFACT_SHA256:raise RunnerError("identity")
|
||||
if any(a[k] is not True for k in ("one_connection","one_transfer","one_execution","result_receive","four_exact_metadata_reads","possible_atime_effect_acknowledged")):raise RunnerError("authority")
|
||||
if any(a[k] is not False for k in ("app_pkg_read","backup_read","persistent_device_write","installation","autoload","retry","reconnect")):raise RunnerError("forbidden")
|
||||
ipaddress.IPv4Address(a["target"])
|
||||
for key in ("output_path","receipt_path"):
|
||||
p=Path(a[key])
|
||||
if not p.is_absolute() or p.exists():raise RunnerError("exclusive path")
|
||||
return a
|
||||
def exclusive(path,value):
|
||||
with path.open("x",encoding="utf-8",newline="\n") as f:json.dump(value,f,sort_keys=True);f.write("\n");f.flush();os.fsync(f.fileno())
|
||||
def run(artifact,activation,approval,socket_factory=socket.socket,clock=time.monotonic,wall=time.time):
|
||||
p=validate(load(activation),load(approval),wall());raw=artifact.read_bytes()
|
||||
if len(raw)!=ARTIFACT_SIZE or hashlib.sha256(raw).hexdigest()!=ARTIFACT_SHA256:raise RunnerError("artifact")
|
||||
exclusive(Path(p["receipt_path"]),{"artifact_sha256":ARTIFACT_SHA256,"consumed_before_socket":True,"run_id":p["run_id"]});wire=bytearray();s=None;deadline=clock()+20
|
||||
try:
|
||||
s=socket_factory(socket.AF_INET,socket.SOCK_STREAM);s.settimeout(3);s.connect((p["target"],PORT));s.sendall(raw);s.shutdown(socket.SHUT_WR)
|
||||
while True:
|
||||
left=deadline-clock()
|
||||
if left<=0:raise RunnerError("deadline")
|
||||
s.settimeout(left);chunk=s.recv(min(4096,MAX_WIRE-len(wire)))
|
||||
if not chunk:break
|
||||
wire.extend(chunk)
|
||||
if len(wire)==MAX_WIRE:break
|
||||
files=parse_stream(bytes(wire));exclusive(Path(p["output_path"]),{"files":{k:{"base64":base64.b64encode(v).decode(),"sha256":hashlib.sha256(v).hexdigest(),"size":len(v)} for k,v in files.items()},"run_id":p["run_id"]});return files
|
||||
finally:
|
||||
for i in range(len(wire)):wire[i]=0
|
||||
if s is not None:s.close()
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import hashlib,ipaddress,json,os,socket,struct,time
|
||||
from pathlib import Path
|
||||
SIZE=109688;SHA="25b972ac202050ab5a9c1c89651b27e841cdf788d19c93bf75f051c9c8acd403";REC=struct.Struct("<8sIIiiIIQQQqq");PORT=9021
|
||||
FIELDS={"active","run_id","target","port","artifact_size","artifact_sha256","output_path","receipt_path","not_before","not_after","one_connection","one_transfer","one_execution","result_receive","two_exact_lstat","file_content_read","device_write","retry","reconnect"}
|
||||
class Error(RuntimeError):pass
|
||||
def load(p):return json.loads(p.read_text())
|
||||
def validate(a,b,now):
|
||||
if set(a)!=FIELDS or set(b)!=FIELDS or a!=b or a["active"] is not True:raise Error("inactive/mismatch")
|
||||
if not a["not_before"]<=now<=a["not_after"] or a["port"]!=PORT or a["artifact_size"]!=SIZE or a["artifact_sha256"]!=SHA:raise Error("window/identity")
|
||||
if any(a[k] is not True for k in ("one_connection","one_transfer","one_execution","result_receive","two_exact_lstat")) or any(a[k] is not False for k in ("file_content_read","device_write","retry","reconnect")):raise Error("authority")
|
||||
ipaddress.IPv4Address(a["target"])
|
||||
for k in ("output_path","receipt_path"):
|
||||
p=Path(a[k]);
|
||||
if not p.is_absolute() or p.exists():raise Error("exclusive path")
|
||||
return a
|
||||
def put(p,v):
|
||||
with p.open("x",encoding="utf-8",newline="\n") as f:json.dump(v,f,sort_keys=True);f.write("\n");f.flush();os.fsync(f.fileno())
|
||||
def run(elf,activation,approval,factory=socket.socket,wall=time.time):
|
||||
p=validate(load(activation),load(approval),wall());raw=elf.read_bytes()
|
||||
if len(raw)!=SIZE or hashlib.sha256(raw).hexdigest()!=SHA:raise Error("artifact")
|
||||
put(Path(p["receipt_path"]),{"consumed_before_socket":True,"run_id":p["run_id"],"artifact_sha256":SHA});s=None;wire=bytearray()
|
||||
try:
|
||||
s=factory(socket.AF_INET,socket.SOCK_STREAM);s.settimeout(3);s.connect((p["target"],PORT));s.sendall(raw);s.shutdown(socket.SHUT_WR);s.settimeout(10)
|
||||
while len(wire)<144:
|
||||
c=s.recv(144-len(wire))
|
||||
if not c:break
|
||||
wire.extend(c)
|
||||
if len(wire)!=144 or s.recv(1):raise Error("result length")
|
||||
result=[]
|
||||
for i in range(2):
|
||||
v=REC.unpack(wire[i*72:(i+1)*72])
|
||||
if v[0]!=b"CHP10DU1" or v[1]!=1 or v[2]!=i or v[6]!=0:raise Error("record")
|
||||
result.append({"index":i,"exists":bool(v[3]),"errno":v[4],"mode":v[5],"size":v[7],"device":v[8],"inode":v[9],"mtime":v[10],"ctime":v[11]})
|
||||
put(Path(p["output_path"]),{"paths":["/user/app/FAKE00000/app.pkg","/mnt/usb0/IV9999-FAKE00000_00-HOMEBREWLOADER01.pkg"],"records":result,"run_id":p["run_id"]});return result
|
||||
finally:
|
||||
if s is not None:s.close()
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import hashlib,ipaddress,json,os,socket,struct,time
|
||||
from pathlib import Path
|
||||
ELF_SIZE=109904;ELF_SHA="b41763f9261b838be2f83b0ee1c7b9bb5e07ced856e5638e3357de37e4405394";PACKAGE_SIZE=18153472;PACKAGE_SHA="dbcdd4dbc6303fc7a94aa0e8bb3e2c7de1d8b5770c6a30cf1ce50bc6e373aa7e";FRAME=struct.Struct("<8sIIiiQQQQq");PORT=9021;MAX_WIRE=PACKAGE_SIZE+128
|
||||
FIELDS={"active","run_id","target","port","artifact_size","artifact_sha256","output_path","receipt_path","not_before","not_after","one_connection","one_transfer","one_execution","result_receive","exact_package_read","possible_atime_effect_acknowledged","device_write","usb_read","backup_read","installation","autoload","retry","reconnect"}
|
||||
class Error(RuntimeError):pass
|
||||
def load(p):return json.loads(p.read_text())
|
||||
def validate(a,b,now):
|
||||
if set(a)!=FIELDS or set(b)!=FIELDS or a!=b or a["active"] is not True:raise Error("inactive/mismatch")
|
||||
if not a["not_before"]<=now<=a["not_after"] or a["port"]!=PORT or a["artifact_size"]!=ELF_SIZE or a["artifact_sha256"]!=ELF_SHA:raise Error("window/identity")
|
||||
if any(a[k] is not True for k in ("one_connection","one_transfer","one_execution","result_receive","exact_package_read","possible_atime_effect_acknowledged")) or any(a[k] is not False for k in ("device_write","usb_read","backup_read","installation","autoload","retry","reconnect")):raise Error("authority")
|
||||
ipaddress.IPv4Address(a["target"])
|
||||
for k in ("output_path","receipt_path"):
|
||||
p=Path(a[k]);
|
||||
if not p.is_absolute() or p.exists():raise Error("exclusive path")
|
||||
return a
|
||||
def put(p,v):
|
||||
with p.open("x",encoding="utf-8",newline="\n") as f:json.dump(v,f,sort_keys=True);f.write("\n");f.flush();os.fsync(f.fileno())
|
||||
def parse_frame(raw):
|
||||
v=FRAME.unpack(raw)
|
||||
if v[0]!=b"CHP10DW1" or v[1]!=1:raise Error("frame identity")
|
||||
return v
|
||||
def run(elf,activation,approval,factory=socket.socket,clock=time.monotonic,wall=time.time):
|
||||
p=validate(load(activation),load(approval),wall());raw=elf.read_bytes()
|
||||
if len(raw)!=ELF_SIZE or hashlib.sha256(raw).hexdigest()!=ELF_SHA:raise Error("artifact")
|
||||
put(Path(p["receipt_path"]),{"consumed_before_socket":True,"run_id":p["run_id"],"artifact_sha256":ELF_SHA});s=None;out=None;header=bytearray();terminal=bytearray();written=0;digest=hashlib.sha256();deadline=clock()+45
|
||||
try:
|
||||
out=Path(p["output_path"]).open("xb");s=factory(socket.AF_INET,socket.SOCK_STREAM);s.settimeout(3);s.connect((p["target"],PORT));s.sendall(raw);s.shutdown(socket.SHUT_WR)
|
||||
while len(header)<64:
|
||||
s.settimeout(max(.001,deadline-clock()));c=s.recv(64-len(header))
|
||||
if not c:raise Error("header eof")
|
||||
header.extend(c)
|
||||
h=parse_frame(bytes(header));
|
||||
if h[2]!=1 or h[3] or h[4] or h[5]!=PACKAGE_SIZE or h[6]:raise Error("header")
|
||||
while written<PACKAGE_SIZE:
|
||||
if clock()>=deadline:raise Error("deadline")
|
||||
s.settimeout(max(.001,deadline-clock()));c=s.recv(min(65536,PACKAGE_SIZE-written))
|
||||
if not c:raise Error("payload eof")
|
||||
out.write(c);digest.update(c);written+=len(c)
|
||||
while len(terminal)<64:
|
||||
s.settimeout(max(.001,deadline-clock()));c=s.recv(64-len(terminal))
|
||||
if not c:raise Error("terminal eof")
|
||||
terminal.extend(c)
|
||||
if s.recv(1):raise Error("trailing data")
|
||||
t=parse_frame(bytes(terminal));
|
||||
if t[2]!=2 or t[3] or t[4] or t[5]!=PACKAGE_SIZE or t[6]!=PACKAGE_SIZE or t[7:10]!=h[7:10]:raise Error("terminal")
|
||||
out.flush();os.fsync(out.fileno());out.close();out=None
|
||||
if digest.hexdigest()!=PACKAGE_SHA:raise Error("package digest mismatch")
|
||||
return {"size":written,"sha256":digest.hexdigest()}
|
||||
finally:
|
||||
if out is not None:out.close()
|
||||
if s is not None:s.close()
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Sanitize and classify an already-supplied shsrv transcript offline.
|
||||
|
||||
This tool has no networking and never preserves serial or telemetry values.
|
||||
It cannot establish an exact deployed binary identity.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
CURRENT_COMMAND_HASH = (
|
||||
"f41168292e205590bda1d243cdf727044e0af280a89fb0c070f4c5d6c92f2fd7")
|
||||
V07_COMMAND_HASH = (
|
||||
"40313637116b532f3c7f9bebe2c23c0018fe7d4093840cf463a22ba0314ca021")
|
||||
|
||||
|
||||
def command_hash(commands: list[str]) -> str:
|
||||
normalized = "\n".join(sorted(set(commands)))
|
||||
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def parse_transcript(
|
||||
transcript: str, expected_paths: set[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return a sanitized, deliberately non-exact identity record."""
|
||||
allowed_paths = expected_paths or set()
|
||||
lines = transcript.splitlines()
|
||||
compile_date = None
|
||||
compile_time = None
|
||||
firmware = None
|
||||
commands: list[str] = []
|
||||
observations: dict[str, dict[str, Any]] = {}
|
||||
serial_discarded = False
|
||||
telemetry_discarded = False
|
||||
greeting_seen = False
|
||||
in_help = False
|
||||
current_observation_path = None
|
||||
|
||||
greeting_pattern = re.compile(
|
||||
r"Welcome to shsrv\.elf running on pid \d+, "
|
||||
r"compiled (.+?) at ([0-9:]+)")
|
||||
command_pattern = re.compile(r"^\s{2}([A-Za-z0-9_]+)(?:\s+-.*)?$")
|
||||
weak_sum_pattern = re.compile(r"^([0-9]{5})\s+(.+)$")
|
||||
|
||||
for line in lines:
|
||||
greeting = greeting_pattern.search(line)
|
||||
if greeting:
|
||||
greeting_seen = True
|
||||
compile_date = greeting.group(1).strip()
|
||||
compile_time = greeting.group(2).strip()
|
||||
continue
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("S/N:"):
|
||||
serial_discarded = True
|
||||
continue
|
||||
if stripped.startswith(("SoC temp:", "CPU temp:", "CPU freq:")):
|
||||
telemetry_discarded = True
|
||||
continue
|
||||
if stripped.startswith("Model:"):
|
||||
continue
|
||||
if stripped.startswith("S/W:"):
|
||||
firmware = stripped.split(":", 1)[1].strip()
|
||||
continue
|
||||
if stripped == "Builtin commands:":
|
||||
in_help = True
|
||||
continue
|
||||
if in_help:
|
||||
command = command_pattern.match(line)
|
||||
if command:
|
||||
commands.append(command.group(1))
|
||||
continue
|
||||
if stripped == "":
|
||||
in_help = False
|
||||
if stripped.startswith("filename:"):
|
||||
path = stripped.split(":", 1)[1].strip()
|
||||
if path in allowed_paths:
|
||||
observations.setdefault(path, {})["metadata_seen"] = True
|
||||
current_observation_path = path
|
||||
else:
|
||||
current_observation_path = None
|
||||
continue
|
||||
if ":" in stripped and current_observation_path is not None:
|
||||
key, value = (part.strip() for part in stripped.split(":", 1))
|
||||
if key in {"size", "mtime", "ctime"} and value.isdigit():
|
||||
observations[current_observation_path][key] = int(value)
|
||||
continue
|
||||
weak_sum = weak_sum_pattern.match(stripped)
|
||||
if weak_sum and weak_sum.group(2) in allowed_paths:
|
||||
path = weak_sum.group(2)
|
||||
observation = observations.setdefault(path, {})
|
||||
observation["weak_checksum"] = weak_sum.group(1)
|
||||
observation["weak_checksum_algorithm"] = "BSD_ROTATE_16"
|
||||
observation["cryptographic_checksum"] = False
|
||||
|
||||
normalized_commands = sorted(set(commands))
|
||||
fingerprint = command_hash(normalized_commands) if normalized_commands else None
|
||||
family = "UNRESOLVED"
|
||||
if fingerprint == CURRENT_COMMAND_HASH:
|
||||
family = "OFFICIAL_V019_SOURCE_FAMILY_CANDIDATE"
|
||||
elif fingerprint == V07_COMMAND_HASH:
|
||||
family = "OFFICIAL_V07_SOURCE_FAMILY_CANDIDATE"
|
||||
|
||||
classification = "INVALID_OR_INCOMPLETE"
|
||||
if greeting_seen:
|
||||
classification = "COMPILE_METADATA_ONLY"
|
||||
if greeting_seen and normalized_commands:
|
||||
classification = "SOURCE_FAMILY_FINGERPRINT_ONLY"
|
||||
if greeting_seen and observations:
|
||||
classification = "WEAK_FILE_CORRELATION_ONLY"
|
||||
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"classification": classification,
|
||||
"exact_identity": False,
|
||||
"compile_metadata": {
|
||||
"date": compile_date,
|
||||
"time": compile_time,
|
||||
"firmware": firmware,
|
||||
},
|
||||
"sensitive_input": {
|
||||
"serial_line_seen": serial_discarded,
|
||||
"serial_value_retained": False,
|
||||
"telemetry_line_seen": telemetry_discarded,
|
||||
"telemetry_values_retained": False,
|
||||
},
|
||||
"command_fingerprint": {
|
||||
"count": len(normalized_commands),
|
||||
"sha256": fingerprint,
|
||||
"source_family_match": family,
|
||||
"commands": normalized_commands,
|
||||
"proves_exact_binary": False,
|
||||
},
|
||||
"file_observations": [
|
||||
{"path": path, **value, "proves_exact_binary": False}
|
||||
for path, value in sorted(observations.items())
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--expected-path", action="append", default=[],
|
||||
help="Literal pre-approved path whose metadata may be retained")
|
||||
args = parser.parse_args()
|
||||
result = parse_transcript(sys.stdin.read(), set(args.expected_path))
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Offline-only, one-shot shsrv transcript collection model.
|
||||
|
||||
This module has no network transport. It accepts bounded bytes from stdin or
|
||||
tests, removes Telnet control traffic, and delegates sanitized classification
|
||||
to the Phase-1.0T parser. It does not prove memory erasure or device behavior.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import posixpath
|
||||
import re
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from phase10t_shsrv_transcript import parse_transcript
|
||||
|
||||
|
||||
MAX_RAW_BYTES = 65_536
|
||||
MAX_SANITIZED_BYTES = 65_536
|
||||
MAX_CHUNKS = 256
|
||||
EXPECTED_FIRMWARE = "9.60"
|
||||
IAC = 0xFF
|
||||
SE = 0xF0
|
||||
SB = 0xFA
|
||||
WILL = 0xFB
|
||||
WONT = 0xFC
|
||||
DO = 0xFD
|
||||
DONT = 0xFE
|
||||
|
||||
|
||||
class CollectorError(RuntimeError):
|
||||
"""Fail-closed model error that never embeds input bytes."""
|
||||
|
||||
|
||||
class TelnetFilter:
|
||||
"""Incrementally remove Telnet negotiations and subnegotiations."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.state = "DATA"
|
||||
|
||||
def feed(self, chunk: bytes) -> bytes:
|
||||
output = bytearray()
|
||||
for value in chunk:
|
||||
if self.state == "DATA":
|
||||
if value == IAC:
|
||||
self.state = "IAC"
|
||||
else:
|
||||
output.append(value)
|
||||
elif self.state == "IAC":
|
||||
if value == IAC:
|
||||
output.append(IAC)
|
||||
self.state = "DATA"
|
||||
elif value in {WILL, WONT, DO, DONT}:
|
||||
self.state = "NEGOTIATION_OPTION"
|
||||
elif value == SB:
|
||||
self.state = "SUBNEGOTIATION"
|
||||
else:
|
||||
self.state = "DATA"
|
||||
elif self.state == "NEGOTIATION_OPTION":
|
||||
self.state = "DATA"
|
||||
elif self.state == "SUBNEGOTIATION":
|
||||
if value == IAC:
|
||||
self.state = "SUBNEGOTIATION_IAC"
|
||||
elif self.state == "SUBNEGOTIATION_IAC":
|
||||
if value == SE:
|
||||
self.state = "DATA"
|
||||
elif value == IAC:
|
||||
self.state = "SUBNEGOTIATION"
|
||||
else:
|
||||
self.state = "SUBNEGOTIATION"
|
||||
return bytes(output)
|
||||
|
||||
def is_complete(self) -> bool:
|
||||
return self.state == "DATA"
|
||||
|
||||
|
||||
class OfflineCollector:
|
||||
"""Bounded one-shot state model with no connection capability."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.state = "READY"
|
||||
self.raw_bytes_received = 0
|
||||
self.chunk_count = 0
|
||||
self._filter = TelnetFilter()
|
||||
self._sanitized = bytearray()
|
||||
|
||||
def _discard_buffer(self) -> None:
|
||||
for index in range(len(self._sanitized)):
|
||||
self._sanitized[index] = 0
|
||||
self._sanitized.clear()
|
||||
|
||||
def _invalidate(self, message: str) -> None:
|
||||
self._discard_buffer()
|
||||
self.state = "INVALID"
|
||||
raise CollectorError(message)
|
||||
|
||||
def feed(self, chunk: bytes) -> None:
|
||||
if self.state not in {"READY", "RECEIVING"}:
|
||||
raise CollectorError("collector is not accepting input")
|
||||
if not isinstance(chunk, bytes):
|
||||
self._invalidate("input must be bytes")
|
||||
if not chunk:
|
||||
return
|
||||
self.chunk_count += 1
|
||||
self.raw_bytes_received += len(chunk)
|
||||
if self.chunk_count > MAX_CHUNKS:
|
||||
self._invalidate("chunk limit exceeded")
|
||||
if self.raw_bytes_received > MAX_RAW_BYTES:
|
||||
self._invalidate("byte limit exceeded")
|
||||
filtered = self._filter.feed(chunk)
|
||||
if len(self._sanitized) + len(filtered) > MAX_SANITIZED_BYTES:
|
||||
self._invalidate("sanitized byte limit exceeded")
|
||||
self._sanitized.extend(filtered)
|
||||
self.state = "RECEIVING"
|
||||
|
||||
def abort(self) -> None:
|
||||
if self.state in {"SEALED", "INVALID"}:
|
||||
raise CollectorError("collector can no longer abort")
|
||||
self._discard_buffer()
|
||||
self.state = "ABORTED"
|
||||
|
||||
@staticmethod
|
||||
def _validate_expected_paths(expected_paths: set[str]) -> None:
|
||||
path_pattern = re.compile(r"^/[A-Za-z0-9._/-]{1,511}$")
|
||||
for path in expected_paths:
|
||||
if not isinstance(path, str) or not path_pattern.fullmatch(path):
|
||||
raise CollectorError("expected path is not a safe absolute path")
|
||||
if posixpath.normpath(path) != path or "//" in path:
|
||||
raise CollectorError("expected path is not normalized")
|
||||
|
||||
@staticmethod
|
||||
def _validate_metadata(result: dict[str, Any]) -> None:
|
||||
metadata = result["compile_metadata"]
|
||||
date = metadata["date"]
|
||||
time = metadata["time"]
|
||||
firmware = metadata["firmware"]
|
||||
if date is not None and not re.fullmatch(
|
||||
r"[A-Z][a-z]{2} [ 0-3][0-9] [0-9]{4}", date):
|
||||
raise CollectorError("compile date format is invalid")
|
||||
if time is not None and not re.fullmatch(
|
||||
r"(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]", time):
|
||||
raise CollectorError("compile time format is invalid")
|
||||
if firmware is not None and firmware != EXPECTED_FIRMWARE:
|
||||
raise CollectorError("firmware metadata does not match the gate")
|
||||
|
||||
def finalize(self, expected_paths: set[str] | None = None) -> dict[str, Any]:
|
||||
if self.state not in {"READY", "RECEIVING"}:
|
||||
raise CollectorError("collector cannot be finalized")
|
||||
if not self._filter.is_complete():
|
||||
self._invalidate("incomplete Telnet control sequence")
|
||||
try:
|
||||
transcript = bytes(self._sanitized).decode("utf-8", errors="strict")
|
||||
except UnicodeDecodeError:
|
||||
self._invalidate("transcript is not valid UTF-8")
|
||||
allowed_paths = expected_paths or set()
|
||||
try:
|
||||
self._validate_expected_paths(allowed_paths)
|
||||
result = parse_transcript(transcript, allowed_paths)
|
||||
self._validate_metadata(result)
|
||||
except (ValueError, OverflowError, CollectorError):
|
||||
self._invalidate("transcript validation failed")
|
||||
self._discard_buffer()
|
||||
self.state = "SEALED"
|
||||
result["collector_model"] = {
|
||||
"offline_only": True,
|
||||
"one_shot": True,
|
||||
"network_transport_present": False,
|
||||
"raw_bytes_received": self.raw_bytes_received,
|
||||
"chunk_count": self.chunk_count,
|
||||
"raw_transcript_persisted": False,
|
||||
"logical_buffer_discard_performed": True,
|
||||
"physical_memory_erasure_proven": False,
|
||||
"device_behavior_proven": False,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--offline-transcript", action="store_true", required=True,
|
||||
help="Confirm that stdin is an already-supplied offline transcript")
|
||||
parser.add_argument(
|
||||
"--expected-path", action="append", default=[],
|
||||
help="Literal pre-approved path whose metadata may be retained")
|
||||
args = parser.parse_args()
|
||||
collector = OfflineCollector()
|
||||
try:
|
||||
while True:
|
||||
chunk = sys.stdin.buffer.read(4096)
|
||||
if not chunk:
|
||||
break
|
||||
collector.feed(chunk)
|
||||
result = collector.finalize(set(args.expected_path))
|
||||
except CollectorError as error:
|
||||
print(f"ERROR: {error}", file=sys.stderr)
|
||||
return 1
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Offline policy model for a future one-shot shsrv client.
|
||||
|
||||
There is intentionally no transport, CLI, socket, DNS lookup, file output or
|
||||
clock acquisition. Tests supply synthetic records and an explicit host time.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from phase10v_shsrv_collector_model import (
|
||||
CollectorError as CollectorModelError,
|
||||
OfflineCollector,
|
||||
)
|
||||
|
||||
|
||||
COLLECTOR_SHA256 = \
|
||||
"f8a306dafee5d135919bec5afda789dd741e57f39803b7683fb8747c186db25c"
|
||||
SOURCE_BOUND_PORT = 2323
|
||||
MAX_DEADLINE_SECONDS = 10
|
||||
MAX_APPROVAL_LIFETIME = timedelta(minutes=15)
|
||||
WINDOW_COMMANDS = {
|
||||
"T2_GREETING_AND_HELP": ("help",),
|
||||
"T3_ONE_EXACT_PATH": ("stat", "sum"),
|
||||
}
|
||||
ACTIVATION_FIELDS = {
|
||||
"active", "policy_sha256", "collector_sha256", "run_id",
|
||||
"target_address", "target_port", "window", "exact_literal_path",
|
||||
"commands", "deadline_seconds", "expires_at",
|
||||
}
|
||||
REQUIRED_TRUE_FIELDS = {
|
||||
"ps5_connection_authorized", "device_request_authorized",
|
||||
"result_receive_authorized", "spawned_shell_effects_accepted",
|
||||
"automatic_serial_query_accepted", "automatic_telemetry_query_accepted",
|
||||
"sanitized_output_only_accepted",
|
||||
"physical_memory_erasure_unproven_accepted",
|
||||
}
|
||||
REQUIRED_FALSE_FIELDS = {
|
||||
"target_build_authorized", "device_transfer_authorized",
|
||||
"device_execution_authorized", "installation_authorized",
|
||||
"autoload_authorized", "device_write_authorized", "automatic_retry",
|
||||
"reconnect_authorized", "resume_authorized", "fallback_authorized",
|
||||
}
|
||||
APPROVAL_FIELDS = ACTIVATION_FIELDS | REQUIRED_TRUE_FIELDS | \
|
||||
REQUIRED_FALSE_FIELDS | {"attested", "listener_already_running_attested"}
|
||||
|
||||
|
||||
class PolicyError(RuntimeError):
|
||||
"""Fail-closed policy error without record values."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SessionPlan:
|
||||
"""Immutable plan only; it has no method capable of network I/O."""
|
||||
|
||||
run_id: str
|
||||
target_address: str
|
||||
target_port: int
|
||||
window: str
|
||||
exact_literal_path: str | None
|
||||
commands: tuple[str, ...]
|
||||
deadline_seconds: int
|
||||
expires_at: datetime
|
||||
|
||||
|
||||
def inactive_record_is_inert(record: dict[str, Any]) -> bool:
|
||||
return record == {
|
||||
"active": False,
|
||||
"policy_sha256": None,
|
||||
"collector_sha256": None,
|
||||
"run_id": None,
|
||||
"target_address": None,
|
||||
"target_port": None,
|
||||
"window": None,
|
||||
"exact_literal_path": None,
|
||||
"commands": [],
|
||||
"deadline_seconds": None,
|
||||
"expires_at": None,
|
||||
}
|
||||
|
||||
|
||||
def _parse_expiry(value: Any, now: datetime) -> datetime:
|
||||
if not isinstance(value, str):
|
||||
raise PolicyError("expiry is missing")
|
||||
try:
|
||||
expiry = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError as error:
|
||||
raise PolicyError("expiry format is invalid") from error
|
||||
if now.tzinfo is None or expiry.tzinfo is None:
|
||||
raise PolicyError("expiry must be timezone aware")
|
||||
if not now < expiry <= now + MAX_APPROVAL_LIFETIME:
|
||||
raise PolicyError("approval is expired or too long")
|
||||
return expiry
|
||||
|
||||
|
||||
def _validate_target(value: Any) -> str:
|
||||
if not isinstance(value, str) or not re.fullmatch(
|
||||
r"[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?", value):
|
||||
raise PolicyError("target syntax is invalid")
|
||||
if ".." in value:
|
||||
raise PolicyError("target syntax is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _validate_authority(record: dict[str, Any]) -> None:
|
||||
if not all(record.get(field) is True for field in REQUIRED_TRUE_FIELDS):
|
||||
raise PolicyError("required authority or effect acceptance is missing")
|
||||
if not all(record.get(field) is False for field in REQUIRED_FALSE_FIELDS):
|
||||
raise PolicyError("forbidden authority is active")
|
||||
|
||||
|
||||
def build_session_plan(
|
||||
activation: dict[str, Any], approval: dict[str, Any], now: datetime,
|
||||
) -> SessionPlan:
|
||||
"""Validate dual synthetic records and return an immutable offline plan."""
|
||||
if set(activation) != ACTIVATION_FIELDS or set(approval) != APPROVAL_FIELDS:
|
||||
raise PolicyError("record shape is not exact")
|
||||
if activation.get("active") is not True or approval.get("active") is not True:
|
||||
raise PolicyError("activation is inactive")
|
||||
if approval.get("attested") is not True:
|
||||
raise PolicyError("operator attestation is missing")
|
||||
if approval.get("listener_already_running_attested") is not True:
|
||||
raise PolicyError("listener attestation is missing")
|
||||
_validate_authority(approval)
|
||||
matching_fields = (
|
||||
"policy_sha256", "collector_sha256", "run_id", "target_address", "target_port",
|
||||
"window", "exact_literal_path", "commands", "deadline_seconds",
|
||||
"expires_at",
|
||||
)
|
||||
if any(activation.get(field) != approval.get(field)
|
||||
for field in matching_fields):
|
||||
raise PolicyError("activation and approval do not match")
|
||||
if activation.get("collector_sha256") != COLLECTOR_SHA256:
|
||||
raise PolicyError("collector identity mismatch")
|
||||
if not isinstance(activation.get("policy_sha256"), str) or \
|
||||
not re.fullmatch(r"[0-9a-f]{64}", activation["policy_sha256"]):
|
||||
raise PolicyError("policy identity syntax is invalid")
|
||||
run_id = activation.get("run_id")
|
||||
if not isinstance(run_id, str) or not re.fullmatch(
|
||||
r"[A-Za-z0-9_-]{8,64}", run_id):
|
||||
raise PolicyError("run identifier is invalid")
|
||||
target = _validate_target(activation.get("target_address"))
|
||||
if activation.get("target_port") != SOURCE_BOUND_PORT:
|
||||
raise PolicyError("target port mismatch")
|
||||
window = activation.get("window")
|
||||
if window not in WINDOW_COMMANDS:
|
||||
raise PolicyError("window is not allowlisted")
|
||||
commands = activation.get("commands")
|
||||
if commands != list(WINDOW_COMMANDS[window]):
|
||||
raise PolicyError("command sequence mismatch")
|
||||
path = activation.get("exact_literal_path")
|
||||
if window == "T2_GREETING_AND_HELP" and path is not None:
|
||||
raise PolicyError("help window cannot include a path")
|
||||
if window == "T3_ONE_EXACT_PATH":
|
||||
if not isinstance(path, str):
|
||||
raise PolicyError("exact path is missing")
|
||||
try:
|
||||
OfflineCollector._validate_expected_paths({path})
|
||||
except CollectorModelError as error:
|
||||
raise PolicyError("exact path is invalid") from error
|
||||
deadline = activation.get("deadline_seconds")
|
||||
if not isinstance(deadline, int) or isinstance(deadline, bool) or \
|
||||
not 1 <= deadline <= MAX_DEADLINE_SECONDS:
|
||||
raise PolicyError("deadline is invalid")
|
||||
expiry = _parse_expiry(activation.get("expires_at"), now)
|
||||
return SessionPlan(
|
||||
run_id=run_id,
|
||||
target_address=target,
|
||||
target_port=SOURCE_BOUND_PORT,
|
||||
window=window,
|
||||
exact_literal_path=path,
|
||||
commands=WINDOW_COMMANDS[window],
|
||||
deadline_seconds=deadline,
|
||||
expires_at=expiry,
|
||||
)
|
||||
@@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Inactive Phase-1.0X transport orchestration with injected adapters only.
|
||||
|
||||
This module has no socket, DNS, CLI, target address, byte-command formatter or
|
||||
live prompt detector. It implements local exclusive evidence and orchestrates
|
||||
synthetic adapter boundaries under an injected monotonic clock.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Protocol
|
||||
|
||||
from phase10v_shsrv_collector_model import CollectorError, OfflineCollector
|
||||
from phase10w_shsrv_client_policy import (
|
||||
COLLECTOR_SHA256,
|
||||
SessionPlan,
|
||||
)
|
||||
|
||||
|
||||
POLICY_SHA256 = \
|
||||
"747d23c88f2722e8e8846599c3ac1dae3826eb3fca881caaad36b251f30f3592"
|
||||
MAX_BOUNDARY_CHUNKS = 64
|
||||
|
||||
|
||||
class SessionFailure(RuntimeError):
|
||||
"""Generic failure that never embeds adapter data or target values."""
|
||||
|
||||
|
||||
class EvidenceFailure(RuntimeError):
|
||||
"""Generic exclusive-evidence failure."""
|
||||
|
||||
|
||||
class MonotonicClock(Protocol):
|
||||
def monotonic(self) -> float: ...
|
||||
|
||||
|
||||
class InjectedSessionAdapter(Protocol):
|
||||
def open_once(self, plan: SessionPlan, remaining_seconds: float) -> None: ...
|
||||
def receive_boundary(
|
||||
self, boundary: str, remaining_seconds: float,
|
||||
) -> list[bytes]: ...
|
||||
def send_command_token(
|
||||
self, command: str, exact_path: str | None, remaining_seconds: float,
|
||||
) -> None: ...
|
||||
def close_once(self) -> None: ...
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EvidenceRecord:
|
||||
path: Path
|
||||
size: int
|
||||
sha256: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SessionOutcome:
|
||||
receipt: EvidenceRecord
|
||||
output: EvidenceRecord
|
||||
classification: str
|
||||
exact_identity: bool
|
||||
|
||||
|
||||
class ExclusiveEvidenceStore:
|
||||
"""Write local JSON once with O_EXCL; never overwrite or clean up."""
|
||||
|
||||
def __init__(self, root: Path) -> None:
|
||||
self.root = root
|
||||
|
||||
@staticmethod
|
||||
def _encode(record: dict[str, Any]) -> bytes:
|
||||
return (json.dumps(
|
||||
record, sort_keys=True, separators=(",", ":"), ensure_ascii=True,
|
||||
) + "\n").encode("ascii")
|
||||
|
||||
def _create(self, filename: str, record: dict[str, Any]) -> EvidenceRecord:
|
||||
try:
|
||||
self.root.mkdir(parents=True, exist_ok=True)
|
||||
path = self.root / filename
|
||||
payload = self._encode(record)
|
||||
except (OSError, TypeError, ValueError) as error:
|
||||
raise EvidenceFailure("exclusive evidence preparation failed") from error
|
||||
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
||||
if hasattr(os, "O_BINARY"):
|
||||
flags |= os.O_BINARY
|
||||
descriptor = -1
|
||||
close_failure: OSError | None = None
|
||||
try:
|
||||
descriptor = os.open(path, flags, 0o600)
|
||||
offset = 0
|
||||
while offset < len(payload):
|
||||
written = os.write(descriptor, payload[offset:])
|
||||
if written <= 0:
|
||||
raise OSError("short local evidence write")
|
||||
offset += written
|
||||
os.fsync(descriptor)
|
||||
except OSError as error:
|
||||
raise EvidenceFailure("exclusive evidence creation failed") from error
|
||||
finally:
|
||||
if descriptor >= 0:
|
||||
try:
|
||||
os.close(descriptor)
|
||||
except OSError as error:
|
||||
close_failure = error
|
||||
if close_failure is not None:
|
||||
raise EvidenceFailure("exclusive evidence close failed") from close_failure
|
||||
try:
|
||||
reopened = path.read_bytes()
|
||||
except OSError as error:
|
||||
raise EvidenceFailure("exclusive evidence reopen failed") from error
|
||||
if reopened != payload:
|
||||
raise EvidenceFailure("exclusive evidence reopen mismatch")
|
||||
return EvidenceRecord(
|
||||
path=path,
|
||||
size=len(reopened),
|
||||
sha256=hashlib.sha256(reopened).hexdigest(),
|
||||
)
|
||||
|
||||
def create_consumed_receipt(
|
||||
self, plan: SessionPlan, monotonic_value: float,
|
||||
) -> EvidenceRecord:
|
||||
return self._create(f"{plan.run_id}.consumed.json", {
|
||||
"schema_version": 1,
|
||||
"status": "CONSUMED_BEFORE_ADAPTER_OPEN",
|
||||
"run_id": plan.run_id,
|
||||
"policy_sha256": POLICY_SHA256,
|
||||
"collector_sha256": COLLECTOR_SHA256,
|
||||
"window": plan.window,
|
||||
"deadline_seconds": plan.deadline_seconds,
|
||||
"created_monotonic": monotonic_value,
|
||||
"target_retained": False,
|
||||
"retry_allowed": False,
|
||||
})
|
||||
|
||||
def create_sanitized_output(
|
||||
self, plan: SessionPlan, receipt: EvidenceRecord,
|
||||
sanitized: dict[str, Any],
|
||||
) -> EvidenceRecord:
|
||||
return self._create(f"{plan.run_id}.sanitized.json", {
|
||||
"schema_version": 1,
|
||||
"status": "SANITIZED_OUTPUT_COMPLETE",
|
||||
"run_id": plan.run_id,
|
||||
"receipt_sha256": receipt.sha256,
|
||||
"raw_transcript_persisted": False,
|
||||
"result": sanitized,
|
||||
})
|
||||
|
||||
|
||||
def _remaining(clock: MonotonicClock, deadline: float) -> float:
|
||||
remaining = deadline - clock.monotonic()
|
||||
if remaining <= 0:
|
||||
raise SessionFailure("session deadline reached")
|
||||
return remaining
|
||||
|
||||
|
||||
def _feed_boundary(
|
||||
collector: OfflineCollector, chunks: list[bytes],
|
||||
) -> None:
|
||||
if not isinstance(chunks, list) or len(chunks) > MAX_BOUNDARY_CHUNKS:
|
||||
raise SessionFailure("adapter boundary is invalid")
|
||||
try:
|
||||
for chunk in chunks:
|
||||
collector.feed(chunk)
|
||||
except CollectorError as error:
|
||||
raise SessionFailure("collector rejected adapter input") from error
|
||||
|
||||
|
||||
def run_injected_session(
|
||||
plan: SessionPlan,
|
||||
adapter: InjectedSessionAdapter,
|
||||
clock: MonotonicClock,
|
||||
evidence: ExclusiveEvidenceStore,
|
||||
) -> SessionOutcome:
|
||||
"""Run one injected session; never retries and never performs live I/O."""
|
||||
start = clock.monotonic()
|
||||
deadline = start + plan.deadline_seconds
|
||||
receipt = evidence.create_consumed_receipt(plan, start)
|
||||
collector = OfflineCollector()
|
||||
open_attempted = False
|
||||
sanitized: dict[str, Any] | None = None
|
||||
primary_failure: SessionFailure | None = None
|
||||
try:
|
||||
open_attempted = True
|
||||
adapter.open_once(plan, _remaining(clock, deadline))
|
||||
chunks = adapter.receive_boundary(
|
||||
"INITIAL_PROMPT", _remaining(clock, deadline))
|
||||
_feed_boundary(collector, chunks)
|
||||
for command in plan.commands:
|
||||
adapter.send_command_token(
|
||||
command, plan.exact_literal_path, _remaining(clock, deadline))
|
||||
chunks = adapter.receive_boundary(
|
||||
f"AFTER_{command.upper()}", _remaining(clock, deadline))
|
||||
_feed_boundary(collector, chunks)
|
||||
_remaining(clock, deadline)
|
||||
expected_paths = (
|
||||
{plan.exact_literal_path}
|
||||
if plan.exact_literal_path is not None else set()
|
||||
)
|
||||
sanitized = collector.finalize(expected_paths)
|
||||
except Exception: # noqa: BLE001 - injected adapter boundary
|
||||
# Adapter exceptions are deliberately normalized; raw messages never
|
||||
# become evidence or user output.
|
||||
primary_failure = SessionFailure("injected session failed")
|
||||
finally:
|
||||
if open_attempted:
|
||||
try:
|
||||
adapter.close_once()
|
||||
except Exception: # noqa: BLE001 - injected adapter
|
||||
if primary_failure is None:
|
||||
primary_failure = SessionFailure("injected session close failed")
|
||||
if primary_failure is not None:
|
||||
raise primary_failure from None
|
||||
if sanitized is None:
|
||||
raise SessionFailure("sanitized result is missing")
|
||||
try:
|
||||
output = evidence.create_sanitized_output(plan, receipt, sanitized)
|
||||
except EvidenceFailure as error:
|
||||
raise SessionFailure("sanitized output creation failed") from error
|
||||
return SessionOutcome(
|
||||
receipt=receipt,
|
||||
output=output,
|
||||
classification=str(sanitized["classification"]),
|
||||
exact_identity=bool(sanitized["exact_identity"]),
|
||||
)
|
||||
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Offline source-family model for shsrv framing; never opens a transport."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
LEGACY_RAW = "LEGACY_RAW_V07_V08"
|
||||
LIBTELNET_NVT = "LIBTELNET_NVT_V09_V019"
|
||||
SOURCE_FAMILIES = {LEGACY_RAW, LIBTELNET_NVT}
|
||||
MAX_MODEL_BYTES = 65_536
|
||||
MAX_MODEL_CHUNKS = 256
|
||||
|
||||
IAC = 0xFF
|
||||
SE = 0xF0
|
||||
SB = 0xFA
|
||||
WILL = 0xFB
|
||||
WONT = 0xFC
|
||||
DO = 0xFD
|
||||
DONT = 0xFE
|
||||
|
||||
|
||||
class FramingError(RuntimeError):
|
||||
"""Fail-closed model error without input bytes."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DecodedFraming:
|
||||
application: bytes
|
||||
negotiation_replies: tuple[bytes, ...]
|
||||
iac_commands: tuple[int, ...]
|
||||
source_family: str
|
||||
source_model_only: bool = True
|
||||
device_behavior_proven: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PromptAssessment:
|
||||
candidate_offsets: tuple[int, ...]
|
||||
ends_with_candidate: bool
|
||||
classification: str
|
||||
exact_completion_proven: bool = False
|
||||
|
||||
|
||||
def initial_server_bytes(family: str) -> bytes:
|
||||
"""Both audited families emit no proactive Telnet negotiation."""
|
||||
_require_family(family)
|
||||
return b""
|
||||
|
||||
|
||||
def encode_server_text(family: str, application: bytes) -> bytes:
|
||||
"""Model the source-family transformation from stdout to peer bytes."""
|
||||
_require_family(family)
|
||||
if not isinstance(application, bytes):
|
||||
raise FramingError("application text must be bytes")
|
||||
if len(application) > MAX_MODEL_BYTES:
|
||||
raise FramingError("application text exceeds model limit")
|
||||
if family == LEGACY_RAW:
|
||||
return application
|
||||
output = bytearray()
|
||||
for value in application:
|
||||
if value == IAC:
|
||||
output.extend((IAC, IAC))
|
||||
elif value == 0x0D:
|
||||
output.extend((0x0D, 0x00))
|
||||
elif value == 0x0A:
|
||||
output.extend((0x0D, 0x0A))
|
||||
else:
|
||||
output.append(value)
|
||||
return bytes(output)
|
||||
|
||||
|
||||
class ClientWireDecoder:
|
||||
"""Bounded incremental model of bytes received by the shell."""
|
||||
|
||||
def __init__(self, family: str) -> None:
|
||||
_require_family(family)
|
||||
self.family = family
|
||||
self.state = "DATA"
|
||||
self.negotiation_command: int | None = None
|
||||
self.raw_bytes = 0
|
||||
self.chunks = 0
|
||||
self._application = bytearray()
|
||||
self._replies: list[bytes] = []
|
||||
self._iac_commands: list[int] = []
|
||||
self._sealed = False
|
||||
|
||||
def _append(self, value: int) -> None:
|
||||
self._application.append(value)
|
||||
if len(self._application) > MAX_MODEL_BYTES:
|
||||
raise FramingError("decoded application exceeds model limit")
|
||||
|
||||
def feed(self, chunk: bytes) -> None:
|
||||
if self._sealed:
|
||||
raise FramingError("model is sealed")
|
||||
if not isinstance(chunk, bytes):
|
||||
raise FramingError("wire chunk must be bytes")
|
||||
if not chunk:
|
||||
return
|
||||
self.chunks += 1
|
||||
self.raw_bytes += len(chunk)
|
||||
if self.chunks > MAX_MODEL_CHUNKS:
|
||||
raise FramingError("wire chunk limit exceeded")
|
||||
if self.raw_bytes > MAX_MODEL_BYTES:
|
||||
raise FramingError("wire byte limit exceeded")
|
||||
if self.family == LEGACY_RAW:
|
||||
self._application.extend(chunk)
|
||||
return
|
||||
for value in chunk:
|
||||
self._feed_libtelnet(value)
|
||||
|
||||
def _feed_libtelnet(self, value: int) -> None:
|
||||
if self.state == "DATA":
|
||||
if value == IAC:
|
||||
self.state = "IAC"
|
||||
elif value == 0x0D:
|
||||
self.state = "EOL"
|
||||
else:
|
||||
self._append(value)
|
||||
elif self.state == "EOL":
|
||||
if value == 0x0A:
|
||||
self._append(0x0A)
|
||||
else:
|
||||
self._append(0x0D)
|
||||
if value != 0x00:
|
||||
self._append(value)
|
||||
self.state = "DATA"
|
||||
elif self.state == "IAC":
|
||||
if value == IAC:
|
||||
self._append(IAC)
|
||||
self.state = "DATA"
|
||||
elif value in {WILL, WONT, DO, DONT}:
|
||||
self.negotiation_command = value
|
||||
self.state = "NEGOTIATION"
|
||||
elif value == SB:
|
||||
self.state = "SUBNEGOTIATION_OPTION"
|
||||
else:
|
||||
self._iac_commands.append(value)
|
||||
self.state = "DATA"
|
||||
elif self.state == "NEGOTIATION":
|
||||
command = self.negotiation_command
|
||||
if command == WILL:
|
||||
self._replies.append(bytes((IAC, DONT, value)))
|
||||
elif command == DO:
|
||||
self._replies.append(bytes((IAC, WONT, value)))
|
||||
self.negotiation_command = None
|
||||
self.state = "DATA"
|
||||
elif self.state == "SUBNEGOTIATION_OPTION":
|
||||
self.state = "SUBNEGOTIATION"
|
||||
elif self.state == "SUBNEGOTIATION":
|
||||
if value == IAC:
|
||||
self.state = "SUBNEGOTIATION_IAC"
|
||||
elif self.state == "SUBNEGOTIATION_IAC":
|
||||
if value == SE:
|
||||
self.state = "DATA"
|
||||
elif value == IAC:
|
||||
self.state = "SUBNEGOTIATION"
|
||||
else:
|
||||
self.state = "IAC"
|
||||
self._feed_libtelnet(value)
|
||||
else:
|
||||
raise FramingError("unknown model state")
|
||||
|
||||
def finalize(self) -> DecodedFraming:
|
||||
if self._sealed:
|
||||
raise FramingError("model is sealed")
|
||||
self._sealed = True
|
||||
if self.family == LIBTELNET_NVT and self.state != "DATA":
|
||||
raise FramingError("incomplete Telnet or NVT sequence")
|
||||
return DecodedFraming(
|
||||
application=bytes(self._application),
|
||||
negotiation_replies=tuple(self._replies),
|
||||
iac_commands=tuple(self._iac_commands),
|
||||
source_family=self.family,
|
||||
)
|
||||
|
||||
|
||||
def assess_prompt_candidates(application: bytes) -> PromptAssessment:
|
||||
"""Find source-shaped prompt suffixes without promoting them to proof."""
|
||||
if not isinstance(application, bytes):
|
||||
raise FramingError("application text must be bytes")
|
||||
candidates: list[int] = []
|
||||
start = 0
|
||||
while True:
|
||||
offset = application.find(b"$ ", start)
|
||||
if offset < 0:
|
||||
break
|
||||
candidates.append(offset)
|
||||
start = offset + 2
|
||||
ends = application.endswith(b"$ ")
|
||||
if not ends:
|
||||
classification = "NO_TERMINAL_PROMPT_CANDIDATE"
|
||||
elif len(candidates) == 1:
|
||||
classification = "SOURCE_SHAPE_CANDIDATE_ONLY"
|
||||
else:
|
||||
classification = "AMBIGUOUS_PROMPT_CANDIDATES"
|
||||
return PromptAssessment(tuple(candidates), ends, classification)
|
||||
|
||||
|
||||
def _require_family(family: str) -> None:
|
||||
if family not in SOURCE_FAMILIES:
|
||||
raise FramingError("unknown source family")
|
||||
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Offline-only passive LF-batch contract for synthetic shsrv inputs.
|
||||
|
||||
The module has no transport, CLI, address, clock, file output or prompt
|
||||
detector. It formats one bounded batch from an already validated Phase-1.0W
|
||||
plan and seals supplied bytes only when the caller supplies a synthetic hard
|
||||
deadline event.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from phase10v_shsrv_collector_model import CollectorError, OfflineCollector
|
||||
from phase10w_shsrv_client_policy import SessionPlan, WINDOW_COMMANDS
|
||||
|
||||
|
||||
IAC = 0xFF
|
||||
MAX_LITERAL_PATH_BYTES = 512
|
||||
MAX_BATCH_BYTES = 1035
|
||||
KNOWN_HELP_FAMILIES = {
|
||||
"OFFICIAL_V07_SOURCE_FAMILY_CANDIDATE",
|
||||
"OFFICIAL_V019_SOURCE_FAMILY_CANDIDATE",
|
||||
}
|
||||
|
||||
|
||||
class PassiveContractError(RuntimeError):
|
||||
"""Fail-closed contract error that never embeds supplied bytes or paths."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PassiveBatch:
|
||||
"""Target-free immutable outbound bytes for one synthetic session."""
|
||||
|
||||
window: str
|
||||
payload: bytes
|
||||
expected_paths: tuple[str, ...]
|
||||
command_count: int
|
||||
deadline_seconds: int
|
||||
outbound_batches: int = 1
|
||||
lf_only: bool = True
|
||||
telnet_negotiation: bool = False
|
||||
retry_allowed: bool = False
|
||||
reconnect_allowed: bool = False
|
||||
resume_allowed: bool = False
|
||||
|
||||
|
||||
def _validate_payload(payload: bytes) -> None:
|
||||
if not payload or len(payload) > MAX_BATCH_BYTES:
|
||||
raise PassiveContractError("batch length is invalid")
|
||||
if any(value in payload for value in (0x00, 0x0D, IAC)):
|
||||
raise PassiveContractError("batch contains a forbidden byte")
|
||||
if any(value > 0x7F for value in payload):
|
||||
raise PassiveContractError("batch is not ASCII")
|
||||
if not payload.endswith(b"\n"):
|
||||
raise PassiveContractError("batch is not LF terminated")
|
||||
|
||||
|
||||
def _validate_batch(batch: PassiveBatch) -> None:
|
||||
_validate_payload(batch.payload)
|
||||
if batch.outbound_batches != 1 or batch.lf_only is not True or \
|
||||
batch.telnet_negotiation is not False or \
|
||||
batch.retry_allowed is not False or \
|
||||
batch.reconnect_allowed is not False or batch.resume_allowed is not False:
|
||||
raise PassiveContractError("batch safety fields are invalid")
|
||||
if not isinstance(batch.deadline_seconds, int) or isinstance(
|
||||
batch.deadline_seconds, bool) or not 1 <= batch.deadline_seconds <= 10:
|
||||
raise PassiveContractError("batch deadline is invalid")
|
||||
if batch.window == "T2_GREETING_AND_HELP":
|
||||
if batch.payload != b"help\n" or batch.expected_paths != () or \
|
||||
batch.command_count != 1:
|
||||
raise PassiveContractError("help batch contract is invalid")
|
||||
return
|
||||
if batch.window == "T3_ONE_EXACT_PATH" and \
|
||||
len(batch.expected_paths) == 1 and batch.command_count == 2:
|
||||
path = batch.expected_paths[0]
|
||||
try:
|
||||
OfflineCollector._validate_expected_paths({path})
|
||||
encoded = path.encode("ascii", errors="strict")
|
||||
except (CollectorError, UnicodeEncodeError) as error:
|
||||
raise PassiveContractError("path batch contract is invalid") from error
|
||||
expected = b"stat " + encoded + b"\nsum " + encoded + b"\n"
|
||||
if len(encoded) <= MAX_LITERAL_PATH_BYTES and batch.payload == expected:
|
||||
return
|
||||
raise PassiveContractError("path batch contract is invalid")
|
||||
|
||||
|
||||
def build_passive_batch(plan: SessionPlan) -> PassiveBatch:
|
||||
"""Build one target-free LF batch from an exact Phase-1.0W plan."""
|
||||
if not isinstance(plan, SessionPlan):
|
||||
raise PassiveContractError("session plan type is invalid")
|
||||
expected_commands = WINDOW_COMMANDS.get(plan.window)
|
||||
if expected_commands is None or plan.commands != expected_commands:
|
||||
raise PassiveContractError("window command sequence is invalid")
|
||||
if not isinstance(plan.deadline_seconds, int) or isinstance(
|
||||
plan.deadline_seconds, bool) or not 1 <= plan.deadline_seconds <= 10:
|
||||
raise PassiveContractError("deadline is invalid")
|
||||
|
||||
expected_paths: tuple[str, ...]
|
||||
if plan.window == "T2_GREETING_AND_HELP":
|
||||
if plan.exact_literal_path is not None:
|
||||
raise PassiveContractError("help window contains a path")
|
||||
payload = b"help\n"
|
||||
expected_paths = ()
|
||||
elif plan.window == "T3_ONE_EXACT_PATH":
|
||||
path = plan.exact_literal_path
|
||||
if not isinstance(path, str):
|
||||
raise PassiveContractError("exact path is missing")
|
||||
try:
|
||||
OfflineCollector._validate_expected_paths({path})
|
||||
encoded = path.encode("ascii", errors="strict")
|
||||
except (CollectorError, UnicodeEncodeError) as error:
|
||||
raise PassiveContractError("exact path is invalid") from error
|
||||
if len(encoded) > MAX_LITERAL_PATH_BYTES:
|
||||
raise PassiveContractError("exact path is too long")
|
||||
payload = b"stat " + encoded + b"\nsum " + encoded + b"\n"
|
||||
expected_paths = (path,)
|
||||
else:
|
||||
raise PassiveContractError("window is not allowlisted")
|
||||
|
||||
batch = PassiveBatch(
|
||||
window=plan.window,
|
||||
payload=payload,
|
||||
expected_paths=expected_paths,
|
||||
command_count=len(plan.commands),
|
||||
deadline_seconds=plan.deadline_seconds,
|
||||
)
|
||||
_validate_batch(batch)
|
||||
return batch
|
||||
|
||||
|
||||
class PassiveResultAccumulator:
|
||||
"""Consume supplied chunks once; never observes prompts or remote EOF."""
|
||||
|
||||
def __init__(self, batch: PassiveBatch) -> None:
|
||||
if not isinstance(batch, PassiveBatch):
|
||||
raise PassiveContractError("batch type is invalid")
|
||||
_validate_batch(batch)
|
||||
self.batch = batch
|
||||
self._collector = OfflineCollector()
|
||||
self.state = "READY"
|
||||
|
||||
def _invalidate(self, message: str) -> None:
|
||||
if self._collector.state not in {"SEALED", "INVALID", "ABORTED"}:
|
||||
try:
|
||||
self._collector.abort()
|
||||
except CollectorError:
|
||||
pass
|
||||
self.state = "INVALID"
|
||||
raise PassiveContractError(message)
|
||||
|
||||
def feed_supplied_chunk(self, chunk: bytes) -> None:
|
||||
if self.state not in {"READY", "RECEIVING"}:
|
||||
raise PassiveContractError("accumulator is not accepting input")
|
||||
if not isinstance(chunk, bytes):
|
||||
self._invalidate("input must be bytes")
|
||||
if IAC in chunk:
|
||||
self._invalidate("unexpected Telnet control byte")
|
||||
try:
|
||||
self._collector.feed(chunk)
|
||||
except CollectorError as error:
|
||||
self._invalidate("collector rejected supplied input")
|
||||
raise AssertionError("unreachable") from error
|
||||
if chunk:
|
||||
self.state = "RECEIVING"
|
||||
|
||||
def abort(self) -> None:
|
||||
if self.state not in {"READY", "RECEIVING"}:
|
||||
raise PassiveContractError("accumulator can no longer abort")
|
||||
try:
|
||||
self._collector.abort()
|
||||
except CollectorError as error:
|
||||
raise PassiveContractError("collector abort failed") from error
|
||||
self.state = "ABORTED"
|
||||
|
||||
def _validate_complete_result(self, result: dict[str, Any]) -> None:
|
||||
if self.batch.window == "T2_GREETING_AND_HELP":
|
||||
fingerprint = result.get("command_fingerprint", {})
|
||||
if fingerprint.get("source_family_match") not in KNOWN_HELP_FAMILIES:
|
||||
raise PassiveContractError("help response is incomplete or unknown")
|
||||
if result.get("classification") != "SOURCE_FAMILY_FINGERPRINT_ONLY":
|
||||
raise PassiveContractError("help response classification is invalid")
|
||||
elif self.batch.window == "T3_ONE_EXACT_PATH":
|
||||
observations = result.get("file_observations")
|
||||
if not isinstance(observations, list) or len(observations) != 1:
|
||||
raise PassiveContractError("file response is incomplete")
|
||||
observation = observations[0]
|
||||
required = {
|
||||
"path", "metadata_seen", "size", "weak_checksum",
|
||||
"weak_checksum_algorithm", "cryptographic_checksum",
|
||||
"proves_exact_binary",
|
||||
}
|
||||
if not required.issubset(observation) or observation.get("path") != \
|
||||
self.batch.expected_paths[0]:
|
||||
raise PassiveContractError("file response fields are incomplete")
|
||||
if observation.get("metadata_seen") is not True or not isinstance(
|
||||
observation.get("size"), int) or isinstance(
|
||||
observation.get("size"), bool) or observation["size"] < 0:
|
||||
raise PassiveContractError("stat response is incomplete")
|
||||
if observation.get("weak_checksum_algorithm") != "BSD_ROTATE_16" or \
|
||||
observation.get("cryptographic_checksum") is not False:
|
||||
raise PassiveContractError("sum response is incomplete")
|
||||
if result.get("classification") != "WEAK_FILE_CORRELATION_ONLY":
|
||||
raise PassiveContractError("file response classification is invalid")
|
||||
else:
|
||||
raise PassiveContractError("batch window is invalid")
|
||||
|
||||
def seal_at_hard_deadline(
|
||||
self, hard_deadline_reached: bool,
|
||||
) -> dict[str, Any]:
|
||||
"""Seal once only after an externally supplied synthetic deadline."""
|
||||
if hard_deadline_reached is not True:
|
||||
self._invalidate("hard deadline event is absent")
|
||||
if self.state not in {"READY", "RECEIVING"}:
|
||||
raise PassiveContractError("accumulator cannot be sealed")
|
||||
try:
|
||||
result = self._collector.finalize(set(self.batch.expected_paths))
|
||||
self._validate_complete_result(result)
|
||||
except (CollectorError, PassiveContractError) as error:
|
||||
self.state = "INVALID"
|
||||
raise PassiveContractError("deadline result is invalid") from error
|
||||
self.state = "SEALED"
|
||||
result["passive_batch_contract"] = {
|
||||
"offline_only": True,
|
||||
"one_outbound_batch": True,
|
||||
"plain_lf_only": True,
|
||||
"telnet_negotiation_emitted": False,
|
||||
"telnet_control_received": False,
|
||||
"prompt_completion_used": False,
|
||||
"remote_eof_completion_used": False,
|
||||
"sealed_by_synthetic_hard_deadline": True,
|
||||
"source_family_selected": False,
|
||||
"retry_allowed": False,
|
||||
"reconnect_allowed": False,
|
||||
"device_behavior_proven": False,
|
||||
"exact_identity_proven": False,
|
||||
}
|
||||
return result
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Reject common credential material before it reaches Git history."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PATTERNS = {
|
||||
"private key": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
|
||||
"GitHub-style token": re.compile(r"\bgh[pousr]_[A-Za-z0-9]{30,}\b"),
|
||||
"OpenAI-style key": re.compile(r"\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b"),
|
||||
"AWS access key": re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
|
||||
"credential in URL": re.compile(r"https?://[^\s/:@]+:[^\s/@]+@"),
|
||||
}
|
||||
|
||||
SKIP_PARTS = {".git", "work", "build", ".vs", ".idea", ".vscode"}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
root = args.root.resolve()
|
||||
errors: list[str] = []
|
||||
scanned = 0
|
||||
|
||||
for directory, subdirectories, filenames in os.walk(root):
|
||||
subdirectories[:] = sorted(
|
||||
name
|
||||
for name in subdirectories
|
||||
if name not in SKIP_PARTS and not name.startswith("build-")
|
||||
)
|
||||
for filename in sorted(filenames):
|
||||
path = Path(directory, filename)
|
||||
relative = path.relative_to(root)
|
||||
if path.stat().st_size > 2_000_000:
|
||||
continue
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
scanned += 1
|
||||
for label, pattern in PATTERNS.items():
|
||||
if pattern.search(text):
|
||||
errors.append(f"{relative}: possible {label}")
|
||||
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"secret scan failed: {error}")
|
||||
return 1
|
||||
print(f"secret scan passed across {scanned} text files")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,618 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate the offline, fail-closed Phase-0.8R evidence contract."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
STATUS = "READ_ONLY_PREFLIGHT_BLOCKED"
|
||||
SOURCE_COMMIT = "2c944d6d65a08f7e1c02f518721cde061b999329"
|
||||
PAYLOAD_MANAGER_COMMIT = "cfbc70f30f419b09bf2b52283f7409e2d3117ee1"
|
||||
DENYLIST_SHA256 = (
|
||||
"4be1c17b4964f2b68c39b5145bc4af4619c32512d60269ecf5c39728b390fa63"
|
||||
)
|
||||
STOCK_ELFLDR_SHA256 = (
|
||||
"092d16ee0ede0c494947efd38d1a17bbd7cc4b022d3858ea898833c188c703e8"
|
||||
)
|
||||
STOCK_PAYLOAD_MANAGER_SHA256 = (
|
||||
"518740adbacccb9094fadb07dd424c53ee290f38306449ccc9d6957fdf813c0b"
|
||||
)
|
||||
|
||||
IMMUTABLE_EVIDENCE = {
|
||||
"docs/runtime/phase-0.8-read-only-preflight.md": {
|
||||
"role": "historical_human_readable_preflight_record",
|
||||
"sha256": "3fbe086175a6048176075f447ec1482074928e3b5282db97ea2169395fe1d508",
|
||||
},
|
||||
"manifests/runtime/phase-0.8-read-only-preflight.json": {
|
||||
"role": "historical_machine_readable_preflight_record",
|
||||
"sha256": "47d7f452f8799979fe99b3e6d56859f03544112725bf7e5b349eba5ed81b3322",
|
||||
},
|
||||
"tests/test_phase08_preflight.py": {
|
||||
"role": "historical_fail_closed_regression_test",
|
||||
"sha256": "8a4ad7c70de28ffe3148fd3fd1f68c36a872c53c691c9068e1ff163970863c48",
|
||||
},
|
||||
}
|
||||
|
||||
AUTHORIZATION_FIELDS = (
|
||||
"authorized",
|
||||
"installation_authorized",
|
||||
"lifecycle_authorized",
|
||||
"execution_authorized",
|
||||
"transfer_authorized",
|
||||
"automatic_retry",
|
||||
)
|
||||
|
||||
ACTIVITY_FIELDS = (
|
||||
"hardware_observed",
|
||||
"ps5_connected",
|
||||
"files_transferred",
|
||||
"target_execution_performed",
|
||||
"target_mutation_performed",
|
||||
"target_artifact_created",
|
||||
"collector_implemented",
|
||||
"target_build_performed",
|
||||
)
|
||||
|
||||
REQUIRED_BLOCKERS = {
|
||||
"exact_permission_quote_absent": "STOP-RO",
|
||||
"collector_identity_absent": "STOP-RO",
|
||||
"collector_side_effect_contract_absent": "STOP-RO",
|
||||
"two_current_firmware_sources_absent": "STOP-GATE",
|
||||
"live_object_identities_absent": "STOP-GATE",
|
||||
"listeners_absent": "STOP-GATE",
|
||||
"autoload_status_absent": "STOP-GATE",
|
||||
"rollback_backups_absent": "STOP-GATE",
|
||||
"payload_manager_backup_not_byte_exact_on_device": "HARD_STOP-GATE",
|
||||
"unknown_result_is_stop": "STOP",
|
||||
"timeout_is_stop": "STOP",
|
||||
"deviation_is_stop": "STOP",
|
||||
"automatic_retry_forbidden": "STOP",
|
||||
}
|
||||
|
||||
REQUIRED_FINDINGS = {
|
||||
"options_any_endpoint",
|
||||
"get_version",
|
||||
"get_log",
|
||||
"get_autoload_status",
|
||||
"get_config",
|
||||
"get_list_payloads",
|
||||
"get_processes_list",
|
||||
"get_sources_list",
|
||||
"get_ip",
|
||||
}
|
||||
|
||||
REQUIRED_OBSERVATIONS = {
|
||||
"firmware",
|
||||
"live_paths",
|
||||
"object_identities",
|
||||
"file_sizes",
|
||||
"sha256",
|
||||
"processes_services",
|
||||
"listeners",
|
||||
"autoload",
|
||||
"rollback_files",
|
||||
"storage_precondition",
|
||||
}
|
||||
|
||||
REQUIRED_PROHIBITED_ACTIONS = {
|
||||
"connect_to_ps5",
|
||||
"probe_ip_port_or_device_interface",
|
||||
"use_usb_or_removable_media",
|
||||
"transfer_ps5_file",
|
||||
"package_for_ps5_deployment",
|
||||
"install_or_replace_target_component",
|
||||
"execute_elf_or_payload",
|
||||
"build_target_elf",
|
||||
"start_cross_compiler",
|
||||
"implement_or_build_collector",
|
||||
"modify_payload_manager_production_code",
|
||||
"modify_elfldr_production_code",
|
||||
"modify_lifecycle_code",
|
||||
"activate_or_modify_autoload",
|
||||
"activate_retry",
|
||||
"change_target_configuration",
|
||||
"start_stop_or_signal_target_service_or_process",
|
||||
"implement_gnm_videoout_sdl_audio_input_shaders_cores_or_retroarch",
|
||||
"download_or_install_packages",
|
||||
"contact_internet_gitea_or_other_remote",
|
||||
"commit_or_push",
|
||||
}
|
||||
|
||||
TEMPLATE_REQUIRED_FIELDS = {
|
||||
"exact_user_statement",
|
||||
"authorization_date",
|
||||
"expiration_time",
|
||||
"device_identity",
|
||||
"exact_purpose",
|
||||
"exact_observations",
|
||||
"method_or_collector_id",
|
||||
"source_commit",
|
||||
"collector_file_size",
|
||||
"collector_sha256",
|
||||
"firmware_gate",
|
||||
"maximum_runtime_ms",
|
||||
"maximum_execution_count",
|
||||
"maximum_transfer_count",
|
||||
"network_behavior",
|
||||
"output_channel",
|
||||
"allowed_volatile_effects",
|
||||
"prohibited_persistent_effects",
|
||||
"stop_criteria",
|
||||
"cleanup_requirements",
|
||||
"reporting_requirements",
|
||||
"explicit_installation_exclusion",
|
||||
"explicit_lifecycle_probe_exclusion",
|
||||
"explicit_autoload_and_retry_exclusion",
|
||||
"explicit_graphics_and_retroarch_exclusion",
|
||||
"revocation_method",
|
||||
"manual_confirmation_template_does_not_authorize",
|
||||
}
|
||||
|
||||
TARGET_SUFFIXES = {
|
||||
".elf",
|
||||
".self",
|
||||
".sprx",
|
||||
".pkg",
|
||||
".bin",
|
||||
".payload",
|
||||
".zip",
|
||||
".tar",
|
||||
".tgz",
|
||||
".7z",
|
||||
}
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
document = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(document, dict):
|
||||
raise ValueError(f"{path}: expected a JSON object")
|
||||
return document
|
||||
|
||||
|
||||
def extract_json_contract(path: Path, name: str) -> dict[str, Any]:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
begin = f"<!-- BEGIN {name} -->"
|
||||
end = f"<!-- END {name} -->"
|
||||
if text.count(begin) != 1 or text.count(end) != 1:
|
||||
raise ValueError(f"{path}: expected exactly one {name} contract")
|
||||
block = text.split(begin, 1)[1].split(end, 1)[0].strip()
|
||||
if not block.startswith("```json\n") or not block.endswith("\n```"):
|
||||
raise ValueError(f"{path}: {name} must be one fenced JSON object")
|
||||
document = json.loads(block[len("```json\n") : -len("\n```")])
|
||||
if not isinstance(document, dict):
|
||||
raise ValueError(f"{path}: {name} must be a JSON object")
|
||||
return document
|
||||
|
||||
|
||||
def positive_status_values(value: Any) -> list[str]:
|
||||
errors: list[str] = []
|
||||
positive = {
|
||||
"READY",
|
||||
"COMPLETE",
|
||||
"COMPLETED",
|
||||
"AUTHORIZED",
|
||||
"PASS",
|
||||
"PASSED",
|
||||
"READ_ONLY_PREFLIGHT_DATA_COMPLETE",
|
||||
"READY_FOR_HARDENED_RUNTIME_DEPLOYMENT",
|
||||
}
|
||||
if isinstance(value, dict):
|
||||
for key, child in value.items():
|
||||
if key == "historical_validation_report":
|
||||
continue
|
||||
errors.extend(positive_status_values(child))
|
||||
elif isinstance(value, list):
|
||||
for child in value:
|
||||
errors.extend(positive_status_values(child))
|
||||
elif isinstance(value, str) and value.upper() in positive:
|
||||
errors.append(value)
|
||||
return errors
|
||||
|
||||
|
||||
def validate_manifest(
|
||||
manifest: dict[str, Any], denylist: dict[str, Any]
|
||||
) -> list[str]:
|
||||
errors: list[str] = []
|
||||
|
||||
if manifest.get("schema_version") != 1:
|
||||
errors.append("unsupported remediation schema version")
|
||||
schema_contract = manifest.get("schema_contract", {})
|
||||
if schema_contract.get("id") != "chimera-gfx-phase-0.8-remediation-v1":
|
||||
errors.append("remediation schema-contract ID changed")
|
||||
if schema_contract.get("shared_schema_available") is not False:
|
||||
errors.append("remediation incorrectly claims a shared schema")
|
||||
if schema_contract.get("validator") != "tools/validate_phase08_remediation.py":
|
||||
errors.append("remediation validator binding changed")
|
||||
|
||||
if manifest.get("phase_id") != "0.8R":
|
||||
errors.append("remediation phase ID changed")
|
||||
if manifest.get("phase") != "offline_remediation":
|
||||
errors.append("remediation phase widened beyond offline")
|
||||
if manifest.get("status") != STATUS:
|
||||
errors.append("Phase-0.8 status is not fail-closed")
|
||||
if positive_status_values(manifest):
|
||||
errors.append("manifest contains an overriding positive status")
|
||||
|
||||
historical = manifest.get("historical_identity", {})
|
||||
if historical.get("source_commit") != SOURCE_COMMIT:
|
||||
errors.append("historical source commit changed")
|
||||
if historical.get("status") != STATUS:
|
||||
errors.append("historical Phase-0.8 status changed")
|
||||
historical_report = historical.get("historical_validation_report", {})
|
||||
if (
|
||||
historical_report.get("classification")
|
||||
!= "historical_report_not_current_hardware_evidence"
|
||||
):
|
||||
errors.append("historical tests were promoted to hardware evidence")
|
||||
|
||||
immutable = {
|
||||
item.get("path"): {
|
||||
"role": item.get("role"),
|
||||
"sha256": item.get("sha256"),
|
||||
}
|
||||
for item in manifest.get("immutable_evidence", [])
|
||||
}
|
||||
if immutable != IMMUTABLE_EVIDENCE:
|
||||
errors.append("immutable Phase-0.8 evidence binding changed")
|
||||
|
||||
authorization = manifest.get("authorization", {})
|
||||
if any(authorization.get(field) is not False for field in AUTHORIZATION_FIELDS):
|
||||
errors.append("an authorization or retry field is not false")
|
||||
activity = manifest.get("activity", {})
|
||||
if any(activity.get(field) is not False for field in ACTIVITY_FIELDS):
|
||||
errors.append("manifest claims a prohibited target activity")
|
||||
|
||||
if manifest.get("firmware_runtime_behavior") != "UNPROVEN":
|
||||
errors.append("firmware runtime behavior was promoted")
|
||||
claim_boundaries = manifest.get("claim_boundaries", {})
|
||||
required_false_claims = {
|
||||
"hardware_safety_proven",
|
||||
"firmware_behavior_proven",
|
||||
"absence_of_volatile_effects_proven",
|
||||
"no_persistent_write_found_equals_side_effect_free",
|
||||
"host_tests_are_hardware_evidence",
|
||||
"missing_observation_means_safe_absence",
|
||||
}
|
||||
if any(claim_boundaries.get(field) is not False for field in required_false_claims):
|
||||
errors.append("a prohibited safety or evidence claim was enabled")
|
||||
|
||||
stock = manifest.get("stock_identities", {})
|
||||
if (
|
||||
stock.get("classification") != "reference_only"
|
||||
or stock.get("current_device_observed") is not False
|
||||
):
|
||||
errors.append("stock identities were promoted from reference-only")
|
||||
elfldr = stock.get("elfldr", {})
|
||||
if (
|
||||
elfldr.get("size") != 397000
|
||||
or elfldr.get("sha256") != STOCK_ELFLDR_SHA256
|
||||
or elfldr.get("current_device_match") != "UNPROVEN"
|
||||
):
|
||||
errors.append("stock elfldr reference changed or was promoted")
|
||||
manager = stock.get("payload_manager", {})
|
||||
if (
|
||||
manager.get("size") != 2050320
|
||||
or manager.get("sha256") != STOCK_PAYLOAD_MANAGER_SHA256
|
||||
or manager.get("current_device_match") != "UNPROVEN"
|
||||
):
|
||||
errors.append("stock Payload Manager reference changed or was promoted")
|
||||
|
||||
backup = manifest.get("payload_manager_backup", {})
|
||||
if backup != {
|
||||
"classification": "hard_blocker",
|
||||
"on_device_proven": False,
|
||||
"byte_exact_proven": False,
|
||||
"creation_allowed_in_strict_read_only_phase": False,
|
||||
"result": "HARD_STOP-GATE",
|
||||
}:
|
||||
errors.append("Payload Manager backup hard blocker changed")
|
||||
|
||||
entries = denylist.get("entries", [])
|
||||
if (
|
||||
denylist.get("fail_closed") is not True
|
||||
or len(entries) != 1
|
||||
or entries[0].get("sha256") != DENYLIST_SHA256
|
||||
or entries[0].get("status") != "BLOCKED"
|
||||
or entries[0].get("permanent") is not True
|
||||
or entries[0].get("execution_eligible") is not False
|
||||
):
|
||||
errors.append("permanent denylist binding changed")
|
||||
|
||||
blockers = {
|
||||
item.get("id"): item.get("severity") for item in manifest.get("blockers", [])
|
||||
}
|
||||
if blockers != REQUIRED_BLOCKERS:
|
||||
errors.append("remediation blocker set changed")
|
||||
|
||||
findings = {
|
||||
item.get("id"): item for item in manifest.get("side_effect_findings", [])
|
||||
}
|
||||
if set(findings) != REQUIRED_FINDINGS:
|
||||
errors.append("Payload Manager side-effect finding set changed")
|
||||
for finding_id, finding in findings.items():
|
||||
if finding.get("strict_read_only_preflight_suitable") is not False:
|
||||
errors.append(f"{finding_id}: incorrectly marked strict-read-only suitable")
|
||||
references = finding.get("source_references")
|
||||
if not isinstance(references, list) or not references:
|
||||
errors.append(f"{finding_id}: source references are absent")
|
||||
if finding_id != "options_any_endpoint":
|
||||
if finding.get("http_method") == "OPTIONS":
|
||||
errors.append(f"{finding_id}: non-OPTIONS finding mislabeled")
|
||||
if finding.get("writes_server_active_flag") is not True:
|
||||
errors.append(f"{finding_id}: server_active_flag mutation hidden")
|
||||
options = findings.get("options_any_endpoint", {})
|
||||
if (
|
||||
options.get("http_method") != "OPTIONS"
|
||||
or options.get("writes_server_active_flag") is not False
|
||||
or options.get("strict_read_only_preflight_suitable") is not False
|
||||
):
|
||||
errors.append("OPTIONS route classification changed")
|
||||
autoload = findings.get("get_autoload_status", {})
|
||||
if (
|
||||
autoload.get("endpoint") != "/autoload_status"
|
||||
or autoload.get("writes_autoload_triggered") is not True
|
||||
or autoload.get("reads_filesystem_or_configuration") is not True
|
||||
):
|
||||
errors.append("/autoload_status mutations or reads were hidden")
|
||||
|
||||
source = manifest.get("payload_manager_source", {})
|
||||
if (
|
||||
source.get("commit") != PAYLOAD_MANAGER_COMMIT
|
||||
or source.get("release") != "v0.3.1"
|
||||
):
|
||||
errors.append("Payload Manager source identity changed")
|
||||
source_files = source.get("files")
|
||||
if not isinstance(source_files, list) or len(source_files) < 3:
|
||||
errors.append("Payload Manager source-file evidence is incomplete")
|
||||
|
||||
observations = {
|
||||
item.get("id"): item for item in manifest.get("evidence_contract", [])
|
||||
}
|
||||
if set(observations) != REQUIRED_OBSERVATIONS:
|
||||
errors.append("future evidence-contract observation set changed")
|
||||
for observation_id, observation in observations.items():
|
||||
if observation.get("confidence") != "UNPROVEN":
|
||||
errors.append(f"{observation_id}: confidence was promoted")
|
||||
if observation.get("timeout_ms") is not None:
|
||||
errors.append(f"{observation_id}: timeout invented before tool review")
|
||||
if observation.get("fail_closed_result") != "STOP":
|
||||
errors.append(f"{observation_id}: fail-closed result changed")
|
||||
identity = observation.get("required_collector_identity")
|
||||
output = observation.get("reviewer_output")
|
||||
if not isinstance(identity, list) or not identity:
|
||||
errors.append(f"{observation_id}: collector identity contract absent")
|
||||
if not isinstance(output, list) or not output:
|
||||
errors.append(f"{observation_id}: reviewer output contract absent")
|
||||
|
||||
prohibited = set(manifest.get("prohibited_actions", []))
|
||||
if prohibited != REQUIRED_PROHIBITED_ACTIONS:
|
||||
errors.append("prohibited-action set changed")
|
||||
future = manifest.get("future_activity", {})
|
||||
if (
|
||||
future.get("mode") != "design_only"
|
||||
or future.get("bounded_observation_implemented") is not False
|
||||
or future.get("collector_selected") is not False
|
||||
or future.get("transfer_method_selected") is not False
|
||||
or future.get("execution_method_selected") is not False
|
||||
or future.get("new_explicit_authorization_required") is not True
|
||||
):
|
||||
errors.append("future bounded observation was promoted beyond design")
|
||||
retroarch = manifest.get("retroarch", {})
|
||||
if retroarch != {
|
||||
"goal": "long_term_goal",
|
||||
"active_phase": False,
|
||||
"work_started": False,
|
||||
"dependency_chain_only": True,
|
||||
}:
|
||||
errors.append("RetroArch was promoted into the active phase")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def validate_template(template: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if template.get("template_only") is not True:
|
||||
errors.append("bounded-observation template is not template-only")
|
||||
for field in (
|
||||
"authorized",
|
||||
"execution_authorized",
|
||||
"transfer_authorized",
|
||||
"installation_authorized",
|
||||
"lifecycle_authorized",
|
||||
"automatic_retry",
|
||||
):
|
||||
if template.get(field) is not False:
|
||||
errors.append(f"template field {field} is not false")
|
||||
required = template.get("required_fields", {})
|
||||
if set(required) != TEMPLATE_REQUIRED_FIELDS:
|
||||
errors.append("bounded-observation required-field set changed")
|
||||
elif any(value is not None for value in required.values()):
|
||||
errors.append("bounded-observation template contains prefilled request data")
|
||||
exclusions = template.get("fixed_exclusions", {})
|
||||
expected_exclusions = {
|
||||
"installation",
|
||||
"lifecycle_probe",
|
||||
"autoload",
|
||||
"automatic_retry",
|
||||
"gnm",
|
||||
"videoout",
|
||||
"sdl",
|
||||
"retroarch",
|
||||
}
|
||||
if set(exclusions) != expected_exclusions or any(
|
||||
value is not True for value in exclusions.values()
|
||||
):
|
||||
errors.append("bounded-observation fixed exclusions changed")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_doc_contract(
|
||||
contract: dict[str, Any], manifest: dict[str, Any]
|
||||
) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if contract.get("status") != manifest.get("status"):
|
||||
errors.append("documentation/manifest status mismatch")
|
||||
if contract.get("authorization") != manifest.get("authorization"):
|
||||
errors.append("documentation/manifest authorization mismatch")
|
||||
blocker_ids = [item.get("id") for item in manifest.get("blockers", [])]
|
||||
if contract.get("blockers") != blocker_ids:
|
||||
errors.append("documentation/manifest blocker mismatch")
|
||||
if contract.get("firmware_runtime_behavior") != "UNPROVEN":
|
||||
errors.append("documentation promoted firmware behavior")
|
||||
if contract.get("stock_identity_classification") != "reference_only":
|
||||
errors.append("documentation promoted stock identities")
|
||||
if contract.get("payload_manager_backup_classification") != "hard_blocker":
|
||||
errors.append("documentation weakened the manager backup blocker")
|
||||
if contract.get("retroarch_active_phase") is not False:
|
||||
errors.append("documentation promoted RetroArch into the active phase")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_immutable_evidence(root: Path) -> list[str]:
|
||||
errors: list[str] = []
|
||||
for relative, expected in IMMUTABLE_EVIDENCE.items():
|
||||
path = root / relative
|
||||
if not path.is_file():
|
||||
errors.append(f"immutable evidence missing: {relative}")
|
||||
elif sha256(path) != expected["sha256"]:
|
||||
errors.append(f"immutable evidence hash mismatch: {relative}")
|
||||
return errors
|
||||
|
||||
|
||||
def changed_paths(root: Path) -> list[Path]:
|
||||
result = subprocess.run(
|
||||
["git", "status", "--porcelain=v1", "--untracked-files=all"],
|
||||
cwd=root,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
paths: list[Path] = []
|
||||
for line in result.stdout.splitlines():
|
||||
value = line[3:]
|
||||
for candidate in value.split(" -> "):
|
||||
candidate = candidate.strip('"')
|
||||
paths.append(Path(candidate))
|
||||
return paths
|
||||
|
||||
|
||||
def validate_changed_files(root: Path) -> list[str]:
|
||||
errors: list[str] = []
|
||||
for path in changed_paths(root):
|
||||
if path.suffix.lower() in TARGET_SUFFIXES:
|
||||
errors.append(f"target artifact appears in change set: {path}")
|
||||
normalized = path.as_posix()
|
||||
if normalized.startswith(
|
||||
("src/", "include/", "samples/", "adapters/", "work/upstream/")
|
||||
):
|
||||
errors.append(f"production/runtime source changed in remediation: {path}")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_local_payload_manager_source(
|
||||
root: Path, manifest: dict[str, Any]
|
||||
) -> list[str]:
|
||||
errors: list[str] = []
|
||||
source_root = root / "work/upstream/pldmgr-v0.3.1"
|
||||
if not source_root.is_dir():
|
||||
return ["required local Payload Manager source checkout is absent"]
|
||||
head = subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
cwd=source_root,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
if head != PAYLOAD_MANAGER_COMMIT:
|
||||
errors.append("local Payload Manager source commit changed")
|
||||
status = subprocess.run(
|
||||
["git", "status", "--porcelain"],
|
||||
cwd=source_root,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
if status:
|
||||
errors.append("local Payload Manager source checkout is dirty")
|
||||
for item in manifest["payload_manager_source"]["files"]:
|
||||
path = source_root / item["path"]
|
||||
if not path.is_file():
|
||||
errors.append(f"Payload Manager source file missing: {item['path']}")
|
||||
elif sha256(path) != item["sha256"]:
|
||||
errors.append(f"Payload Manager source hash mismatch: {item['path']}")
|
||||
return errors
|
||||
|
||||
|
||||
def collect_errors(root: Path, require_local_source: bool = False) -> list[str]:
|
||||
manifest = load_json(root / "manifests/runtime/phase-0.8-remediation.json")
|
||||
denylist = load_json(root / "manifests/artifact-denylist.json")
|
||||
doc_contract = extract_json_contract(
|
||||
root / "docs/runtime/phase-0.8-remediation.md", "PHASE08R_CONTRACT"
|
||||
)
|
||||
template = extract_json_contract(
|
||||
root / "docs/approvals/phase-0.8-bounded-observation-template.md",
|
||||
"PHASE08_BOUNDED_OBSERVATION_TEMPLATE",
|
||||
)
|
||||
errors = validate_immutable_evidence(root)
|
||||
errors.extend(validate_manifest(manifest, denylist))
|
||||
errors.extend(validate_doc_contract(doc_contract, manifest))
|
||||
errors.extend(validate_template(template))
|
||||
errors.extend(validate_changed_files(root))
|
||||
if require_local_source:
|
||||
errors.extend(validate_local_payload_manager_source(root, manifest))
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
parser.add_argument(
|
||||
"--require-local-source",
|
||||
action="store_true",
|
||||
help="also require and rehash the ignored pinned Payload Manager checkout",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
root = args.root.resolve()
|
||||
errors = collect_errors(root, args.require_local_source)
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"Phase-0.8R validation failed: {error}")
|
||||
return 1
|
||||
print(
|
||||
"Phase-0.8R remediation validation passed: "
|
||||
f"{len(IMMUTABLE_EVIDENCE)} immutable files, "
|
||||
f"{len(REQUIRED_FINDINGS)} side-effect findings, "
|
||||
f"{len(REQUIRED_OBSERVATIONS)} observation contracts, "
|
||||
f"{len(REQUIRED_BLOCKERS)} blockers; hardware evidence not claimed"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (
|
||||
KeyError,
|
||||
OSError,
|
||||
subprocess.CalledProcessError,
|
||||
TypeError,
|
||||
ValueError,
|
||||
json.JSONDecodeError,
|
||||
) as error:
|
||||
print(f"Phase-0.8R validation failed: {error}")
|
||||
raise SystemExit(1) from error
|
||||
@@ -0,0 +1,464 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate the blocked, offline-only Phase-0.9B observer audit."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
EXPECTED_DECISION = {
|
||||
"OBSERVER_STARTUP_OR_EXIT_ABI_UNPROVEN",
|
||||
"NO_PROVEN_NON_PERSISTENT_OUTPUT_CHANNEL",
|
||||
}
|
||||
AUTHORIZATION_FIELDS = (
|
||||
"authorized",
|
||||
"transfer_authorized",
|
||||
"execution_authorized",
|
||||
"installation_authorized",
|
||||
"lifecycle_authorized",
|
||||
"autoload_authorized",
|
||||
"backup_creation_authorized",
|
||||
"automatic_retry",
|
||||
)
|
||||
SOURCE_COMMITS = {
|
||||
"hardened_elfldr": ("../chimera-elfldr", "197623058f509eddde18868dafcb92fdcac66464"),
|
||||
"controlled_payload_manager": (
|
||||
"../chimera-ps5-payload-manager",
|
||||
"e23d94ff91233aa770e2342800c1467875bdef44",
|
||||
),
|
||||
"elfldr_public_base": (
|
||||
"work/upstream/elfldr-v0.23",
|
||||
"699e8bcff03e91e8d6ca6eba281af25c5a58d8c2",
|
||||
),
|
||||
"payload_manager_public_base": (
|
||||
"work/upstream/pldmgr-v0.3.1",
|
||||
"cfbc70f30f419b09bf2b52283f7409e2d3117ee1",
|
||||
),
|
||||
"ps5_payload_sdk_v0_41": (
|
||||
"work/upstream/sdk",
|
||||
"d2e2e585740362976a39fdd5ccf390f199a7bc37",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
value = json.load(handle)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{path} must contain a JSON object")
|
||||
return value
|
||||
|
||||
|
||||
def extract_json_contract(path: Path, marker: str) -> dict[str, Any]:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
pattern = (
|
||||
rf"<!-- BEGIN {re.escape(marker)} -->\s*```json\s*(.*?)\s*```\s*"
|
||||
rf"<!-- END {re.escape(marker)} -->"
|
||||
)
|
||||
match = re.search(pattern, text, flags=re.DOTALL)
|
||||
if not match:
|
||||
raise ValueError(f"{path} is missing {marker}")
|
||||
value = json.loads(match.group(1))
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{marker} must be a JSON object")
|
||||
return value
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def git_output(root: Path, *args: str) -> str:
|
||||
return subprocess.check_output(
|
||||
["git", *args], cwd=root, text=True, encoding="utf-8"
|
||||
).strip()
|
||||
|
||||
|
||||
def validate_authorizations(value: dict[str, Any], prefix: str) -> list[str]:
|
||||
errors: list[str] = []
|
||||
for field in AUTHORIZATION_FIELDS:
|
||||
if value.get(field) is not False:
|
||||
errors.append(f"{prefix}.{field} must be false")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_schema(schema: dict[str, Any], manifest: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
properties = schema.get("properties", {})
|
||||
for field in AUTHORIZATION_FIELDS:
|
||||
if properties.get(field, {}).get("const") is not False:
|
||||
errors.append(f"schema {field} must be const false")
|
||||
if properties.get("device_address", {}).get("const", "missing") is not None:
|
||||
errors.append("schema device_address must be const null")
|
||||
if properties.get("maximum_execution_count", {}).get("const") != 0:
|
||||
errors.append("schema maximum_execution_count must be const zero")
|
||||
|
||||
default = schema.get("x-chimera-default-plan")
|
||||
if default != manifest.get("default_observation_plan"):
|
||||
errors.append("schema default plan must equal manifest default plan")
|
||||
if not isinstance(default, dict):
|
||||
return errors
|
||||
errors.extend(validate_authorizations(default, "default_plan"))
|
||||
if default.get("device_address") is not None:
|
||||
errors.append("default plan contains a device address")
|
||||
if default.get("device_identity") is not None:
|
||||
errors.append("default plan contains a device identity")
|
||||
if default.get("read_paths") != []:
|
||||
errors.append("default plan contains read paths")
|
||||
if default.get("allowed_observations") != []:
|
||||
errors.append("default plan contains observations")
|
||||
if default.get("output_channel") is not None:
|
||||
errors.append("default plan contains an output channel")
|
||||
if default.get("maximum_execution_count") != 0:
|
||||
errors.append("default plan permits an execution")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_manifest(root: Path, manifest: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if manifest.get("status") != "BLOCKED":
|
||||
errors.append("Phase-0.9B status must be BLOCKED")
|
||||
if set(manifest.get("decision", [])) != EXPECTED_DECISION:
|
||||
errors.append("Phase-0.9B hard-gate decision changed")
|
||||
errors.extend(
|
||||
validate_authorizations(manifest.get("authorization", {}), "authorization")
|
||||
)
|
||||
|
||||
canonical = manifest.get("canonical_state_preserved", {})
|
||||
expected_canonical = {
|
||||
"historical_phase08_status": "READ_ONLY_PREFLIGHT_BLOCKED",
|
||||
"phase09a_status": "DESIGN_ONLY",
|
||||
"firmware_runtime_behavior": "UNPROVEN",
|
||||
"stock_hashes": "reference_only",
|
||||
"payload_manager_backup": "HARD_BLOCKER",
|
||||
"device_contact_performed": False,
|
||||
"device_transfer_performed": False,
|
||||
"device_execution_performed": False,
|
||||
}
|
||||
for field, expected in expected_canonical.items():
|
||||
if canonical.get(field) != expected:
|
||||
errors.append(f"canonical state {field} must be {expected!r}")
|
||||
|
||||
deny_binding = manifest.get("permanent_denylist_binding", {})
|
||||
if deny_binding != {
|
||||
"sha256": "4be1c17b4964f2b68c39b5145bc4af4619c32512d60269ecf5c39728b390fa63",
|
||||
"status": "BLOCKED",
|
||||
"permanent": True,
|
||||
"execution_eligible": False,
|
||||
}:
|
||||
errors.append("permanent denylist binding changed")
|
||||
|
||||
commits = manifest.get("source_commits", {})
|
||||
tree_status = manifest.get("source_tree_status", {})
|
||||
for key, (relative, expected_commit) in SOURCE_COMMITS.items():
|
||||
if commits.get(key) != expected_commit:
|
||||
errors.append(f"manifest source commit mismatch: {key}")
|
||||
path = (root / relative).resolve()
|
||||
if not path.exists():
|
||||
errors.append(f"source tree missing: {relative}")
|
||||
continue
|
||||
try:
|
||||
actual_commit = git_output(path, "rev-parse", "HEAD")
|
||||
dirty = git_output(path, "status", "--porcelain")
|
||||
except (OSError, subprocess.CalledProcessError) as exc:
|
||||
errors.append(f"source tree unreadable: {relative}: {exc}")
|
||||
continue
|
||||
if actual_commit != expected_commit:
|
||||
errors.append(f"source tree commit mismatch: {relative}")
|
||||
if dirty:
|
||||
errors.append(f"source tree is dirty: {relative}")
|
||||
if tree_status.get(key) != "clean":
|
||||
errors.append(f"manifest does not classify {key} as clean")
|
||||
|
||||
for evidence in manifest.get("source_evidence", []):
|
||||
relative = evidence.get("path")
|
||||
expected_digest = evidence.get("sha256")
|
||||
if not isinstance(relative, str) or not isinstance(expected_digest, str):
|
||||
errors.append("source evidence entry lacks path or SHA-256")
|
||||
continue
|
||||
path = (root / relative).resolve()
|
||||
if not path.is_file():
|
||||
errors.append(f"source evidence file missing: {relative}")
|
||||
continue
|
||||
if sha256_file(path) != expected_digest:
|
||||
errors.append(f"source evidence digest mismatch: {relative}")
|
||||
if "size" in evidence and path.stat().st_size != evidence["size"]:
|
||||
errors.append(f"source evidence size mismatch: {relative}")
|
||||
|
||||
matrix = manifest.get("capability_matrix", [])
|
||||
expected_facts = {
|
||||
"firmware_source_1",
|
||||
"firmware_source_2",
|
||||
"file_metadata",
|
||||
"object_identity",
|
||||
"sha256",
|
||||
"mount_information",
|
||||
"processes_services",
|
||||
"listeners",
|
||||
"autoload_configuration",
|
||||
"output_channel",
|
||||
"monotonic_time_deadline",
|
||||
"process_exit",
|
||||
}
|
||||
if {entry.get("needed_fact") for entry in matrix} != expected_facts:
|
||||
errors.append("capability matrix is incomplete or changed")
|
||||
if any(entry.get("implement") is not False for entry in matrix):
|
||||
errors.append("a blocked capability is marked for implementation")
|
||||
|
||||
gate = manifest.get("build_gate", {})
|
||||
for field in (
|
||||
"startup_and_exit_abi_proven",
|
||||
"non_persistent_output_channel_proven",
|
||||
"normal_sdk_crt_kernelwrite_free",
|
||||
"custom_freestanding_cleanup_proven",
|
||||
"observer_source_created",
|
||||
"observer_target_declared",
|
||||
"target_build_performed",
|
||||
"double_clean_build_performed",
|
||||
):
|
||||
if gate.get(field) is not False:
|
||||
errors.append(f"build gate {field} must be false")
|
||||
if gate.get("reason") != "BLOCKED_BEFORE_SOURCE_AND_BUILD":
|
||||
errors.append("build gate reason changed")
|
||||
|
||||
implementation = manifest.get("implementation", {})
|
||||
if implementation.get("observer_logic_implemented") is not False:
|
||||
errors.append("observer logic must remain unimplemented")
|
||||
if implementation.get("observations_implemented") != []:
|
||||
errors.append("target observations must remain unimplemented")
|
||||
|
||||
artifact = manifest.get("artifact", {})
|
||||
if artifact.get("present") is not False:
|
||||
errors.append("observer artifact must be absent")
|
||||
for field in ("path", "sha256", "size"):
|
||||
if artifact.get(field) is not None:
|
||||
errors.append(f"artifact {field} must be null")
|
||||
for field in (
|
||||
"imports",
|
||||
"undefined_symbols",
|
||||
"dynamic_dependencies",
|
||||
"network_functions",
|
||||
"filesystem_reads",
|
||||
):
|
||||
if artifact.get(field) != []:
|
||||
errors.append(f"artifact {field} must be empty")
|
||||
for field in (
|
||||
"installation_eligible",
|
||||
"lifecycle_eligible",
|
||||
"autoload_eligible",
|
||||
"execution_authorized",
|
||||
"execution_eligible",
|
||||
):
|
||||
if artifact.get(field) is not False:
|
||||
errors.append(f"artifact {field} must be false")
|
||||
|
||||
if (
|
||||
manifest.get("static_artifact_audit", {}).get("status")
|
||||
!= "NOT_PERFORMED_BLOCKED_BEFORE_BUILD"
|
||||
):
|
||||
errors.append("artifact audit must be recorded as not performed")
|
||||
reproducibility = manifest.get("reproducibility", {})
|
||||
if reproducibility.get("status") != "NOT_PERFORMED_BLOCKED_BEFORE_BUILD":
|
||||
errors.append("reproducibility must be recorded as not performed")
|
||||
for field in ("build_1_sha256", "build_2_sha256", "byte_identical"):
|
||||
if reproducibility.get(field) is not None:
|
||||
errors.append(f"reproducibility {field} must be null")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_denylist(root: Path) -> list[str]:
|
||||
denylist = load_json(root / "manifests/artifact-denylist.json")
|
||||
for entry in denylist.get("entries", []):
|
||||
if (
|
||||
entry.get("sha256")
|
||||
== "4be1c17b4964f2b68c39b5145bc4af4619c32512d60269ecf5c39728b390fa63"
|
||||
and entry.get("status") == "BLOCKED"
|
||||
and entry.get("permanent") is True
|
||||
and entry.get("execution_eligible") is False
|
||||
):
|
||||
return []
|
||||
return ["permanent blocked artifact is missing from the denylist"]
|
||||
|
||||
|
||||
def validate_repository_boundary(root: Path) -> list[str]:
|
||||
errors: list[str] = []
|
||||
cmake = (root / "CMakeLists.txt").read_text(encoding="utf-8")
|
||||
forbidden_cmake = (
|
||||
"CHIMERA_GFX_BUILD_PS5_OBSERVER",
|
||||
"chimera-gfx-bounded-observer",
|
||||
"phase09b-observer.elf",
|
||||
)
|
||||
for value in forbidden_cmake:
|
||||
if value in cmake:
|
||||
errors.append(f"blocked observer target found in CMake: {value}")
|
||||
|
||||
forbidden_paths = (
|
||||
root / "samples/phase09b_observer",
|
||||
root / "samples/bounded_observer",
|
||||
root / "src/observer",
|
||||
root / "packaging/phase09b/observer.elf",
|
||||
)
|
||||
for path in forbidden_paths:
|
||||
if path.exists():
|
||||
errors.append(f"blocked observer source/artifact path exists: {path}")
|
||||
|
||||
for base in (root / "samples", root / "src", root / "packaging"):
|
||||
for path in base.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
lowered = path.as_posix().lower()
|
||||
if path.suffix.lower() in {".c", ".cc", ".cpp", ".s", ".asm"} and (
|
||||
"phase09b" in lowered or "bounded_observer" in lowered
|
||||
):
|
||||
errors.append(f"blocked observer target source exists: {path}")
|
||||
if path.suffix.lower() in {".elf", ".self", ".sprx", ".pkg", ".map"} and (
|
||||
"phase09b" in lowered or "observer" in lowered
|
||||
):
|
||||
errors.append(f"blocked observer target artifact exists: {path}")
|
||||
|
||||
try:
|
||||
tracked = git_output(root, "ls-files").splitlines()
|
||||
except (OSError, subprocess.CalledProcessError) as exc:
|
||||
return [f"could not inspect tracked files: {exc}"]
|
||||
for relative in tracked:
|
||||
lowered = relative.lower()
|
||||
if "phase09b" in lowered or "phase-0.9b" in lowered:
|
||||
if lowered.endswith((".elf", ".self", ".sprx", ".pkg", ".map")):
|
||||
errors.append(f"tracked Phase-0.9B target artifact exists: {relative}")
|
||||
if "phase09b" in lowered and (
|
||||
"install" in lowered or "lifecycle-package" in lowered
|
||||
):
|
||||
errors.append(f"Phase-0.9B install/lifecycle package exists: {relative}")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_review_checksums(root: Path) -> list[str]:
|
||||
errors: list[str] = []
|
||||
expected_paths = {
|
||||
"docs/approvals/phase-0.9b-observer-execution-template.md",
|
||||
"docs/runtime/phase-0.9b-bounded-observer-design.md",
|
||||
"docs/runtime/phase-0.9b-observer-limitations.md",
|
||||
"docs/runtime/phase-0.9b-observer-result-contract.md",
|
||||
"docs/runtime/phase-0.9b-observer-static-audit.md",
|
||||
"manifests/runtime/phase-0.9b-observation-plan.schema.json",
|
||||
"manifests/runtime/phase-0.9b-observer.json",
|
||||
"tests/phase09b_observer_model.py",
|
||||
"tests/test_phase09b_observer_audit.py",
|
||||
"tools/validate_phase09b_observer_audit.py",
|
||||
}
|
||||
checksum_path = root / "packaging/phase09b/SHA256SUMS.txt"
|
||||
observed: set[str] = set()
|
||||
for line_number, line in enumerate(
|
||||
checksum_path.read_text(encoding="utf-8").splitlines(), start=1
|
||||
):
|
||||
match = re.fullmatch(r"([0-9a-f]{64}) ([^\r\n]+)", line)
|
||||
if not match:
|
||||
errors.append(f"invalid checksum line {line_number}")
|
||||
continue
|
||||
expected_digest, relative = match.groups()
|
||||
if relative in observed:
|
||||
errors.append(f"duplicate checksum path: {relative}")
|
||||
continue
|
||||
observed.add(relative)
|
||||
path = root / relative
|
||||
if not path.is_file():
|
||||
errors.append(f"checksummed file missing: {relative}")
|
||||
elif sha256_file(path) != expected_digest:
|
||||
errors.append(f"review checksum mismatch: {relative}")
|
||||
if observed != expected_paths:
|
||||
errors.append("Phase-0.9B review checksum inventory is incomplete or expanded")
|
||||
return errors
|
||||
|
||||
|
||||
def collect_errors(root: Path, require_source_trees: bool = True) -> list[str]:
|
||||
errors: list[str] = []
|
||||
manifest_path = root / "manifests/runtime/phase-0.9b-observer.json"
|
||||
schema_path = root / "manifests/runtime/phase-0.9b-observation-plan.schema.json"
|
||||
required = [
|
||||
manifest_path,
|
||||
schema_path,
|
||||
root / "docs/runtime/phase-0.9b-bounded-observer-design.md",
|
||||
root / "docs/runtime/phase-0.9b-observer-static-audit.md",
|
||||
root / "docs/runtime/phase-0.9b-observer-result-contract.md",
|
||||
root / "docs/runtime/phase-0.9b-observer-limitations.md",
|
||||
root / "docs/approvals/phase-0.9b-observer-execution-template.md",
|
||||
root / "tests/phase09b_observer_model.py",
|
||||
root / "tests/test_phase09b_observer_audit.py",
|
||||
root / "packaging/phase09b/SHA256SUMS.txt",
|
||||
]
|
||||
for path in required:
|
||||
if not path.is_file():
|
||||
errors.append(f"required Phase-0.9B file missing: {path}")
|
||||
if errors:
|
||||
return errors
|
||||
|
||||
manifest = load_json(manifest_path)
|
||||
schema = load_json(schema_path)
|
||||
errors.extend(validate_manifest(root, manifest))
|
||||
if not require_source_trees:
|
||||
errors = [
|
||||
error
|
||||
for error in errors
|
||||
if not error.startswith(("source tree missing:", "source tree unreadable:"))
|
||||
]
|
||||
errors.extend(validate_schema(schema, manifest))
|
||||
errors.extend(validate_denylist(root))
|
||||
errors.extend(validate_repository_boundary(root))
|
||||
errors.extend(validate_review_checksums(root))
|
||||
|
||||
template = extract_json_contract(
|
||||
root / "docs/approvals/phase-0.9b-observer-execution-template.md",
|
||||
"PHASE09B_OBSERVER_EXECUTION_TEMPLATE",
|
||||
)
|
||||
errors.extend(validate_authorizations(template, "execution_template"))
|
||||
if template.get("status") != "BLOCKED":
|
||||
errors.append("execution template must remain BLOCKED")
|
||||
required_fields = template.get("required_fields", {})
|
||||
if any(value is not None for value in required_fields.values()):
|
||||
errors.append("execution template contains prefilled request-specific values")
|
||||
|
||||
for relative in (
|
||||
"docs/runtime/phase-0.9b-bounded-observer-design.md",
|
||||
"docs/runtime/phase-0.9b-observer-limitations.md",
|
||||
):
|
||||
text = (root / relative).read_text(encoding="utf-8")
|
||||
normalized_text = re.sub(r"[^A-Z0-9]+", " ", text.upper()).strip()
|
||||
for decision in EXPECTED_DECISION:
|
||||
if decision.replace("_", " ") not in normalized_text:
|
||||
errors.append(f"{relative} does not state blocker {decision}")
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
parser.add_argument(
|
||||
"--no-source-tree-check",
|
||||
action="store_true",
|
||||
help="Skip only missing external source-tree errors for packaged review.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
root = args.root.resolve()
|
||||
errors = collect_errors(root, require_source_trees=not args.no_source_tree_check)
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"ERROR: {error}", file=sys.stderr)
|
||||
return 1
|
||||
print("Phase-0.9B blocked observer audit: PASS")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,687 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate the blocked, host-only Phase-0.9C feasibility closure."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
|
||||
EXPECTED_CLASSIFICATION = "BLOCKED_MULTIPLE_FOUNDATIONAL_CONTRACTS"
|
||||
EXPECTED_BASELINE = "3ddc213ea67bb286256ae42e52c65e00488608ca"
|
||||
EXPECTED_BRANCH = "codex/chimera-gfx-phase09c-execution-feasibility"
|
||||
BLOCKED_HASH = (
|
||||
"4be1c17b4964f2b68c39b5145bc4af4619c32512d60269ecf5c39728b390fa63"
|
||||
)
|
||||
|
||||
AUTHORIZATION_FIELDS = (
|
||||
"authorized",
|
||||
"transfer_authorized",
|
||||
"execution_authorized",
|
||||
"installation_authorized",
|
||||
"lifecycle_authorized",
|
||||
"autoload_authorized",
|
||||
"backup_creation_authorized",
|
||||
"observer_build_authorized",
|
||||
"automatic_retry",
|
||||
)
|
||||
|
||||
SOURCE_COMMITS = {
|
||||
"hardened_elfldr": (
|
||||
"../chimera-elfldr",
|
||||
"197623058f509eddde18868dafcb92fdcac66464",
|
||||
),
|
||||
"controlled_payload_manager": (
|
||||
"../chimera-ps5-payload-manager",
|
||||
"e23d94ff91233aa770e2342800c1467875bdef44",
|
||||
),
|
||||
"elfldr_public_base": (
|
||||
"work/upstream/elfldr-v0.23",
|
||||
"699e8bcff03e91e8d6ca6eba281af25c5a58d8c2",
|
||||
),
|
||||
"payload_manager_public_base": (
|
||||
"work/upstream/pldmgr-v0.3.1",
|
||||
"cfbc70f30f419b09bf2b52283f7409e2d3117ee1",
|
||||
),
|
||||
"ps5_payload_sdk_v0_41": (
|
||||
"work/upstream/sdk",
|
||||
"d2e2e585740362976a39fdd5ccf390f199a7bc37",
|
||||
),
|
||||
}
|
||||
|
||||
SOURCE_HASHES = {
|
||||
"../chimera-elfldr/elfldr.c": (
|
||||
"9949f8e4037984d10f1f5aa498e4665593d1fac8a33614d7f2141349839bb803"
|
||||
),
|
||||
"../chimera-elfldr/socksrv.c": (
|
||||
"d642ced3e9b4a296dd15e355050ebe956f53a6dfdaa6ac10109cd067a3bba3d7"
|
||||
),
|
||||
"../chimera-elfldr/controlled_runtime.c": (
|
||||
"10145f3bbb3b54e3d715b1667e45c6f12d3c7f52a04342ab104ec8dd49e384c9"
|
||||
),
|
||||
"../chimera-elfldr/ps5_controlled.c": (
|
||||
"68717ef1cc31c483743c5af325c59e73a90f66e93b880b989e349a4ddc748772"
|
||||
),
|
||||
"../chimera-ps5-payload-manager/src/verified_launcher.c": (
|
||||
"066100ca4917c7acc560e2e85666ca136cd7ccfd9094417377048f41106dd56e"
|
||||
),
|
||||
"../chimera-ps5-payload-manager/src/ps5_launcher.c": (
|
||||
"29c1a5fd01784a59e88b3698940f120cb03020071bc2b7d74a1da1a51524ef59"
|
||||
),
|
||||
"work/upstream/sdk/crt/crt.c": (
|
||||
"3875f4739ec40b33f1f4967a1acbb585a527d2c281a360153e1f69c8b945932a"
|
||||
),
|
||||
"work/upstream/sdk/crt/patch.c": (
|
||||
"4f76a677bba54f4641e1cf2755768c29afb7464a35b2f21e85db2aa2785eceac"
|
||||
),
|
||||
"work/upstream/sdk/crt/Makefile": (
|
||||
"6a62f777f32ab05cbe7bff81c00cfd6639ba8bf440aba611a40ff0b62732568b"
|
||||
),
|
||||
"work/upstream/sdk/host/bin/prospero-clang": (
|
||||
"0cf49ae43d6110a7606c0ee4d702fc4b5d5e1c3ae9a722945a48b80294e295ba"
|
||||
),
|
||||
"work/upstream/sdk/host/elf_x86_64.x": (
|
||||
"169b80d01da601ef96bbc584986608dec1d9c01397eae81eccd4e1a66b0a6c6a"
|
||||
),
|
||||
"work/upstream/sdk/crt/kernel.c": (
|
||||
"ac1c375aae8d3cb1be5fb8bad2f4e6492b6ec1f1450977b534202f46b5b70321"
|
||||
),
|
||||
"work/upstream/sdk/sce_stubs/libkernel_web.c": (
|
||||
"dca70757a0680ede52502fe7db10060fe30506f7ed0adfc43ce00700145ab4ff"
|
||||
),
|
||||
"samples/lifecycle_probe/main.c": (
|
||||
"1ae7df1fe921ccab2a252f77975d3d441ef7725e34535b024580c0d4a242d766"
|
||||
),
|
||||
}
|
||||
|
||||
IMMUTABLE_HASHES = {
|
||||
"docs/runtime/phase-0.8-read-only-preflight.md": (
|
||||
"3fbe086175a6048176075f447ec1482074928e3b5282db97ea2169395fe1d508"
|
||||
),
|
||||
"manifests/runtime/phase-0.8-read-only-preflight.json": (
|
||||
"47d7f452f8799979fe99b3e6d56859f03544112725bf7e5b349eba5ed81b3322"
|
||||
),
|
||||
"tests/test_phase08_preflight.py": (
|
||||
"8a4ad7c70de28ffe3148fd3fd1f68c36a872c53c691c9068e1ff163970863c48"
|
||||
),
|
||||
"manifests/runtime/phase-0.9-anti-brick-design.json": (
|
||||
"39fd7c70cae998d9d74e7caf1ff3c19f9f76f7de2e5a50bc09baede4ba53e9e9"
|
||||
),
|
||||
"manifests/runtime/phase-0.9b-observer.json": (
|
||||
"104c4a667ad17f9827fc7276852c6faeef96effb8e3561a5ebc19a62c7d51634"
|
||||
),
|
||||
"manifests/runtime/phase-0.9b-observation-plan.schema.json": (
|
||||
"efcea3b0001ef5b2da65c372ceb93ee2fec09c9331b2e4cbb6008212504c0918"
|
||||
),
|
||||
}
|
||||
|
||||
CHECKSUM_FILES = (
|
||||
"docs/runtime/phase-0.9c-capability-closure.md",
|
||||
"docs/runtime/phase-0.9c-output-channel-feasibility.md",
|
||||
"docs/runtime/phase-0.9c-side-effect-model.md",
|
||||
"docs/runtime/phase-0.9c-startup-exit-feasibility.md",
|
||||
"docs/runtime/phase-0.9c-static-audit.md",
|
||||
"manifests/runtime/phase-0.9c-feasibility.json",
|
||||
"manifests/runtime/phase-0.9c-feasibility.schema.json",
|
||||
"tests/phase09c_feasibility_model.py",
|
||||
"tests/test_phase09c_feasibility.py",
|
||||
"tests/test_phase09c_protocol.py",
|
||||
"tools/validate_phase09c_feasibility.py",
|
||||
)
|
||||
|
||||
CAPABILITIES = {
|
||||
"runtime_self_identity",
|
||||
"firmware_source_1",
|
||||
"firmware_source_2",
|
||||
"mount_query",
|
||||
"metadata",
|
||||
"object_id",
|
||||
"size",
|
||||
"sha256",
|
||||
"processes",
|
||||
"services",
|
||||
"listeners",
|
||||
"autoload",
|
||||
"rollback_objects",
|
||||
"monotonic_time",
|
||||
"startup",
|
||||
"output",
|
||||
"normal_exit",
|
||||
"error_exit",
|
||||
"timeout",
|
||||
"cleanup",
|
||||
"recovery_independence",
|
||||
}
|
||||
|
||||
OUTPUT_ARCHITECTURES = (
|
||||
(
|
||||
"D1_CALLER_OWNED_BOUNDED_BUFFER",
|
||||
"CONCEPT_FEASIBLE_REQUIRES_LOADER_CHANGE_AND_EXIT_PROOF",
|
||||
),
|
||||
("D2_EXISTING_REQUEST_RESPONSE", "REJECTED_SEND_ONLY_NO_RESULT_RECEIVE"),
|
||||
(
|
||||
"D3_LOADER_OWNED_STATUS_RECORD",
|
||||
"UNPROVEN_REQUIRES_LOADER_STATE_AND_PROPAGATION_CHANGE",
|
||||
),
|
||||
(
|
||||
"D4_PROCESS_EXIT_STATUS",
|
||||
"REJECTED_WAIT_STATUS_DISCARDED_AND_AMBIGUOUS",
|
||||
),
|
||||
)
|
||||
|
||||
FORBIDDEN_PHASE09C_SUFFIXES = {
|
||||
".c",
|
||||
".cc",
|
||||
".cpp",
|
||||
".cxx",
|
||||
".s",
|
||||
".asm",
|
||||
".o",
|
||||
".obj",
|
||||
".elf",
|
||||
".self",
|
||||
".sprx",
|
||||
".map",
|
||||
".pkg",
|
||||
".zip",
|
||||
".tar",
|
||||
".gz",
|
||||
}
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
value = json.load(handle)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{path} must contain a JSON object")
|
||||
return value
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def git_output(root: Path, *args: str) -> str:
|
||||
return subprocess.check_output(
|
||||
["git", *args], cwd=root, text=True, encoding="utf-8"
|
||||
).strip()
|
||||
|
||||
|
||||
def _json_type_matches(expected: str, value: Any) -> bool:
|
||||
if expected == "object":
|
||||
return isinstance(value, dict)
|
||||
if expected == "array":
|
||||
return isinstance(value, list)
|
||||
if expected == "string":
|
||||
return isinstance(value, str)
|
||||
if expected == "integer":
|
||||
return isinstance(value, int) and not isinstance(value, bool)
|
||||
if expected == "boolean":
|
||||
return isinstance(value, bool)
|
||||
if expected == "null":
|
||||
return value is None
|
||||
return True
|
||||
|
||||
|
||||
def validate_schema_instance(
|
||||
schema: dict[str, Any], value: Any, path: str = "$"
|
||||
) -> list[str]:
|
||||
"""Validate the JSON-Schema features used by the Phase-0.9C schema."""
|
||||
|
||||
errors: list[str] = []
|
||||
if "const" in schema and value != schema["const"]:
|
||||
errors.append(f"{path} differs from schema const")
|
||||
return errors
|
||||
expected_type = schema.get("type")
|
||||
if isinstance(expected_type, str) and not _json_type_matches(expected_type, value):
|
||||
errors.append(f"{path} is not {expected_type}")
|
||||
return errors
|
||||
|
||||
if isinstance(value, dict):
|
||||
required = schema.get("required", [])
|
||||
for key in required:
|
||||
if key not in value:
|
||||
errors.append(f"{path}.{key} is required")
|
||||
properties = schema.get("properties", {})
|
||||
if schema.get("additionalProperties") is False:
|
||||
for key in value:
|
||||
if key not in properties:
|
||||
errors.append(f"{path}.{key} is not allowed")
|
||||
for key, child_schema in properties.items():
|
||||
if key in value:
|
||||
errors.extend(
|
||||
validate_schema_instance(child_schema, value[key], f"{path}.{key}")
|
||||
)
|
||||
|
||||
if isinstance(value, list):
|
||||
minimum = schema.get("minItems")
|
||||
maximum = schema.get("maxItems")
|
||||
if isinstance(minimum, int) and len(value) < minimum:
|
||||
errors.append(f"{path} has fewer than {minimum} items")
|
||||
if isinstance(maximum, int) and len(value) > maximum:
|
||||
errors.append(f"{path} has more than {maximum} items")
|
||||
if schema.get("uniqueItems") is True:
|
||||
normalized = [json.dumps(item, sort_keys=True) for item in value]
|
||||
if len(normalized) != len(set(normalized)):
|
||||
errors.append(f"{path} contains duplicate items")
|
||||
item_schema = schema.get("items")
|
||||
if isinstance(item_schema, dict):
|
||||
for index, item in enumerate(value):
|
||||
errors.extend(
|
||||
validate_schema_instance(item_schema, item, f"{path}[{index}]")
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
def validate_authorizations(value: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
for field in AUTHORIZATION_FIELDS:
|
||||
if value.get(field) is not False:
|
||||
errors.append(f"authorization.{field} must be false")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_manifest(manifest: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if manifest.get("schema_version") != 1 or manifest.get("phase") != "0.9C":
|
||||
errors.append("Phase-0.9C manifest identity changed")
|
||||
if manifest.get("status") != "BLOCKED":
|
||||
errors.append("Phase-0.9C status must remain BLOCKED")
|
||||
if manifest.get("classification") != EXPECTED_CLASSIFICATION:
|
||||
errors.append("Phase-0.9C classification changed")
|
||||
if manifest.get("baseline_commit") != EXPECTED_BASELINE:
|
||||
errors.append("Phase-0.9C baseline changed")
|
||||
if manifest.get("branch") != EXPECTED_BRANCH:
|
||||
errors.append("Phase-0.9C branch changed")
|
||||
errors.extend(validate_authorizations(manifest.get("authorization", {})))
|
||||
|
||||
canonical = manifest.get("canonical_state_preserved", {})
|
||||
expected_canonical = {
|
||||
"historical_phase08_status": "READ_ONLY_PREFLIGHT_BLOCKED",
|
||||
"phase09a_status": "DESIGN_ONLY",
|
||||
"phase09b_status": "BLOCKED",
|
||||
"firmware_runtime_behavior": "UNPROVEN",
|
||||
"stock_hashes": "reference_only",
|
||||
"payload_manager_backup": "HARD_BLOCKER",
|
||||
"device_contact_performed": False,
|
||||
"device_transfer_performed": False,
|
||||
"device_execution_performed": False,
|
||||
"target_build_performed": False,
|
||||
}
|
||||
if canonical != expected_canonical:
|
||||
errors.append("canonical Phase-0.8/0.9 state changed")
|
||||
|
||||
deny = manifest.get("permanent_denylist_binding", {})
|
||||
if deny != {
|
||||
"sha256": BLOCKED_HASH,
|
||||
"status": "BLOCKED",
|
||||
"permanent": True,
|
||||
"execution_eligible": False,
|
||||
}:
|
||||
errors.append("permanent artifact denial changed")
|
||||
|
||||
source_commits = manifest.get("source_commits", {})
|
||||
for key, (_, expected_commit) in SOURCE_COMMITS.items():
|
||||
if source_commits.get(key) != expected_commit:
|
||||
errors.append(f"source commit changed: {key}")
|
||||
if source_commits.get("lifecycle_probe_source") != (
|
||||
"fe08300339a13f899fb78ea404ada381a5cba87c"
|
||||
):
|
||||
errors.append("lifecycle source commit changed")
|
||||
if manifest.get("source_tree_status") != {
|
||||
key: "clean" for key in SOURCE_COMMITS
|
||||
}:
|
||||
errors.append("source tree status record changed")
|
||||
|
||||
evidence = {
|
||||
item.get("path"): item.get("sha256")
|
||||
for item in manifest.get("source_evidence", [])
|
||||
if isinstance(item, dict)
|
||||
}
|
||||
if evidence != SOURCE_HASHES:
|
||||
errors.append("source evidence path/hash inventory changed")
|
||||
|
||||
immutable = manifest.get("immutable_evidence", {})
|
||||
if immutable.get("phase08", {}).get("files") != {
|
||||
key: IMMUTABLE_HASHES[key]
|
||||
for key in (
|
||||
"docs/runtime/phase-0.8-read-only-preflight.md",
|
||||
"manifests/runtime/phase-0.8-read-only-preflight.json",
|
||||
"tests/test_phase08_preflight.py",
|
||||
)
|
||||
}:
|
||||
errors.append("Phase-0.8 immutable manifest bindings changed")
|
||||
if immutable.get("phase09a_manifest_sha256") != IMMUTABLE_HASHES[
|
||||
"manifests/runtime/phase-0.9-anti-brick-design.json"
|
||||
]:
|
||||
errors.append("Phase-0.9A manifest binding changed")
|
||||
if immutable.get("phase09b_manifest_sha256") != IMMUTABLE_HASHES[
|
||||
"manifests/runtime/phase-0.9b-observer.json"
|
||||
]:
|
||||
errors.append("Phase-0.9B manifest binding changed")
|
||||
if immutable.get("phase09b_schema_sha256") != IMMUTABLE_HASHES[
|
||||
"manifests/runtime/phase-0.9b-observation-plan.schema.json"
|
||||
]:
|
||||
errors.append("Phase-0.9B schema binding changed")
|
||||
|
||||
startup = manifest.get("startup_exit", {})
|
||||
false_startup_fields = (
|
||||
"normal_sdk_kernelwrite_free",
|
||||
"freestanding_dependency_closure_proven",
|
||||
"stack_alignment_proven",
|
||||
"complete_relocation_and_bss_tls_contract_proven",
|
||||
"callable_read_and_time_abi_proven",
|
||||
"safe_return_proven",
|
||||
"safe_process_exit_proven",
|
||||
"error_exit_proven",
|
||||
"timeout_safe_exit_proven",
|
||||
"complete_cleanup_proven",
|
||||
)
|
||||
for field in false_startup_fields:
|
||||
if startup.get(field) is not False:
|
||||
errors.append(f"startup_exit.{field} must be false")
|
||||
if startup.get("normal_sdk_status") != "PROVEN_SIDE_EFFECTING":
|
||||
errors.append("normal SDK startup side effects were hidden")
|
||||
if not startup.get("blockers"):
|
||||
errors.append("startup/exit blockers are absent")
|
||||
|
||||
architectures = manifest.get("output_architectures", [])
|
||||
observed_architectures = tuple(
|
||||
(item.get("id"), item.get("status"))
|
||||
for item in architectures
|
||||
if isinstance(item, dict)
|
||||
)
|
||||
if observed_architectures != OUTPUT_ARCHITECTURES:
|
||||
errors.append("output architecture decisions changed or reordered")
|
||||
if any(item.get("current_implementation") is not False for item in architectures):
|
||||
errors.append("an output architecture claims current implementation")
|
||||
|
||||
protocol = manifest.get("host_protocol", {})
|
||||
expected_protocol = {
|
||||
"model": "tests/phase09c_feasibility_model.py",
|
||||
"host_only": True,
|
||||
"target_implemented": False,
|
||||
"magic": "CHG09C01",
|
||||
"version": 1,
|
||||
"header_size": 256,
|
||||
"maximum_output_size": 4096,
|
||||
"maximum_body_size": 3840,
|
||||
"integer_encoding": "unsigned_big_endian",
|
||||
"execution_nonce_bytes": 16,
|
||||
"request_id_bytes": 16,
|
||||
"firmware_field_bytes": 8,
|
||||
"artifact_hash_algorithm": "sha256",
|
||||
"body_checksum_algorithm": "sha256",
|
||||
"result_checksum_algorithm": "sha256",
|
||||
"completion_marker": "COMPLETE",
|
||||
"pointers_present": False,
|
||||
"dynamic_growth": False,
|
||||
}
|
||||
for field, expected in expected_protocol.items():
|
||||
if protocol.get(field) != expected:
|
||||
errors.append(f"host protocol field changed: {field}")
|
||||
if len(protocol.get("required_fields", [])) < 16:
|
||||
errors.append("host protocol required fields are incomplete")
|
||||
if len(protocol.get("fail_closed_conditions", [])) < 10:
|
||||
errors.append("host protocol fail-closed cases are incomplete")
|
||||
|
||||
firmware = manifest.get("firmware", {})
|
||||
source_two = firmware.get("source_two", {})
|
||||
if firmware.get("expected") != "9.60":
|
||||
errors.append("exact firmware gate changed")
|
||||
if source_two != {
|
||||
"identity": None,
|
||||
"status": "ABSENT",
|
||||
"export_name_candidate_accepted": False,
|
||||
"nonce_bound_runtime_result_present": False,
|
||||
}:
|
||||
errors.append("firmware source two was fabricated or promoted")
|
||||
if firmware.get("agreement_proven") is not False:
|
||||
errors.append("firmware agreement was claimed")
|
||||
if firmware.get("gate") != "BLOCKED_FIRMWARE_SOURCE_INCOMPLETE":
|
||||
errors.append("firmware gate was promoted")
|
||||
|
||||
side_effects = manifest.get("side_effect_model", {})
|
||||
for field in (
|
||||
"no_persistent_content_write_is_side_effect_free",
|
||||
"read_only_flag_is_side_effect_free",
|
||||
"all_planned_observations_proven_side_effect_free",
|
||||
):
|
||||
if side_effects.get(field) is not False:
|
||||
errors.append(f"side-effect claim must remain false: {field}")
|
||||
if side_effects.get("gate") != "BLOCKED_OBSERVATION_SIDE_EFFECTS_UNBOUNDED":
|
||||
errors.append("side-effect gate changed")
|
||||
required_dimensions = {
|
||||
"content",
|
||||
"metadata",
|
||||
"atime",
|
||||
"audit",
|
||||
"cache",
|
||||
"counters",
|
||||
"service_state",
|
||||
"security_monitoring",
|
||||
"open_bookkeeping",
|
||||
"process_accounting",
|
||||
"object_lifetime",
|
||||
"races",
|
||||
}
|
||||
if set(side_effects.get("dimensions", [])) != required_dimensions:
|
||||
errors.append("side-effect dimensions are incomplete")
|
||||
|
||||
capabilities = manifest.get("capability_closure", [])
|
||||
if {item.get("id") for item in capabilities if isinstance(item, dict)} != CAPABILITIES:
|
||||
errors.append("capability closure inventory changed")
|
||||
for item in capabilities:
|
||||
if not isinstance(item, dict):
|
||||
errors.append("capability entry is not an object")
|
||||
continue
|
||||
if item.get("implementation_allowed") is not False:
|
||||
errors.append(f"capability implementation enabled: {item.get('id')}")
|
||||
if item.get("execution_allowed") is not False:
|
||||
errors.append(f"capability execution enabled: {item.get('id')}")
|
||||
if item.get("target_evidence") in (None, "", "PROVEN"):
|
||||
errors.append(f"capability target evidence invalid: {item.get('id')}")
|
||||
if not item.get("blocker"):
|
||||
errors.append(f"capability blocker absent: {item.get('id')}")
|
||||
|
||||
implementation = manifest.get("implementation", {})
|
||||
if not implementation or any(value is not False for value in implementation.values()):
|
||||
errors.append("target/runtime implementation state must be entirely false")
|
||||
if manifest.get("artifact") != {
|
||||
"present": False,
|
||||
"path": None,
|
||||
"sha256": None,
|
||||
"size": None,
|
||||
"execution_eligible": False,
|
||||
"execution_authorized": False,
|
||||
}:
|
||||
errors.append("Phase-0.9C artifact must not exist")
|
||||
static = manifest.get("static_audit", {})
|
||||
if static.get("status") != "NOT_APPLICABLE_NO_TARGET_SOURCE_OR_ARTIFACT":
|
||||
errors.append("static target audit was falsely promoted")
|
||||
if static.get("host_model_only") is not True:
|
||||
errors.append("static audit is not explicitly host-only")
|
||||
|
||||
decision = manifest.get("final_decision", {})
|
||||
if decision.get("positive_classification_allowed") is not False:
|
||||
errors.append("positive classification was enabled")
|
||||
if decision.get("classification") != EXPECTED_CLASSIFICATION:
|
||||
errors.append("final decision differs from top-level classification")
|
||||
expected_blockers = {
|
||||
"BLOCKED_STARTUP_ABI_UNPROVEN",
|
||||
"BLOCKED_EXIT_CLEANUP_UNPROVEN",
|
||||
"BLOCKED_NO_BOUNDED_OUTPUT_CHANNEL",
|
||||
"BLOCKED_FIRMWARE_SOURCE_INCOMPLETE",
|
||||
"BLOCKED_OBSERVATION_SIDE_EFFECTS_UNBOUNDED",
|
||||
}
|
||||
if set(decision.get("foundational_blockers", [])) != expected_blockers:
|
||||
errors.append("foundational blocker set changed")
|
||||
if decision.get("next_phase_automatic") is not False:
|
||||
errors.append("automatic next phase was enabled")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_source_trees(root: Path) -> list[str]:
|
||||
errors: list[str] = []
|
||||
for key, (relative, expected_commit) in SOURCE_COMMITS.items():
|
||||
source_root = (root / relative).resolve()
|
||||
if not source_root.is_dir():
|
||||
errors.append(f"source tree missing: {key}")
|
||||
continue
|
||||
try:
|
||||
commit = git_output(source_root, "rev-parse", "HEAD")
|
||||
status = git_output(source_root, "status", "--short")
|
||||
except subprocess.CalledProcessError:
|
||||
errors.append(f"source tree is not readable Git: {key}")
|
||||
continue
|
||||
if commit != expected_commit:
|
||||
errors.append(f"source tree commit mismatch: {key}")
|
||||
if status:
|
||||
errors.append(f"source tree is dirty: {key}")
|
||||
|
||||
for relative, expected_hash in SOURCE_HASHES.items():
|
||||
path = (root / relative).resolve()
|
||||
if not path.is_file():
|
||||
errors.append(f"source evidence missing: {relative}")
|
||||
elif sha256_file(path) != expected_hash:
|
||||
errors.append(f"source evidence hash mismatch: {relative}")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_immutable_evidence(root: Path) -> list[str]:
|
||||
errors: list[str] = []
|
||||
for relative, expected_hash in IMMUTABLE_HASHES.items():
|
||||
path = root / relative
|
||||
if not path.is_file():
|
||||
errors.append(f"immutable evidence missing: {relative}")
|
||||
elif sha256_file(path) != expected_hash:
|
||||
errors.append(f"immutable evidence changed: {relative}")
|
||||
return errors
|
||||
|
||||
|
||||
def forbidden_repository_path(relative: str) -> bool:
|
||||
normalized = relative.replace("\\", "/").lower()
|
||||
path = Path(normalized)
|
||||
phase_marker = "phase09c" in normalized or "phase-0.9c" in normalized
|
||||
if phase_marker and path.suffix in FORBIDDEN_PHASE09C_SUFFIXES:
|
||||
return True
|
||||
if normalized.startswith(
|
||||
(
|
||||
"samples/phase09c",
|
||||
"samples/phase-0.9c",
|
||||
"src/backends/ps5/phase09c",
|
||||
"src/backends/ps5/observer",
|
||||
)
|
||||
):
|
||||
return True
|
||||
if normalized.startswith("packaging/phase09c/"):
|
||||
return normalized != "packaging/phase09c/sha256sums.txt"
|
||||
return False
|
||||
|
||||
|
||||
def validate_repository_boundary(root: Path) -> list[str]:
|
||||
errors: list[str] = []
|
||||
listed = git_output(
|
||||
root, "ls-files", "--cached", "--others", "--exclude-standard"
|
||||
).splitlines()
|
||||
for relative in listed:
|
||||
if forbidden_repository_path(relative):
|
||||
errors.append(f"forbidden Phase-0.9C target/package path: {relative}")
|
||||
|
||||
cmake = (root / "CMakeLists.txt").read_text(encoding="utf-8")
|
||||
if re.search(
|
||||
r"add_(?:executable|library)\s*\([^)]*phase[-_]?0?9c",
|
||||
cmake,
|
||||
flags=re.IGNORECASE | re.DOTALL,
|
||||
):
|
||||
errors.append("CMake declares a Phase-0.9C target")
|
||||
forbidden_directories = (
|
||||
root / "samples/phase09c_observer",
|
||||
root / "samples/phase-0.9c-observer",
|
||||
root / "packaging/phase09c/lifecycle",
|
||||
root / "packaging/phase09c/install",
|
||||
root / "packaging/phase09c/autoload",
|
||||
)
|
||||
for path in forbidden_directories:
|
||||
if path.exists():
|
||||
errors.append(f"forbidden Phase-0.9C path exists: {path.relative_to(root)}")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_checksums(root: Path) -> list[str]:
|
||||
errors: list[str] = []
|
||||
checksum_path = root / "packaging/phase09c/SHA256SUMS.txt"
|
||||
if not checksum_path.is_file():
|
||||
return ["Phase-0.9C checksum file is missing"]
|
||||
observed: dict[str, str] = {}
|
||||
for line in checksum_path.read_text(encoding="utf-8").splitlines():
|
||||
parts = line.split(" ", 1)
|
||||
if len(parts) != 2 or not re.fullmatch(r"[0-9a-f]{64}", parts[0]):
|
||||
errors.append("malformed Phase-0.9C checksum line")
|
||||
continue
|
||||
observed[parts[1]] = parts[0]
|
||||
if tuple(observed) != CHECKSUM_FILES:
|
||||
errors.append("Phase-0.9C checksum inventory or order changed")
|
||||
for relative in CHECKSUM_FILES:
|
||||
path = root / relative
|
||||
if not path.is_file():
|
||||
errors.append(f"Phase-0.9C checksummed file missing: {relative}")
|
||||
elif observed.get(relative) != sha256_file(path):
|
||||
errors.append(f"Phase-0.9C checksum mismatch: {relative}")
|
||||
return errors
|
||||
|
||||
|
||||
def collect_errors(root: Path) -> list[str]:
|
||||
manifest_path = root / "manifests/runtime/phase-0.9c-feasibility.json"
|
||||
schema_path = root / "manifests/runtime/phase-0.9c-feasibility.schema.json"
|
||||
manifest = load_json(manifest_path)
|
||||
schema = load_json(schema_path)
|
||||
errors = validate_manifest(manifest)
|
||||
errors.extend(validate_schema_instance(schema, manifest))
|
||||
errors.extend(validate_source_trees(root))
|
||||
errors.extend(validate_immutable_evidence(root))
|
||||
errors.extend(validate_repository_boundary(root))
|
||||
errors.extend(validate_checksums(root))
|
||||
|
||||
denylist = load_json(root / "manifests/artifact-denylist.json")
|
||||
entries = denylist.get("entries", [])
|
||||
if (
|
||||
len(entries) != 1
|
||||
or entries[0].get("sha256") != BLOCKED_HASH
|
||||
or entries[0].get("status") != "BLOCKED"
|
||||
or entries[0].get("permanent") is not True
|
||||
or entries[0].get("execution_eligible") is not False
|
||||
):
|
||||
errors.append("permanent denylist no longer blocks the legacy artifact")
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
root = args.root.resolve()
|
||||
errors = collect_errors(root)
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"Phase-0.9C validation failed: {error}")
|
||||
return 1
|
||||
print(
|
||||
"Phase-0.9C feasibility manifest, schema, sources, immutable evidence, "
|
||||
"denylist, checksums, and no-target boundary: PASS"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,759 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate the offline-only Phase-0.9D readback and recovery audit."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
|
||||
EXPECTED_BASELINE = "e0e68829ab76977c845e7106ef93e6c01fbc966e"
|
||||
EXPECTED_BRANCH = "codex/chimera-gfx-phase09d-existing-stack-readback"
|
||||
EXPECTED_PHASE = "PHASE_0_9D_EXISTING_STACK_READBACK"
|
||||
EXPECTED_STATUS = "DESIGN_ONLY"
|
||||
BLOCKED_HASH = (
|
||||
"4be1c17b4964f2b68c39b5145bc4af4619c32512d60269ecf5c39728b390fa63"
|
||||
)
|
||||
DENYLIST_HASH = (
|
||||
"e9603b0e3792781ad5b511afb22ef61e3d4fd4c5a16bf928f8609193bcd97783"
|
||||
)
|
||||
|
||||
AUTHORIZATION_FIELDS = (
|
||||
"installation_authorized",
|
||||
"execution_authorized",
|
||||
"lifecycle_authorized",
|
||||
"automatic_retry",
|
||||
"autoload_authorized",
|
||||
"device_write_authorized",
|
||||
"transfer_authorized",
|
||||
"observer_build_authorized",
|
||||
"backup_creation_authorized",
|
||||
)
|
||||
|
||||
ACTION_FIELDS = (
|
||||
"ps5_connected",
|
||||
"device_request_performed",
|
||||
"files_transferred",
|
||||
"device_write_performed",
|
||||
"target_execution_performed",
|
||||
"target_artifact_created",
|
||||
"target_build_performed",
|
||||
"observer_created",
|
||||
"device_client_created",
|
||||
"backup_created",
|
||||
"staging_performed",
|
||||
)
|
||||
|
||||
SOURCE_COMMITS = {
|
||||
"hardened_elfldr": (
|
||||
"../chimera-elfldr",
|
||||
"197623058f509eddde18868dafcb92fdcac66464",
|
||||
),
|
||||
"controlled_payload_manager": (
|
||||
"../chimera-ps5-payload-manager",
|
||||
"e23d94ff91233aa770e2342800c1467875bdef44",
|
||||
),
|
||||
"elfldr_public_base": (
|
||||
"work/upstream/elfldr-v0.23",
|
||||
"699e8bcff03e91e8d6ca6eba281af25c5a58d8c2",
|
||||
),
|
||||
"payload_manager_public_base": (
|
||||
"work/upstream/pldmgr-v0.3.1",
|
||||
"cfbc70f30f419b09bf2b52283f7409e2d3117ee1",
|
||||
),
|
||||
"ps5_payload_sdk_v0_41": (
|
||||
"work/upstream/sdk",
|
||||
"d2e2e585740362976a39fdd5ccf390f199a7bc37",
|
||||
),
|
||||
}
|
||||
|
||||
SOURCE_HASHES = {
|
||||
"../chimera-elfldr/README.md": (
|
||||
"372aeb28dc971b2bd98093a47fdaf77c32f75bbdc3b3d7e8678900744b91eadb"
|
||||
),
|
||||
"../chimera-elfldr/main.c": (
|
||||
"876389a26999073994e63ca29926982280d9594a1ee941244205b54f57e2b4d1"
|
||||
),
|
||||
"../chimera-elfldr/bootstrap.c": (
|
||||
"5a8072ec0d6db919cb3a81a7028dc91fd8e2c3a0d69b0fa7e84836fa93b45381"
|
||||
),
|
||||
"../chimera-elfldr/socksrv.c": (
|
||||
"d642ced3e9b4a296dd15e355050ebe956f53a6dfdaa6ac10109cd067a3bba3d7"
|
||||
),
|
||||
"../chimera-ps5-payload-manager/README.md": (
|
||||
"a00277252da46c701326ef66e5ca0d13cadffd50b3c8adf6da89cd1948c97718"
|
||||
),
|
||||
"../chimera-ps5-payload-manager/DEVELOPMENT.md": (
|
||||
"0c17bed71b07c9aadf31c47625bbe1ccb42e670e594fb64a1e07aa782d5eec31"
|
||||
),
|
||||
"../chimera-ps5-payload-manager/deploy.sh": (
|
||||
"2facdc1ca70db57ba258265c07a8ae1d30d3d2fb429d6718d5d5088300788e52"
|
||||
),
|
||||
"../chimera-ps5-payload-manager/include/pldmgr.h": (
|
||||
"8603b8338364cea60ffaf94f985f112fb2ddfda2f44b1ccc9a2bd7a15d1b229a"
|
||||
),
|
||||
"../chimera-ps5-payload-manager/src/http_server.c": (
|
||||
"2c8ff2a4bc1028d71e3cc342839584d425b6e7502e55e18e2762cdbf62c59d40"
|
||||
),
|
||||
"../chimera-ps5-payload-manager/src/log_server.c": (
|
||||
"659095f43df1bbe8eb24acb165f027edc277af1e60aabb26ba9e3920b233d6f1"
|
||||
),
|
||||
"../chimera-ps5-payload-manager/src/autoload.c": (
|
||||
"7051cab3ee1a3e0b9f6498000565eb9e160b9c63efa1771f250e98ec3aa4ae67"
|
||||
),
|
||||
"../chimera-ps5-payload-manager/src/main.c": (
|
||||
"b2374e8fb101587b15c8261c58cb4f0573d88c490214365051eb9facd60f6eed"
|
||||
),
|
||||
"../chimera-ps5-payload-manager/src/controlled_manager.c": (
|
||||
"042b55b2cece32effed636529249fd18061a2fe3c5e70c7f755f2817cfb84b99"
|
||||
),
|
||||
"../chimera-ps5-payload-manager/src/verified_launcher.c": (
|
||||
"066100ca4917c7acc560e2e85666ca136cd7ccfd9094417377048f41106dd56e"
|
||||
),
|
||||
"../chimera-ps5-payload-manager/src/ps5_launcher.c": (
|
||||
"29c1a5fd01784a59e88b3698940f120cb03020071bc2b7d74a1da1a51524ef59"
|
||||
),
|
||||
"../chimera-ps5-payload-manager/src/payload_mgr.c": (
|
||||
"d67e9ba33edc8ca3a45aae07923d4c4790348b5f8570307e581e16780abafcba"
|
||||
),
|
||||
"../chimera-ps5-payload-manager/src/repository.c": (
|
||||
"8ba694ae6d4813573752acd82ce2dbfb715b51d4ec6155f783904d76dd115adb"
|
||||
),
|
||||
"../chimera-ps5-payload-manager/src/sources.c": (
|
||||
"a7a4a5cafccfba74902d6ed21ba001e6a9f62d4dff840ac38882d04827162c96"
|
||||
),
|
||||
"work/upstream/elfldr-v0.23/socksrv.c": (
|
||||
"500d3c7df7ed5eac1adc925c89344d75c469651b71143716fdb77bfb2209a40c"
|
||||
),
|
||||
"work/upstream/pldmgr-v0.3.1/include/pldmgr.h": (
|
||||
"01c693a3248dce7a663dd4ed9c73ce5f3a4443b5f2bd210746d94993dee27b91"
|
||||
),
|
||||
"work/upstream/pldmgr-v0.3.1/src/http_server.c": (
|
||||
"35cf5d8f0dd44cf64ceab5e4b0ecc09413c82d7e9946ba9de2ca4b1898631fdd"
|
||||
),
|
||||
"samples/lifecycle_probe/main.c": (
|
||||
"1ae7df1fe921ccab2a252f77975d3d441ef7725e34535b024580c0d4a242d766"
|
||||
),
|
||||
}
|
||||
|
||||
IMMUTABLE_HASHES = {
|
||||
"manifests/runtime/phase-0.8-read-only-preflight.json": (
|
||||
"47d7f452f8799979fe99b3e6d56859f03544112725bf7e5b349eba5ed81b3322"
|
||||
),
|
||||
"manifests/runtime/phase-0.8-remediation.json": (
|
||||
"a9dafed8c83722c43709dcf90ea117c21d6f996ff8fe233810ed08aa20cbe071"
|
||||
),
|
||||
"manifests/runtime/phase-0.9-anti-brick-design.json": (
|
||||
"39fd7c70cae998d9d74e7caf1ff3c19f9f76f7de2e5a50bc09baede4ba53e9e9"
|
||||
),
|
||||
"manifests/runtime/phase-0.9b-observer.json": (
|
||||
"104c4a667ad17f9827fc7276852c6faeef96effb8e3561a5ebc19a62c7d51634"
|
||||
),
|
||||
"manifests/runtime/phase-0.9c-feasibility.json": (
|
||||
"84eb737ff3486d0c5d8b5ecf06809f93bc573497da4919595b4c1817fd34247c"
|
||||
),
|
||||
}
|
||||
|
||||
DELIVERABLES = (
|
||||
"docs/runtime/phase-0.9d-bootstrap-recovery-chain.md",
|
||||
"docs/runtime/phase-0.9d-existing-stack-endpoint-matrix.md",
|
||||
"docs/runtime/phase-0.9d-readback-feasibility.md",
|
||||
"docs/runtime/phase-0.9d-off-device-backup-contract.md",
|
||||
"docs/runtime/phase-0.9d-independent-recovery-analysis.md",
|
||||
"docs/runtime/phase-0.9d-operational-windows.md",
|
||||
"manifests/runtime/phase-0.9d-existing-stack-readback.json",
|
||||
"manifests/runtime/phase-0.9d-existing-stack-readback.schema.json",
|
||||
"tools/validate_phase09d_readback.py",
|
||||
"tests/test_phase09d_readback.py",
|
||||
)
|
||||
|
||||
EXPECTED_DECISIONS = {
|
||||
"new_observer_feasibility": "BLOCKED",
|
||||
"existing_stack_manual_fact_collection": "PARTIAL",
|
||||
"existing_stack_single_readback": "BLOCKED_NO_READBACK_PATH",
|
||||
"existing_stack_repeat_readback": "BLOCKED",
|
||||
"elfldr_independent_recovery": "PARTIAL",
|
||||
"payload_manager_independent_recovery": "PARTIAL",
|
||||
"side_by_side_feasibility": "BLOCKED",
|
||||
"device_write": "NOT_AUTHORIZED",
|
||||
"target_execution": "NOT_AUTHORIZED",
|
||||
"installation": "NOT_AUTHORIZED",
|
||||
}
|
||||
|
||||
REQUIRED_EFFECTIVE_ROUTE_FIELDS = (
|
||||
"method",
|
||||
"endpoint",
|
||||
"handler",
|
||||
"source",
|
||||
"lines",
|
||||
"parameters",
|
||||
"authentication",
|
||||
"response",
|
||||
"open_flags",
|
||||
"reads_bytes",
|
||||
"writes_bytes",
|
||||
"creates_file",
|
||||
"removes_file",
|
||||
"renames_file",
|
||||
"reads_directory",
|
||||
"reads_metadata",
|
||||
"calculates_hash",
|
||||
"modifies_configuration",
|
||||
"writes_server_active_flag",
|
||||
"writes_autoload_triggered",
|
||||
"writes_log_ring",
|
||||
"launches_payload",
|
||||
"process_or_service_action",
|
||||
"network_behavior",
|
||||
"timeout_behavior",
|
||||
"retry_behavior",
|
||||
"maximum_size",
|
||||
"short_read_behavior",
|
||||
"error_behavior",
|
||||
"effect_class",
|
||||
"readback_candidate",
|
||||
"observation_candidate",
|
||||
"binary_safe_file_response",
|
||||
"exact_returned_byte_count",
|
||||
"partial_result_rejected",
|
||||
"forbidden_reason",
|
||||
)
|
||||
|
||||
BACKUP_STATES = (
|
||||
"TRANSFER_NOT_STARTED",
|
||||
"TRANSFER_INCOMPLETE",
|
||||
"HOST_COPY_RECEIVED",
|
||||
"HOST_COPY_REOPENED",
|
||||
"HOST_COPY_HASHED",
|
||||
"SECOND_COPY_CREATED",
|
||||
"SECOND_COPY_REOPENED",
|
||||
"SECOND_COPY_HASHED",
|
||||
"COPIES_MATCH",
|
||||
"SOURCE_MAPPING_PARTIAL",
|
||||
"SOURCE_MAPPING_VERIFIED",
|
||||
"INVALID",
|
||||
)
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
value = json.load(stream)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{path} does not contain an object")
|
||||
return value
|
||||
|
||||
|
||||
def sha256_file(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 git(root: Path, *args: str) -> str:
|
||||
result = subprocess.run(
|
||||
["git", *args],
|
||||
cwd=root,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(result.stderr.strip() or "git command failed")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def effective_route(
|
||||
route: dict[str, Any], defaults: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
merged = dict(defaults)
|
||||
merged.update(route)
|
||||
return merged
|
||||
|
||||
|
||||
def route_readback_errors(
|
||||
route: dict[str, Any], defaults: dict[str, Any] | None = None
|
||||
) -> list[str]:
|
||||
merged = effective_route(route, defaults or {})
|
||||
errors: list[str] = []
|
||||
flags = {str(flag).lower() for flag in merged.get("open_flags", [])}
|
||||
mutating_open = any(
|
||||
token in flag
|
||||
for flag in flags
|
||||
for token in ("o_wronly", "o_rdwr", "o_creat", "o_append", "wb", "(w)")
|
||||
)
|
||||
mutating = any(
|
||||
bool(merged.get(field))
|
||||
for field in (
|
||||
"writes_bytes",
|
||||
"creates_file",
|
||||
"removes_file",
|
||||
"renames_file",
|
||||
"modifies_configuration",
|
||||
)
|
||||
) or mutating_open
|
||||
|
||||
if merged.get("readback_candidate"):
|
||||
if mutating:
|
||||
errors.append("readback candidate mutates a device file or configuration")
|
||||
if merged.get("launches_payload"):
|
||||
errors.append("readback candidate launches a payload")
|
||||
if merged.get("process_or_service_action"):
|
||||
errors.append("readback candidate performs a process/service action")
|
||||
if merged.get("writes_autoload_triggered"):
|
||||
errors.append("readback candidate writes autoload_triggered")
|
||||
if not merged.get("binary_safe_file_response"):
|
||||
errors.append("readback candidate lacks binary-safe framing")
|
||||
if not merged.get("exact_returned_byte_count"):
|
||||
errors.append("readback candidate lacks an exact byte count")
|
||||
if not merged.get("partial_result_rejected"):
|
||||
errors.append("readback candidate does not reject partial output")
|
||||
if str(merged.get("short_read_behavior", "")).upper() in {
|
||||
"",
|
||||
"ABSENT",
|
||||
"UNPROVEN",
|
||||
"NO_HOST_FILE_READBACK_CONTRACT",
|
||||
"NO_FILE_RESPONSE",
|
||||
}:
|
||||
errors.append("readback candidate lacks short-read detection")
|
||||
if merged.get("automatic_retry") is True:
|
||||
errors.append("readback candidate enables automatic retry")
|
||||
if merged.get("automatic_resume") is True:
|
||||
errors.append("readback candidate enables automatic resume")
|
||||
return errors
|
||||
|
||||
|
||||
def backup_record_errors(record: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
status = record.get("status")
|
||||
if status not in BACKUP_STATES:
|
||||
errors.append("unknown backup status")
|
||||
if record.get("automatic_resume"):
|
||||
errors.append("automatic resume is forbidden")
|
||||
if record.get("automatic_retry"):
|
||||
errors.append("automatic retry is forbidden")
|
||||
if status in {"HOST_COPY_HASHED", "SECOND_COPY_HASHED", "COPIES_MATCH"}:
|
||||
if not isinstance(record.get("exact_byte_count"), int):
|
||||
errors.append("a hash requires an exact byte count")
|
||||
if not record.get("closed_and_reopened"):
|
||||
errors.append("hash requires close and reopen")
|
||||
if not record.get("sha256"):
|
||||
errors.append("hashed state requires SHA-256")
|
||||
if status == "COPIES_MATCH":
|
||||
if not all(
|
||||
record.get(field)
|
||||
for field in ("sizes_match", "hashes_match", "bytes_match")
|
||||
):
|
||||
errors.append("COPIES_MATCH requires size, hash, and byte equality")
|
||||
if record.get("transfer_complete") is False and status != "INVALID":
|
||||
errors.append("partial transfer must be INVALID")
|
||||
if record.get("recovery_proven"):
|
||||
errors.append("host backup cannot prove recovery")
|
||||
return errors
|
||||
|
||||
|
||||
def server_active_observation_status(semantics: dict[str, Any]) -> str:
|
||||
if (
|
||||
not semantics.get("fully_documented")
|
||||
or semantics.get("reset_path") in {None, "UNPROVEN"}
|
||||
):
|
||||
return "BLOCKED"
|
||||
if semantics.get("reset_path") == "NONE_IN_PROCESS":
|
||||
return "PARTIAL"
|
||||
return "READY_FOR_REVIEW"
|
||||
|
||||
|
||||
def recovery_dependency_classification(
|
||||
component: str, recovery_executor: str
|
||||
) -> str:
|
||||
if component == recovery_executor:
|
||||
return "SELF_DEPENDENT"
|
||||
if recovery_executor in {"ABSENT", "UNPROVEN", ""}:
|
||||
return recovery_executor
|
||||
return "CROSS_DEPENDENT"
|
||||
|
||||
|
||||
def _type_matches(value: Any, expected: str) -> bool:
|
||||
return {
|
||||
"object": isinstance(value, dict),
|
||||
"array": isinstance(value, list),
|
||||
"string": isinstance(value, str),
|
||||
"boolean": isinstance(value, bool),
|
||||
"integer": isinstance(value, int) and not isinstance(value, bool),
|
||||
"number": isinstance(value, (int, float)) and not isinstance(value, bool),
|
||||
"null": value is None,
|
||||
}.get(expected, True)
|
||||
|
||||
|
||||
def validate_schema_instance(
|
||||
schema: dict[str, Any], instance: Any, path: str = "$"
|
||||
) -> list[str]:
|
||||
"""Small offline validator for the schema features used by this record."""
|
||||
errors: list[str] = []
|
||||
expected_type = schema.get("type")
|
||||
if expected_type and not _type_matches(instance, expected_type):
|
||||
return [f"{path}: expected {expected_type}"]
|
||||
if "const" in schema and instance != schema["const"]:
|
||||
errors.append(f"{path}: expected constant {schema['const']!r}")
|
||||
if "enum" in schema and instance not in schema["enum"]:
|
||||
errors.append(f"{path}: value is outside enum")
|
||||
|
||||
if isinstance(instance, dict):
|
||||
required = schema.get("required", [])
|
||||
for key in required:
|
||||
if key not in instance:
|
||||
errors.append(f"{path}: missing {key}")
|
||||
if len(instance) < schema.get("minProperties", 0):
|
||||
errors.append(f"{path}: too few properties")
|
||||
properties = schema.get("properties", {})
|
||||
for key, value in instance.items():
|
||||
if key in properties:
|
||||
errors.extend(
|
||||
validate_schema_instance(properties[key], value, f"{path}.{key}")
|
||||
)
|
||||
elif schema.get("additionalProperties") is False:
|
||||
errors.append(f"{path}: unexpected property {key}")
|
||||
elif isinstance(schema.get("additionalProperties"), dict):
|
||||
errors.extend(
|
||||
validate_schema_instance(
|
||||
schema["additionalProperties"], value, f"{path}.{key}"
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(instance, list):
|
||||
if len(instance) < schema.get("minItems", 0):
|
||||
errors.append(f"{path}: too few items")
|
||||
if "maxItems" in schema and len(instance) > schema["maxItems"]:
|
||||
errors.append(f"{path}: too many items")
|
||||
item_schema = schema.get("items")
|
||||
if isinstance(item_schema, dict):
|
||||
for index, item in enumerate(instance):
|
||||
errors.extend(
|
||||
validate_schema_instance(item_schema, item, f"{path}[{index}]")
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
def validate_manifest(manifest: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if manifest.get("phase") != EXPECTED_PHASE:
|
||||
errors.append("wrong phase")
|
||||
if manifest.get("status") != EXPECTED_STATUS:
|
||||
errors.append("status must remain DESIGN_ONLY")
|
||||
if manifest.get("baseline_commit") != EXPECTED_BASELINE:
|
||||
errors.append("wrong baseline")
|
||||
if manifest.get("branch") != EXPECTED_BRANCH:
|
||||
errors.append("wrong branch")
|
||||
|
||||
for field in AUTHORIZATION_FIELDS:
|
||||
if manifest.get("authorization", {}).get(field) is not False:
|
||||
errors.append(f"authorization.{field} must be false")
|
||||
for field in ACTION_FIELDS:
|
||||
if manifest.get("actions", {}).get(field) is not False:
|
||||
errors.append(f"actions.{field} must be false")
|
||||
|
||||
canonical = manifest.get("canonical_state", {})
|
||||
expected_canonical = {
|
||||
"phase08_status": "READ_ONLY_PREFLIGHT_BLOCKED",
|
||||
"phase09a_status": "DESIGN_ONLY",
|
||||
"phase09b_status": "BLOCKED",
|
||||
"phase09c_classification": "BLOCKED_MULTIPLE_FOUNDATIONAL_CONTRACTS",
|
||||
"firmware_runtime_behavior": "UNPROVEN",
|
||||
"stock_identities": "reference_only",
|
||||
"payload_manager_backup": "HARD_BLOCKER_FOR_INSTALLATION",
|
||||
"independent_recovery": "UNPROVEN",
|
||||
"permanent_denylist_sha256": DENYLIST_HASH,
|
||||
"permanently_blocked_artifact_sha256": BLOCKED_HASH,
|
||||
}
|
||||
for field, expected in expected_canonical.items():
|
||||
if canonical.get(field) != expected:
|
||||
errors.append(f"canonical_state.{field} changed")
|
||||
|
||||
if manifest.get("decisions") != EXPECTED_DECISIONS:
|
||||
errors.append("decision matrix changed")
|
||||
if manifest.get("source_commits", {}).get("lifecycle_source") != (
|
||||
"fe08300339a13f899fb78ea404ada381a5cba87c"
|
||||
):
|
||||
errors.append("lifecycle source binding changed")
|
||||
for name, (_, expected) in SOURCE_COMMITS.items():
|
||||
if manifest.get("source_commits", {}).get(name) != expected:
|
||||
errors.append(f"source commit changed: {name}")
|
||||
if manifest.get("source_tree_status", {}).get(name) != "clean":
|
||||
errors.append(f"source tree is not recorded clean: {name}")
|
||||
|
||||
routes = manifest.get("endpoint_matrix", [])
|
||||
if len(routes) != 40:
|
||||
errors.append("endpoint matrix must contain all 40 audited route records")
|
||||
defaults = manifest.get("endpoint_defaults", {})
|
||||
identities: set[tuple[str, str]] = set()
|
||||
for route in routes:
|
||||
effective = effective_route(route, defaults)
|
||||
identity = (str(route.get("profile")), str(route.get("endpoint")))
|
||||
if identity in identities:
|
||||
errors.append(f"duplicate route identity: {identity}")
|
||||
identities.add(identity)
|
||||
for field in REQUIRED_EFFECTIVE_ROUTE_FIELDS:
|
||||
if field not in effective:
|
||||
errors.append(f"{identity}: missing effective field {field}")
|
||||
errors.extend(
|
||||
f"{identity}: {error}"
|
||||
for error in route_readback_errors(route, defaults)
|
||||
)
|
||||
if effective.get("readback_candidate") is not False:
|
||||
errors.append(f"{identity}: no audited route may be a readback candidate")
|
||||
|
||||
autoload = next(
|
||||
(
|
||||
effective_route(route, defaults)
|
||||
for route in routes
|
||||
if route.get("profile") == "full"
|
||||
and route.get("endpoint") == "/autoload_status"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not autoload or not autoload.get("writes_autoload_triggered"):
|
||||
errors.append("/autoload_status mutation is not recorded")
|
||||
|
||||
flags = manifest.get("flag_semantics", {})
|
||||
server_active = flags.get("server_active_flag", {})
|
||||
if server_active.get("classification") == "ANTI_BRICK_CRITICAL":
|
||||
errors.append("server_active_flag is incorrectly anti-brick critical")
|
||||
if (
|
||||
not server_active.get("fully_documented")
|
||||
or server_active.get("reset_path") != "NONE_IN_PROCESS"
|
||||
or server_active.get("lifetime") != "PROCESS_LOCAL"
|
||||
):
|
||||
errors.append("server_active_flag semantics are incomplete")
|
||||
autoload_flag = flags.get("autoload_triggered", {})
|
||||
if autoload_flag.get("excluded_windows") != [1, 2]:
|
||||
errors.append("autoload_triggered must be excluded from Windows 1 and 2")
|
||||
|
||||
if len(manifest.get("readback_routes", [])) != 7:
|
||||
errors.append("readback route search is incomplete")
|
||||
for route in manifest.get("readback_routes", []):
|
||||
if route.get("usable_once") or route.get("usable_twice"):
|
||||
errors.append("a rejected readback route is marked usable")
|
||||
if manifest.get("path_classification") != "PATH_CONFLICT":
|
||||
errors.append("path conflict was removed")
|
||||
if manifest.get("runtime_observed_live_paths") != []:
|
||||
errors.append("offline package/config paths cannot become live paths")
|
||||
|
||||
contract = manifest.get("host_backup_contract", {})
|
||||
if contract.get("statuses") != list(BACKUP_STATES):
|
||||
errors.append("backup status vocabulary changed")
|
||||
required_true = (
|
||||
"one_component_per_session",
|
||||
"new_exclusive_local_output",
|
||||
"binary_mode",
|
||||
"exact_received_byte_count_required",
|
||||
"close_reopen_before_hash",
|
||||
"sha256_required",
|
||||
"size_required",
|
||||
"second_independent_connection",
|
||||
"second_new_output",
|
||||
"compare_size",
|
||||
"compare_sha256",
|
||||
"compare_every_byte",
|
||||
"capture_raw_protocol_metadata",
|
||||
"capture_literal_source_path",
|
||||
"capture_device_and_session",
|
||||
"capture_client_commit",
|
||||
"off_device_backup_valid_requires_copies_match",
|
||||
)
|
||||
for field in required_true:
|
||||
if contract.get(field) is not True:
|
||||
errors.append(f"host_backup_contract.{field} must be true")
|
||||
required_false = (
|
||||
"overwrite_existing_output",
|
||||
"automatic_resume",
|
||||
"automatic_retry",
|
||||
"contains_device_write_command",
|
||||
"recovery_proven_allowed",
|
||||
)
|
||||
for field in required_false:
|
||||
if contract.get(field) is not False:
|
||||
errors.append(f"host_backup_contract.{field} must be false")
|
||||
if contract.get("partial_transfer_status") != "INVALID":
|
||||
errors.append("partial host transfer must be INVALID")
|
||||
|
||||
recovery = manifest.get("recovery_dependencies", {})
|
||||
if recovery.get("elfldr", {}).get("classification") != "PARTIAL":
|
||||
errors.append("elfldr recovery must remain PARTIAL")
|
||||
manager = recovery.get("payload_manager", {})
|
||||
if manager.get("classification") != "PARTIAL":
|
||||
errors.append("Payload Manager recovery must remain PARTIAL")
|
||||
if "CROSS_DEPENDENT" not in manager.get("detailed", []):
|
||||
errors.append("Payload Manager cross-dependence is missing")
|
||||
if manifest.get("side_by_side", {}).get("classification") != "BLOCKED":
|
||||
errors.append("side-by-side must remain blocked")
|
||||
if manifest.get("side_by_side", {}).get(
|
||||
"grants_installation_authorization"
|
||||
):
|
||||
errors.append("side-by-side cannot authorize installation")
|
||||
|
||||
windows = manifest.get("operational_windows", [])
|
||||
if [window.get("window") for window in windows] != list(range(1, 8)):
|
||||
errors.append("operational windows are incomplete or reordered")
|
||||
for window in windows:
|
||||
if window.get("device_write") is not False:
|
||||
errors.append(f"Window {window.get('window')} permits device write")
|
||||
if window.get("payload_launch") is not False:
|
||||
errors.append(f"Window {window.get('window')} permits payload launch")
|
||||
if window.get("autoload_status_route") is not False:
|
||||
errors.append(f"Window {window.get('window')} permits /autoload_status")
|
||||
if window.get("automatic_retry") is not False:
|
||||
errors.append(f"Window {window.get('window')} permits retry")
|
||||
if windows and windows[0].get("file_transfer") is not False:
|
||||
errors.append("Window 1 contains file transfer")
|
||||
if len(windows) >= 3 and windows[2].get("automatic_third_attempt") is not False:
|
||||
errors.append("Window 3 permits an automatic third attempt")
|
||||
if len(windows) >= 4 and windows[3].get("component_session_separate") is not True:
|
||||
errors.append("components do not have separate windows")
|
||||
|
||||
final = manifest.get("final_decision", {})
|
||||
if (
|
||||
final.get("classification") != "BLOCKED"
|
||||
or final.get("hardware_evidence_claimed") is not False
|
||||
or final.get("device_action_authorized") is not False
|
||||
):
|
||||
errors.append("final decision must remain an offline-only block")
|
||||
return errors
|
||||
|
||||
|
||||
def _phase09d_paths(root: Path) -> list[str]:
|
||||
paths: set[str] = set()
|
||||
for candidate in git(root, "ls-files").splitlines():
|
||||
normalized = candidate.replace("\\", "/")
|
||||
lowered = normalized.lower()
|
||||
if "phase-0.9d" in lowered or "phase09d" in lowered:
|
||||
paths.add(normalized)
|
||||
|
||||
output = git(root, "status", "--porcelain=v1", "--untracked-files=all")
|
||||
for line in output.splitlines():
|
||||
if len(line) < 4:
|
||||
continue
|
||||
candidate = line[3:].replace("\\", "/")
|
||||
if " -> " in candidate:
|
||||
candidate = candidate.rsplit(" -> ", 1)[1]
|
||||
lowered = candidate.lower()
|
||||
if "phase-0.9d" in lowered or "phase09d" in lowered:
|
||||
paths.add(candidate)
|
||||
return sorted(paths)
|
||||
|
||||
|
||||
def collect_errors(root: Path) -> list[str]:
|
||||
errors: list[str] = []
|
||||
manifest_path = (
|
||||
root / "manifests/runtime/phase-0.9d-existing-stack-readback.json"
|
||||
)
|
||||
schema_path = (
|
||||
root
|
||||
/ "manifests/runtime/phase-0.9d-existing-stack-readback.schema.json"
|
||||
)
|
||||
try:
|
||||
manifest = load_json(manifest_path)
|
||||
schema = load_json(schema_path)
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
return [str(error)]
|
||||
|
||||
errors.extend(validate_manifest(manifest))
|
||||
errors.extend(validate_schema_instance(schema, manifest))
|
||||
|
||||
for relative in DELIVERABLES:
|
||||
if not (root / relative).is_file():
|
||||
errors.append(f"missing deliverable: {relative}")
|
||||
|
||||
for relative, expected in SOURCE_HASHES.items():
|
||||
path = (root / relative).resolve()
|
||||
if not path.is_file():
|
||||
errors.append(f"missing source evidence: {relative}")
|
||||
elif sha256_file(path) != expected:
|
||||
errors.append(f"source evidence changed: {relative}")
|
||||
if manifest.get("source_evidence", {}).get(relative) != expected:
|
||||
errors.append(f"manifest source hash mismatch: {relative}")
|
||||
|
||||
for relative, expected in IMMUTABLE_HASHES.items():
|
||||
path = root / relative
|
||||
if not path.is_file() or sha256_file(path) != expected:
|
||||
errors.append(f"immutable evidence changed: {relative}")
|
||||
if manifest.get("immutable_evidence", {}).get(relative) != expected:
|
||||
errors.append(f"manifest immutable hash mismatch: {relative}")
|
||||
|
||||
denylist = root / "manifests/artifact-denylist.json"
|
||||
if not denylist.is_file() or sha256_file(denylist) != DENYLIST_HASH:
|
||||
errors.append("permanent denylist changed")
|
||||
else:
|
||||
denylist_data = load_json(denylist)
|
||||
entries = json.dumps(denylist_data, sort_keys=True)
|
||||
if BLOCKED_HASH not in entries:
|
||||
errors.append("permanently blocked artifact is absent from denylist")
|
||||
|
||||
for name, (relative, expected) in SOURCE_COMMITS.items():
|
||||
source_root = (root / relative).resolve()
|
||||
try:
|
||||
if git(source_root, "rev-parse", "HEAD") != expected:
|
||||
errors.append(f"source HEAD changed: {name}")
|
||||
if git(source_root, "status", "--porcelain=v1"):
|
||||
errors.append(f"source tree is dirty: {name}")
|
||||
except RuntimeError as error:
|
||||
errors.append(f"{name}: {error}")
|
||||
|
||||
try:
|
||||
if git(root, "branch", "--show-current") != EXPECTED_BRANCH:
|
||||
errors.append("current branch is not the Phase-0.9D branch")
|
||||
except RuntimeError as error:
|
||||
errors.append(str(error))
|
||||
|
||||
allowed_suffixes = {".md", ".json", ".py"}
|
||||
forbidden_roots = ("samples/", "src/", "include/", "packaging/", "adapters/")
|
||||
for relative in _phase09d_paths(root):
|
||||
lowered = relative.lower()
|
||||
if Path(relative).suffix.lower() not in allowed_suffixes:
|
||||
errors.append(f"forbidden Phase-0.9D artifact/source suffix: {relative}")
|
||||
if lowered.startswith(forbidden_roots):
|
||||
errors.append(f"forbidden Phase-0.9D target/product path: {relative}")
|
||||
if lowered.endswith((".elf", ".o", ".a", ".so", ".map", ".s", ".asm", ".ld")):
|
||||
errors.append(f"forbidden Phase-0.9D target artifact: {relative}")
|
||||
|
||||
docs = "\n".join(
|
||||
(root / relative).read_text(encoding="utf-8")
|
||||
for relative in DELIVERABLES
|
||||
if relative.endswith(".md") and (root / relative).is_file()
|
||||
)
|
||||
for marker in (
|
||||
"BLOCKED_NO_READBACK_PATH",
|
||||
"PATH_CONFLICT",
|
||||
"server_active_flag",
|
||||
"autoload_triggered",
|
||||
"OFF_DEVICE_BACKUP_VALID",
|
||||
"RECOVERY_PROVEN",
|
||||
"No PS5 was contacted",
|
||||
):
|
||||
if marker not in docs:
|
||||
errors.append(f"documentation marker missing: {marker}")
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
errors = collect_errors(args.root.resolve())
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"ERROR: {error}")
|
||||
return 1
|
||||
print("Phase-0.9D existing-stack readback audit: PASS")
|
||||
print("Decision: BLOCKED_NO_READBACK_PATH")
|
||||
print("Device contact/write/execution/artifact creation: false")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,798 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate the offline-only Phase-0.9E bootstrap provenance audit."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
from typing import Any
|
||||
import zipfile
|
||||
|
||||
|
||||
EXPECTED_BASELINE = "e5304e394efb0790e1939bef87404f19b58caa43"
|
||||
EXPECTED_BRANCH = "codex/chimera-gfx-phase09e-bootstrap-rescue-provenance"
|
||||
EXPECTED_PHASE = "PHASE_0_9E_BOOTSTRAP_RESCUE_PROVENANCE"
|
||||
EXPECTED_STATUS = "DESIGN_ONLY"
|
||||
EXPECTED_CLASSIFICATION = "BOOTSTRAP_IMPLEMENTATION_MISSING"
|
||||
BLOCKED_HASH = (
|
||||
"4be1c17b4964f2b68c39b5145bc4af4619c32512d60269ecf5c39728b390fa63"
|
||||
)
|
||||
DENYLIST_HASH = (
|
||||
"e9603b0e3792781ad5b511afb22ef61e3d4fd4c5a16bf928f8609193bcd97783"
|
||||
)
|
||||
OUTER_NAME = "Y2JB-Autoloader-403-1240.zip"
|
||||
OUTER_SIZE = 504159435
|
||||
OUTER_SHA256 = (
|
||||
"805e3f87f0c371223619ffc7d3a7b3c0d41a1fae8a8b1171d9e2f162659e8291"
|
||||
)
|
||||
INNER_NAME = "PS5/EXPORT/BACKUP/202606102126_00/archive.dat"
|
||||
INNER_SIZE = 504365056
|
||||
INNER_SHA256 = (
|
||||
"6439834e8856d45b6d6fe699b74c35ca6985a199ea8ecf3e398c018d37be2d55"
|
||||
)
|
||||
ZIP_ENTRIES = (
|
||||
"PS5/",
|
||||
"PS5/EXPORT/",
|
||||
"PS5/EXPORT/BACKUP/",
|
||||
"PS5/EXPORT/BACKUP/202606102126_00/",
|
||||
INNER_NAME,
|
||||
)
|
||||
|
||||
AUTHORIZATION_FIELDS = (
|
||||
"device_action_authorized",
|
||||
"execution_authorized",
|
||||
"installation_authorized",
|
||||
"lifecycle_authorized",
|
||||
"autoload_authorized",
|
||||
"device_write_authorized",
|
||||
"transfer_authorized",
|
||||
"automatic_retry",
|
||||
"target_build_authorized",
|
||||
"rescue_payload_design_authorized",
|
||||
)
|
||||
ACTION_FIELDS = (
|
||||
"internet_used",
|
||||
"fetch_performed",
|
||||
"clone_performed",
|
||||
"package_installed",
|
||||
"ps5_connected",
|
||||
"device_request_performed",
|
||||
"files_transferred",
|
||||
"device_write_performed",
|
||||
"target_execution_performed",
|
||||
"target_build_performed",
|
||||
"target_artifact_created",
|
||||
"rescue_payload_created",
|
||||
"readback_payload_created",
|
||||
"device_client_created",
|
||||
"backup_created",
|
||||
"staging_performed",
|
||||
)
|
||||
PROVENANCE_FIELDS = (
|
||||
"logical_name",
|
||||
"artifact_role",
|
||||
"local_relative_path",
|
||||
"size",
|
||||
"sha256",
|
||||
"file_type",
|
||||
"source_repository",
|
||||
"source_commit",
|
||||
"build_identity",
|
||||
"version",
|
||||
"obtained_from",
|
||||
"evidence_that_it_is_deployed_or_used",
|
||||
"confidence",
|
||||
"immutable",
|
||||
"executable",
|
||||
"persistent_on_device",
|
||||
"transferred_per_session",
|
||||
"required_for_bootstrap",
|
||||
"required_for_recovery",
|
||||
)
|
||||
SOURCE_COMMITS = {
|
||||
"hardened_elfldr": (
|
||||
"../chimera-elfldr",
|
||||
"197623058f509eddde18868dafcb92fdcac66464",
|
||||
),
|
||||
"controlled_payload_manager": (
|
||||
"../chimera-ps5-payload-manager",
|
||||
"e23d94ff91233aa770e2342800c1467875bdef44",
|
||||
),
|
||||
"elfldr_public_base": (
|
||||
"work/upstream/elfldr-v0.23",
|
||||
"699e8bcff03e91e8d6ca6eba281af25c5a58d8c2",
|
||||
),
|
||||
"payload_manager_public_base": (
|
||||
"work/upstream/pldmgr-v0.3.1",
|
||||
"cfbc70f30f419b09bf2b52283f7409e2d3117ee1",
|
||||
),
|
||||
"ps5_payload_sdk_v0_41": (
|
||||
"work/upstream/sdk",
|
||||
"d2e2e585740362976a39fdd5ccf390f199a7bc37",
|
||||
),
|
||||
}
|
||||
SOURCE_HASHES = {
|
||||
"../chimera-elfldr/elfldr-ps5.elf": (
|
||||
397000,
|
||||
"63e810982471eb40cae3a20aa9df9a0a02892f420e429874fae4e99aa400b561",
|
||||
),
|
||||
"../chimera-elfldr/elfldr-ps5.elf.map": (
|
||||
142905,
|
||||
"2ea5ff73299db6e61d5fd998c358ee910bc4e8fa1e6e7c73dbaa842bc813124f",
|
||||
),
|
||||
"../chimera-elfldr/bootstrap.elf.map": (
|
||||
143081,
|
||||
"ec4ae17abeb8270948d8a7d4196593ba94cbdb69376b8b9f7f009b25ab5b3323",
|
||||
),
|
||||
"../chimera-elfldr/socksrv.elf.map": (
|
||||
145072,
|
||||
"bf74db6ee68a60652426936e7037bb5a143078f44cc7774fa04c9bc4bc5447c1",
|
||||
),
|
||||
"../chimera-elfldr/Makefile": (
|
||||
1902,
|
||||
"3df93b48fc61ec67907b136e44ea2085baacf08347864d32befc072a7a783384",
|
||||
),
|
||||
"../chimera-elfldr/README.md": (
|
||||
2821,
|
||||
"372aeb28dc971b2bd98093a47fdaf77c32f75bbdc3b3d7e8678900744b91eadb",
|
||||
),
|
||||
"../chimera-elfldr/main.c": (
|
||||
2473,
|
||||
"876389a26999073994e63ca29926982280d9594a1ee941244205b54f57e2b4d1",
|
||||
),
|
||||
"../chimera-elfldr/bootstrap.c": (
|
||||
2081,
|
||||
"5a8072ec0d6db919cb3a81a7028dc91fd8e2c3a0d69b0fa7e84836fa93b45381",
|
||||
),
|
||||
"../chimera-elfldr/socksrv.c": (
|
||||
11556,
|
||||
"d642ced3e9b4a296dd15e355050ebe956f53a6dfdaa6ac10109cd067a3bba3d7",
|
||||
),
|
||||
"work/upstream/release-assets/elfldr-ps5-v0.23.elf": (
|
||||
397000,
|
||||
"092d16ee0ede0c494947efd38d1a17bbd7cc4b022d3858ea898833c188c703e8",
|
||||
),
|
||||
"../chimera-ps5-payload-manager/pldmgr-controlled.elf": (
|
||||
99560,
|
||||
"8fecf8241a46246eddbd21e8bb4d875f5d76f1f4f4c6a11384df1f131aa5e5b1",
|
||||
),
|
||||
}
|
||||
IMMUTABLE_HASHES = {
|
||||
"manifests/runtime/phase-0.8-read-only-preflight.json": (
|
||||
"47d7f452f8799979fe99b3e6d56859f03544112725bf7e5b349eba5ed81b3322"
|
||||
),
|
||||
"manifests/runtime/phase-0.8-remediation.json": (
|
||||
"a9dafed8c83722c43709dcf90ea117c21d6f996ff8fe233810ed08aa20cbe071"
|
||||
),
|
||||
"manifests/runtime/phase-0.9-anti-brick-design.json": (
|
||||
"39fd7c70cae998d9d74e7caf1ff3c19f9f76f7de2e5a50bc09baede4ba53e9e9"
|
||||
),
|
||||
"manifests/runtime/phase-0.9b-observer.json": (
|
||||
"104c4a667ad17f9827fc7276852c6faeef96effb8e3561a5ebc19a62c7d51634"
|
||||
),
|
||||
"manifests/runtime/phase-0.9c-feasibility.json": (
|
||||
"84eb737ff3486d0c5d8b5ecf06809f93bc573497da4919595b4c1817fd34247c"
|
||||
),
|
||||
"manifests/runtime/phase-0.9d-existing-stack-readback.json": (
|
||||
"86e5aaf034685dbe058b71ffeec645b682f0a8cc7d249e8ac0397155233991de"
|
||||
),
|
||||
}
|
||||
DELIVERABLES = (
|
||||
"docs/runtime/phase-0.9e-bootstrap-provenance.md",
|
||||
"docs/runtime/phase-0.9e-independent-rescue-chain.md",
|
||||
"docs/runtime/phase-0.9e-loader-9020-protocol.md",
|
||||
"docs/runtime/phase-0.9e-reboot-and-crash-model.md",
|
||||
"docs/runtime/phase-0.9e-future-rescue-payload-contract.md",
|
||||
"docs/runtime/phase-0.9e-output-architecture-options.md",
|
||||
"manifests/runtime/phase-0.9e-bootstrap-provenance.json",
|
||||
"manifests/runtime/phase-0.9e-bootstrap-provenance.schema.json",
|
||||
"manifests/runtime/phase-0.9e-loader-protocol.json",
|
||||
"manifests/runtime/phase-0.9e-loader-protocol.schema.json",
|
||||
"tools/validate_phase09e_bootstrap.py",
|
||||
"tests/test_phase09e_bootstrap.py",
|
||||
"packaging/phase09e/SHA256SUMS.txt",
|
||||
)
|
||||
FORBIDDEN_NEW_SUFFIXES = {
|
||||
".c",
|
||||
".cc",
|
||||
".cpp",
|
||||
".s",
|
||||
".asm",
|
||||
".ld",
|
||||
".elf",
|
||||
".self",
|
||||
".sprx",
|
||||
".pkg",
|
||||
".bin",
|
||||
}
|
||||
FORBIDDEN_PRODUCTION_PREFIXES = (
|
||||
"include/",
|
||||
"src/",
|
||||
"adapters/",
|
||||
"samples/",
|
||||
)
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
value = json.load(stream)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{path} does not contain an object")
|
||||
return value
|
||||
|
||||
|
||||
def sha256_file(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 candidate_archive_path(root: Path) -> Path:
|
||||
"""Resolve the same Windows user Downloads path from Windows or WSL."""
|
||||
windows_home_candidate = Path.home() / "Downloads" / OUTER_NAME
|
||||
if windows_home_candidate.is_file():
|
||||
return windows_home_candidate
|
||||
for ancestor in (root, *root.parents):
|
||||
if ancestor.parent.name.lower() == "users":
|
||||
return ancestor / "Downloads" / OUTER_NAME
|
||||
return windows_home_candidate
|
||||
|
||||
|
||||
def git(root: Path, *args: str) -> str:
|
||||
result = subprocess.run(
|
||||
["git", *args],
|
||||
cwd=root,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(result.stderr.strip() or "git command failed")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def provenance_errors(artifact: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
for field in PROVENANCE_FIELDS:
|
||||
if field not in artifact:
|
||||
errors.append(f"missing provenance field {field}")
|
||||
confidence = artifact.get("confidence")
|
||||
if confidence not in {
|
||||
"EXACT_USED",
|
||||
"STRONG_MATCH",
|
||||
"POSSIBLE",
|
||||
"REFERENCE_ONLY",
|
||||
"UNKNOWN",
|
||||
}:
|
||||
errors.append("invalid confidence")
|
||||
obtained = str(artifact.get("obtained_from", "")).lower()
|
||||
repository = str(artifact.get("source_repository") or "").lower()
|
||||
if (
|
||||
"public" in obtained or "upstream" in obtained or "upstream" in repository
|
||||
) and confidence == "EXACT_USED":
|
||||
errors.append("public upstream cannot be exact-used without provenance")
|
||||
if (
|
||||
artifact.get("source_repository") is None
|
||||
and artifact.get("build_identity") is None
|
||||
and artifact.get("source_commit") is not None
|
||||
):
|
||||
errors.append("opaque binary cannot carry an invented source commit")
|
||||
if not re.fullmatch(r"[0-9a-f]{64}", str(artifact.get("sha256", ""))):
|
||||
errors.append("invalid SHA-256")
|
||||
if not isinstance(artifact.get("size"), int) or artifact.get("size", -1) < 0:
|
||||
errors.append("invalid artifact size")
|
||||
return errors
|
||||
|
||||
|
||||
def rescue_classification(
|
||||
*,
|
||||
actual_package_available: bool,
|
||||
requires_elfldr: bool = False,
|
||||
requires_payload_manager: bool = False,
|
||||
replaces_live_component: bool = False,
|
||||
all_required_properties_proven: bool = False,
|
||||
) -> str:
|
||||
if not actual_package_available:
|
||||
return "BOOTSTRAP_IMPLEMENTATION_MISSING"
|
||||
if requires_elfldr or requires_payload_manager:
|
||||
return "SELF_OR_CROSS_DEPENDENT"
|
||||
if replaces_live_component:
|
||||
return "NO_INDEPENDENT_RESCUE_PATH"
|
||||
if all_required_properties_proven:
|
||||
return "INDEPENDENT_RESCUE_EXECUTOR_CANDIDATE"
|
||||
return "PARTIAL_RESCUE_EXECUTOR"
|
||||
|
||||
|
||||
def host_to_memory_classification(
|
||||
*,
|
||||
receive_code: bool,
|
||||
mapping_code: bool,
|
||||
entrypoint_code: bool,
|
||||
device_file_only: bool = False,
|
||||
) -> str:
|
||||
if device_file_only:
|
||||
return "DEVICE_FILE_ONLY"
|
||||
if receive_code and mapping_code and entrypoint_code:
|
||||
return "PROVEN_FROM_SOURCE"
|
||||
if receive_code or mapping_code or entrypoint_code:
|
||||
return "PARTIAL"
|
||||
return "UNKNOWN"
|
||||
|
||||
|
||||
def protocol_model_complete(protocol: dict[str, Any]) -> bool:
|
||||
required = (
|
||||
"maximum_payload_size",
|
||||
"headers",
|
||||
"length_fields",
|
||||
"short_read_detection",
|
||||
"short_write_detection",
|
||||
"bounds_checks",
|
||||
)
|
||||
return all(protocol.get(field) not in {None, "", "UNKNOWN"} for field in required)
|
||||
|
||||
|
||||
def risk_classification(
|
||||
*,
|
||||
temporary_socket: bool = False,
|
||||
live_filesystem_write: bool = False,
|
||||
autoload_activation: bool = False,
|
||||
) -> str:
|
||||
if live_filesystem_write or autoload_activation:
|
||||
return "BRICK_RELEVANT"
|
||||
if temporary_socket:
|
||||
return "LOW_TECHNICAL"
|
||||
return "UNKNOWN"
|
||||
|
||||
|
||||
def reboot_classification(
|
||||
*,
|
||||
exact_package: bool,
|
||||
source_design_restartable: bool,
|
||||
hardware_observed: bool,
|
||||
) -> str:
|
||||
if exact_package and source_design_restartable and hardware_observed:
|
||||
return "REBOOT_RECOVERY_SUPPORTED_BY_DESIGN"
|
||||
if exact_package and source_design_restartable:
|
||||
return "REBOOT_RECOVERY_PLAUSIBLE"
|
||||
return "REBOOT_RECOVERY_UNPROVEN"
|
||||
|
||||
|
||||
def phase09f_design_allowed(
|
||||
*,
|
||||
actual_package_available: bool,
|
||||
independent_from_elfldr: bool,
|
||||
independent_from_payload_manager: bool,
|
||||
no_live_replacement: bool,
|
||||
) -> bool:
|
||||
return all(
|
||||
(
|
||||
actual_package_available,
|
||||
independent_from_elfldr,
|
||||
independent_from_payload_manager,
|
||||
no_live_replacement,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _type_matches(value: Any, expected: str) -> bool:
|
||||
return {
|
||||
"object": isinstance(value, dict),
|
||||
"array": isinstance(value, list),
|
||||
"string": isinstance(value, str),
|
||||
"boolean": isinstance(value, bool),
|
||||
"integer": isinstance(value, int) and not isinstance(value, bool),
|
||||
"number": isinstance(value, (int, float)) and not isinstance(value, bool),
|
||||
"null": value is None,
|
||||
}.get(expected, True)
|
||||
|
||||
|
||||
def validate_schema_instance(
|
||||
schema: dict[str, Any], instance: Any, path: str = "$"
|
||||
) -> list[str]:
|
||||
"""Small offline validator for the schema features used by this phase."""
|
||||
errors: list[str] = []
|
||||
expected_type = schema.get("type")
|
||||
if expected_type and not _type_matches(instance, expected_type):
|
||||
return [f"{path}: expected {expected_type}"]
|
||||
if "const" in schema and instance != schema["const"]:
|
||||
errors.append(f"{path}: expected constant {schema['const']!r}")
|
||||
if "enum" in schema and instance not in schema["enum"]:
|
||||
errors.append(f"{path}: value is outside enum")
|
||||
pattern = schema.get("pattern")
|
||||
if pattern and isinstance(instance, str) and not re.fullmatch(pattern, instance):
|
||||
errors.append(f"{path}: pattern mismatch")
|
||||
if isinstance(instance, dict):
|
||||
for key in schema.get("required", []):
|
||||
if key not in instance:
|
||||
errors.append(f"{path}: missing {key}")
|
||||
properties = schema.get("properties", {})
|
||||
for key, value in instance.items():
|
||||
if key in properties:
|
||||
errors.extend(
|
||||
validate_schema_instance(properties[key], value, f"{path}.{key}")
|
||||
)
|
||||
elif schema.get("additionalProperties") is False:
|
||||
errors.append(f"{path}: unexpected property {key}")
|
||||
elif isinstance(schema.get("additionalProperties"), dict):
|
||||
errors.extend(
|
||||
validate_schema_instance(
|
||||
schema["additionalProperties"], value, f"{path}.{key}"
|
||||
)
|
||||
)
|
||||
if isinstance(instance, list):
|
||||
if "maxItems" in schema and len(instance) > schema["maxItems"]:
|
||||
errors.append(f"{path}: too many items")
|
||||
item_schema = schema.get("items")
|
||||
if isinstance(item_schema, dict):
|
||||
for index, item in enumerate(instance):
|
||||
errors.extend(
|
||||
validate_schema_instance(item_schema, item, f"{path}[{index}]")
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
def validate_manifest(manifest: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
expected_top = {
|
||||
"phase": EXPECTED_PHASE,
|
||||
"status": EXPECTED_STATUS,
|
||||
"baseline_commit": EXPECTED_BASELINE,
|
||||
"branch": EXPECTED_BRANCH,
|
||||
}
|
||||
for field, expected in expected_top.items():
|
||||
if manifest.get(field) != expected:
|
||||
errors.append(f"{field} changed")
|
||||
for field in AUTHORIZATION_FIELDS:
|
||||
if manifest.get("authorization", {}).get(field) is not False:
|
||||
errors.append(f"authorization.{field} must be false")
|
||||
for field in ACTION_FIELDS:
|
||||
if manifest.get("actions", {}).get(field) is not False:
|
||||
errors.append(f"actions.{field} must be false")
|
||||
|
||||
canonical = manifest.get("canonical_state", {})
|
||||
expected_canonical = {
|
||||
"phase09d_decision": "BLOCKED_NO_READBACK_PATH",
|
||||
"phase09c_new_observer": "BLOCKED_MULTIPLE_FOUNDATIONAL_CONTRACTS",
|
||||
"payload_manager_backup": "HARD_BLOCKER_FOR_INSTALLATION",
|
||||
"independent_recovery": "UNPROVEN",
|
||||
"firmware_runtime_behavior": "UNPROVEN",
|
||||
"permanent_denylist_sha256": DENYLIST_HASH,
|
||||
"permanently_blocked_artifact_sha256": BLOCKED_HASH,
|
||||
}
|
||||
for field, expected in expected_canonical.items():
|
||||
if canonical.get(field) != expected:
|
||||
errors.append(f"canonical_state.{field} changed")
|
||||
|
||||
decisions = manifest.get("decisions", {})
|
||||
expected_decisions = {
|
||||
"actual_bootstrap_package_available": False,
|
||||
"actual_bootstrap_identity": None,
|
||||
"bootstrap_provenance": "POSSIBLE",
|
||||
"independent_from_elfldr": "unproven",
|
||||
"independent_from_payload_manager": "unproven",
|
||||
"restartable_after_reboot": "unproven",
|
||||
"host_to_memory": "UNKNOWN",
|
||||
"output_channel_for_future_rescue": "UNKNOWN",
|
||||
"independent_rescue_classification": EXPECTED_CLASSIFICATION,
|
||||
"phase09f_rescue_payload_design_allowed": False,
|
||||
"device_action_authorized": False,
|
||||
"execution_authorized": False,
|
||||
"installation_authorized": False,
|
||||
"automatic_retry": False,
|
||||
}
|
||||
if decisions != expected_decisions:
|
||||
errors.append("decision matrix changed")
|
||||
if rescue_classification(actual_package_available=False) != (
|
||||
EXPECTED_CLASSIFICATION
|
||||
):
|
||||
errors.append("missing implementation is misclassified")
|
||||
for name, (_, expected) in SOURCE_COMMITS.items():
|
||||
if manifest.get("source_commits", {}).get(name) != expected:
|
||||
errors.append(f"source commit changed: {name}")
|
||||
if manifest.get("source_tree_status", {}).get(name) != "clean":
|
||||
errors.append(f"source tree not recorded clean: {name}")
|
||||
|
||||
audit = manifest.get("candidate_archive_audit", {})
|
||||
expected_audit = {
|
||||
"outer_size": OUTER_SIZE,
|
||||
"outer_sha256": OUTER_SHA256,
|
||||
"zip_entry_count": len(ZIP_ENTRIES),
|
||||
"inner_path": INNER_NAME,
|
||||
"inner_size": INNER_SIZE,
|
||||
"inner_sha256": INNER_SHA256,
|
||||
"inner_magic_ascii": "SIECAF",
|
||||
"acceptable_local_parser_present": False,
|
||||
"decryption_attempted": False,
|
||||
"source_identity_proven": False,
|
||||
"deployed_use_proven": False,
|
||||
"classification": "POSSIBLE",
|
||||
}
|
||||
for field, expected in expected_audit.items():
|
||||
if audit.get(field) != expected:
|
||||
errors.append(f"candidate_archive_audit.{field} changed")
|
||||
|
||||
artifacts = manifest.get("artifacts", [])
|
||||
if len(artifacts) != 13:
|
||||
errors.append("artifact inventory must contain 13 records")
|
||||
seen: set[str] = set()
|
||||
for artifact in artifacts:
|
||||
name = str(artifact.get("logical_name"))
|
||||
if name in seen:
|
||||
errors.append(f"duplicate artifact: {name}")
|
||||
seen.add(name)
|
||||
errors.extend(f"{name}: {error}" for error in provenance_errors(artifact))
|
||||
if artifact.get("confidence") == "EXACT_USED":
|
||||
errors.append(f"{name}: no exact-used artifact is proven")
|
||||
if manifest.get("exact_used_artifacts") != []:
|
||||
errors.append("exact_used_artifacts must remain empty")
|
||||
|
||||
host_memory = manifest.get("host_to_memory_analysis", {})
|
||||
if (
|
||||
host_memory.get("classification") != "UNKNOWN"
|
||||
or host_memory.get("receive_code") != "MISSING"
|
||||
or host_memory.get("mapping_code") != "MISSING"
|
||||
or host_memory.get("entrypoint_selection") != "UNKNOWN"
|
||||
):
|
||||
errors.append("host-to-memory is overclaimed")
|
||||
reboot = manifest.get("reboot_and_crash", {})
|
||||
if (
|
||||
reboot.get("classification") != "REBOOT_RECOVERY_UNPROVEN"
|
||||
or reboot.get("automatic_retry") is not False
|
||||
):
|
||||
errors.append("reboot recovery is overclaimed or retry enabled")
|
||||
|
||||
finding_classes = {
|
||||
item.get("id"): item.get("classification")
|
||||
for item in manifest.get("security_findings", [])
|
||||
}
|
||||
if finding_classes.get("unknown_live_filesystem_effect") != "BRICK_RELEVANT":
|
||||
errors.append("unknown live filesystem effect is not brick relevant")
|
||||
if finding_classes.get("unknown_autoload_effect") != "BRICK_RELEVANT":
|
||||
errors.append("unknown autoload effect is not brick relevant")
|
||||
if finding_classes.get(
|
||||
"temporary_socket_not_automatically_brick_relevant"
|
||||
) == "BRICK_RELEVANT":
|
||||
errors.append("temporary socket is automatically overclassified")
|
||||
|
||||
contract = manifest.get("future_rescue_contract", {})
|
||||
if (
|
||||
contract.get("requirements_total") != 20
|
||||
or contract.get("bootstrap_supported_now") != []
|
||||
or contract.get("target_source_allowed") is not False
|
||||
):
|
||||
errors.append("future rescue contract is not fully blocked")
|
||||
if len(manifest.get("output_options", [])) != 5:
|
||||
errors.append("output architecture options are incomplete")
|
||||
if len(manifest.get("missing_actual_files", [])) < 5:
|
||||
errors.append("missing actual bootstrap inputs are incomplete")
|
||||
final = manifest.get("final_decision", {})
|
||||
if final.get("classification") != EXPECTED_CLASSIFICATION:
|
||||
errors.append("final decision changed")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_protocol(protocol_manifest: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
expected = {
|
||||
"phase": "PHASE_0_9E_LOADER_9020_PROTOCOL",
|
||||
"status": "UNPROVEN_IMPLEMENTATION_MISSING",
|
||||
"exact_server_source_present": False,
|
||||
"exact_server_binary_present": False,
|
||||
"exact_host_client_present": False,
|
||||
"protocol_identity": None,
|
||||
"classification": "CONCEPTUAL_9020_DESCRIPTION_IS_NOT_PROTOCOL_PROOF",
|
||||
}
|
||||
for field, value in expected.items():
|
||||
if protocol_manifest.get(field) != value:
|
||||
errors.append(f"protocol {field} changed")
|
||||
protocol = protocol_manifest.get("protocol", {})
|
||||
if protocol.get("port") != 9020:
|
||||
errors.append("protocol port changed")
|
||||
if protocol_model_complete(protocol):
|
||||
errors.append("missing implementation cannot yield a complete protocol")
|
||||
for field in (
|
||||
"handshake",
|
||||
"length_fields",
|
||||
"maximum_payload_size",
|
||||
"timeout",
|
||||
"retry",
|
||||
"response_direction",
|
||||
"parser",
|
||||
"mappings",
|
||||
"entrypoint_validation",
|
||||
"filesystem_staging",
|
||||
"cleanup",
|
||||
):
|
||||
if protocol.get(field) not in {"UNKNOWN", "MISSING"}:
|
||||
errors.append(f"protocol.{field} is overclaimed")
|
||||
host_model = protocol_manifest.get("host_model", {})
|
||||
for field in (
|
||||
"allowed",
|
||||
"created",
|
||||
"network_port_opened",
|
||||
"device_connection_performed",
|
||||
"payload_sent",
|
||||
"binary_executed",
|
||||
):
|
||||
if host_model.get(field) is not False:
|
||||
errors.append(f"host_model.{field} must be false")
|
||||
return errors
|
||||
|
||||
|
||||
def _changed_paths(root: Path) -> set[str]:
|
||||
paths = {
|
||||
path
|
||||
for path in git(root, "diff", "--name-only", EXPECTED_BASELINE).splitlines()
|
||||
if path
|
||||
}
|
||||
status = git(root, "status", "--porcelain=v1", "--untracked-files=all")
|
||||
for line in status.splitlines():
|
||||
if not line:
|
||||
continue
|
||||
path = line[3:]
|
||||
if " -> " in path:
|
||||
path = path.split(" -> ", 1)[1]
|
||||
paths.add(path.replace("\\", "/"))
|
||||
return paths
|
||||
|
||||
|
||||
def phase09e_path_errors(paths: set[str]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
for path in paths:
|
||||
normalized = path.replace("\\", "/")
|
||||
suffix = Path(normalized).suffix.lower()
|
||||
if normalized.startswith(FORBIDDEN_PRODUCTION_PREFIXES):
|
||||
errors.append(f"production/target path changed: {normalized}")
|
||||
if suffix in FORBIDDEN_NEW_SUFFIXES:
|
||||
errors.append(f"forbidden target artifact/source added: {normalized}")
|
||||
lowered = normalized.lower()
|
||||
if any(
|
||||
token in lowered
|
||||
for token in ("rescue.elf", "readback.elf", "executionpackage", "installpackage")
|
||||
):
|
||||
errors.append(f"forbidden Phase-0.9E output added: {normalized}")
|
||||
if "tests/host/phase09e_protocol_model.py" in paths:
|
||||
errors.append("protocol model created without exact protocol source")
|
||||
return errors
|
||||
|
||||
|
||||
def _validate_checksum_file(root: Path) -> list[str]:
|
||||
path = root / "packaging/phase09e/SHA256SUMS.txt"
|
||||
if not path.is_file():
|
||||
return ["missing packaging/phase09e/SHA256SUMS.txt"]
|
||||
errors: list[str] = []
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
parts = line.split(" ", 1)
|
||||
if len(parts) != 2 or not re.fullmatch(r"[0-9a-f]{64}", parts[0]):
|
||||
errors.append(f"invalid checksum line: {line}")
|
||||
continue
|
||||
expected, label = parts
|
||||
if label == f"~/Downloads/{OUTER_NAME}":
|
||||
actual = sha256_file(candidate_archive_path(root))
|
||||
elif label == f"~/Downloads/{OUTER_NAME}::{INNER_NAME}":
|
||||
with zipfile.ZipFile(candidate_archive_path(root)) as archive:
|
||||
digest = hashlib.sha256()
|
||||
with archive.open(INNER_NAME) as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
actual = digest.hexdigest()
|
||||
else:
|
||||
candidate = root / label
|
||||
if not candidate.is_file():
|
||||
errors.append(f"checksum target missing: {label}")
|
||||
continue
|
||||
actual = sha256_file(candidate)
|
||||
if actual != expected:
|
||||
errors.append(f"checksum mismatch: {label}")
|
||||
return errors
|
||||
|
||||
|
||||
def collect_errors(root: Path) -> list[str]:
|
||||
errors: list[str] = []
|
||||
manifest_path = (
|
||||
root / "manifests/runtime/phase-0.9e-bootstrap-provenance.json"
|
||||
)
|
||||
protocol_path = root / "manifests/runtime/phase-0.9e-loader-protocol.json"
|
||||
schema_path = (
|
||||
root / "manifests/runtime/phase-0.9e-bootstrap-provenance.schema.json"
|
||||
)
|
||||
protocol_schema_path = (
|
||||
root / "manifests/runtime/phase-0.9e-loader-protocol.schema.json"
|
||||
)
|
||||
try:
|
||||
manifest = load_json(manifest_path)
|
||||
protocol_manifest = load_json(protocol_path)
|
||||
schema = load_json(schema_path)
|
||||
protocol_schema = load_json(protocol_schema_path)
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
return [str(error)]
|
||||
errors.extend(validate_schema_instance(schema, manifest))
|
||||
errors.extend(validate_schema_instance(protocol_schema, protocol_manifest))
|
||||
errors.extend(validate_manifest(manifest))
|
||||
errors.extend(validate_protocol(protocol_manifest))
|
||||
|
||||
for relative in DELIVERABLES:
|
||||
if not (root / relative).is_file():
|
||||
errors.append(f"missing deliverable: {relative}")
|
||||
if git(root, "rev-parse", "--abbrev-ref", "HEAD") != EXPECTED_BRANCH:
|
||||
errors.append("current branch is not the Phase-0.9E branch")
|
||||
if git(root, "merge-base", EXPECTED_BASELINE, "HEAD") != EXPECTED_BASELINE:
|
||||
errors.append("Phase-0.9E branch no longer descends from baseline")
|
||||
if sha256_file(root / "manifests/artifact-denylist.json") != DENYLIST_HASH:
|
||||
errors.append("permanent denylist changed")
|
||||
for relative, expected in IMMUTABLE_HASHES.items():
|
||||
path = root / relative
|
||||
if not path.is_file() or sha256_file(path) != expected:
|
||||
errors.append(f"immutable evidence changed: {relative}")
|
||||
for name, (relative, expected) in SOURCE_COMMITS.items():
|
||||
source_root = (root / relative).resolve()
|
||||
if git(source_root, "rev-parse", "HEAD") != expected:
|
||||
errors.append(f"source HEAD changed: {name}")
|
||||
if git(source_root, "status", "--porcelain"):
|
||||
errors.append(f"source tree dirty: {name}")
|
||||
for relative, (expected_size, expected_hash) in SOURCE_HASHES.items():
|
||||
path = (root / relative).resolve()
|
||||
if not path.is_file():
|
||||
errors.append(f"source artifact missing: {relative}")
|
||||
continue
|
||||
if path.stat().st_size != expected_size or sha256_file(path) != expected_hash:
|
||||
errors.append(f"source artifact identity changed: {relative}")
|
||||
|
||||
outer = candidate_archive_path(root)
|
||||
if not outer.is_file():
|
||||
errors.append(f"candidate archive missing: {outer}")
|
||||
else:
|
||||
if outer.stat().st_size != OUTER_SIZE or sha256_file(outer) != OUTER_SHA256:
|
||||
errors.append("candidate outer archive identity changed")
|
||||
try:
|
||||
with zipfile.ZipFile(outer) as archive:
|
||||
if tuple(item.filename for item in archive.infolist()) != ZIP_ENTRIES:
|
||||
errors.append("candidate ZIP inventory changed")
|
||||
info = archive.getinfo(INNER_NAME)
|
||||
if info.file_size != INNER_SIZE:
|
||||
errors.append("candidate inner size changed")
|
||||
digest = hashlib.sha256()
|
||||
magic = b""
|
||||
with archive.open(INNER_NAME) as stream:
|
||||
while True:
|
||||
chunk = stream.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
if not magic:
|
||||
magic = chunk[:6]
|
||||
digest.update(chunk)
|
||||
if magic != b"SIECAF" or digest.hexdigest() != INNER_SHA256:
|
||||
errors.append("candidate inner identity changed")
|
||||
except (OSError, KeyError, zipfile.BadZipFile) as error:
|
||||
errors.append(f"candidate ZIP audit failed: {error}")
|
||||
errors.extend(phase09e_path_errors(_changed_paths(root)))
|
||||
errors.extend(_validate_checksum_file(root))
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
errors = collect_errors(args.root.resolve())
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"ERROR: {error}")
|
||||
return 1
|
||||
print("Phase-0.9E bootstrap provenance validation: PASS")
|
||||
print(f"Decision: {EXPECTED_CLASSIFICATION}")
|
||||
print("Device actions: NONE")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,658 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate the host-only Phase-0.9E-R2 inner-backup correlation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
import zipfile
|
||||
|
||||
|
||||
BASELINE = "48714bb542f3ea5b9893d0461ba4c9d938cd6d8d"
|
||||
BRANCH = "codex/chimera-gfx-phase09e-r2-inner-correlation"
|
||||
PHASE = "PHASE_0_9E_R2_INNER_BACKUP_CORRELATION"
|
||||
OUTER_NAME = "Y2JB-Autoloader-403-1240.zip"
|
||||
OUTER_SIZE = 504159435
|
||||
OUTER_SHA256 = "805e3f87f0c371223619ffc7d3a7b3c0d41a1fae8a8b1171d9e2f162659e8291"
|
||||
INNER_NAME = "PS5/EXPORT/BACKUP/202606102126_00/archive.dat"
|
||||
INNER_SIZE = 504365056
|
||||
INNER_SHA256 = "6439834e8856d45b6d6fe699b74c35ca6985a199ea8ecf3e398c018d37be2d55"
|
||||
DENYLIST_SHA256 = "e9603b0e3792781ad5b511afb22ef61e3d4fd4c5a16bf928f8609193bcd97783"
|
||||
PRIMARY_CLASSIFICATION = "LOCAL_BACKUP_UNCORRELATED"
|
||||
AUTHORIZATION_FIELDS = (
|
||||
"device_action_authorized",
|
||||
"target_build_authorized",
|
||||
"transfer_authorized",
|
||||
"execution_authorized",
|
||||
"installation_authorized",
|
||||
"lifecycle_authorized",
|
||||
"autoload_authorized",
|
||||
"device_write_authorized",
|
||||
"rescue_payload_design_authorized",
|
||||
"automatic_retry",
|
||||
)
|
||||
IMMUTABLE_HASHES = {
|
||||
"manifests/runtime/phase-0.8-read-only-preflight.json": "47d7f452f8799979fe99b3e6d56859f03544112725bf7e5b349eba5ed81b3322",
|
||||
"manifests/runtime/phase-0.8-remediation.json": "a9dafed8c83722c43709dcf90ea117c21d6f996ff8fe233810ed08aa20cbe071",
|
||||
"manifests/runtime/phase-0.9-anti-brick-design.json": "39fd7c70cae998d9d74e7caf1ff3c19f9f76f7de2e5a50bc09baede4ba53e9e9",
|
||||
"manifests/runtime/phase-0.9b-observer.json": "104c4a667ad17f9827fc7276852c6faeef96effb8e3561a5ebc19a62c7d51634",
|
||||
"manifests/runtime/phase-0.9c-feasibility.json": "84eb737ff3486d0c5d8b5ecf06809f93bc573497da4919595b4c1817fd34247c",
|
||||
"manifests/runtime/phase-0.9d-existing-stack-readback.json": "86e5aaf034685dbe058b71ffeec645b682f0a8cc7d249e8ac0397155233991de",
|
||||
"manifests/runtime/phase-0.9e-bootstrap-provenance.json": "5dfa9bfe2ae751b2f0ea0e03c60c1a4471a35452389cf471456e6c54eb4601cf",
|
||||
"manifests/runtime/phase-0.9e-loader-protocol.json": "a7ca8b4e8072cb60ad4cd4869f6c508e8014059c8d81ad3b94a8d9e8cd0db4cb",
|
||||
"manifests/runtime/phase-0.9e-r-release-correlation.json": "09f916b20def51fd519690b6d940de5f93818a4d9bd5eebef462fe2a828b88be",
|
||||
"manifests/runtime/phase-0.9e-r-port9020-audit.json": "ea84f1885ad4286670a401cbf73e9a371cdcd03a1f247908b90ea27553ee820e",
|
||||
}
|
||||
DELIVERABLES = (
|
||||
"docs/runtime/phase-0.9e-r2-local-download-provenance.md",
|
||||
"docs/runtime/phase-0.9e-r2-inner-archive-correlation.md",
|
||||
"docs/runtime/phase-0.9e-r2-siecaf-structural-analysis.md",
|
||||
"docs/runtime/phase-0.9e-r2-community-backup-correlation.md",
|
||||
"docs/runtime/phase-0.9e-r2-final-provenance-decision.md",
|
||||
"manifests/runtime/phase-0.9e-r2-inner-correlation.json",
|
||||
"manifests/runtime/phase-0.9e-r2-siecaf-fingerprints.json",
|
||||
"tools/inspect_siecaf_header.py",
|
||||
"tools/validate_phase09er2_correlation.py",
|
||||
"tests/test_phase09er2_correlation.py",
|
||||
"tests/test_siecaf_header_parser.py",
|
||||
"packaging/phase09er2/SHA256SUMS.txt",
|
||||
)
|
||||
FORBIDDEN_SUFFIXES = {
|
||||
".c",
|
||||
".cc",
|
||||
".cpp",
|
||||
".s",
|
||||
".asm",
|
||||
".ld",
|
||||
".elf",
|
||||
".self",
|
||||
".sprx",
|
||||
".pkg",
|
||||
".bin",
|
||||
".wasm",
|
||||
".sqlite",
|
||||
".download",
|
||||
}
|
||||
FORBIDDEN_PREFIXES = ("include/", "src/", "adapters/", "samples/")
|
||||
MAX_TRACKED_FILE_SIZE = 10 * 1024 * 1024
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
value = json.load(stream)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{path} is not a JSON object")
|
||||
return value
|
||||
|
||||
|
||||
def sha256_file(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 sha256_zip_entry(path: Path, entry: str) -> tuple[int, str]:
|
||||
digest = hashlib.sha256()
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
info = archive.getinfo(entry)
|
||||
with archive.open(info) as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return info.file_size, digest.hexdigest()
|
||||
|
||||
|
||||
def git(root: Path, *args: str) -> str:
|
||||
result = subprocess.run(
|
||||
["git", *args],
|
||||
cwd=root,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(result.stderr.strip() or "git command failed")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def load_module(name: str, path: Path) -> ModuleType:
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"could not import {path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def candidate_archive_path(root: Path) -> Path:
|
||||
direct = Path.home() / "Downloads" / OUTER_NAME
|
||||
if direct.is_file():
|
||||
return direct
|
||||
for ancestor in (root, *root.parents):
|
||||
if ancestor.parent.name.lower() == "users":
|
||||
return ancestor / "Downloads" / OUTER_NAME
|
||||
return direct
|
||||
|
||||
|
||||
def inner_byte_match(
|
||||
*,
|
||||
left_size: int,
|
||||
right_size: int,
|
||||
left_sha256: str,
|
||||
right_sha256: str,
|
||||
full_byte_equal: bool,
|
||||
) -> bool:
|
||||
"""Require count, digest, and complete comparison for an inner match."""
|
||||
return left_size == right_size and left_sha256 == right_sha256 and full_byte_equal
|
||||
|
||||
|
||||
def classify_inner_comparison(
|
||||
*,
|
||||
download_valid: bool,
|
||||
inner_present: bool,
|
||||
left_size: int,
|
||||
right_size: int,
|
||||
left_sha256: str,
|
||||
right_sha256: str,
|
||||
full_byte_equal: bool,
|
||||
) -> str:
|
||||
if not download_valid:
|
||||
return "OFFICIAL_ASSET_DOWNLOAD_INVALID"
|
||||
if not inner_present:
|
||||
return "EXPECTED_INNER_ARCHIVE_ABSENT"
|
||||
if inner_byte_match(
|
||||
left_size=left_size,
|
||||
right_size=right_size,
|
||||
left_sha256=left_sha256,
|
||||
right_sha256=right_sha256,
|
||||
full_byte_equal=full_byte_equal,
|
||||
):
|
||||
return "INNER_ARCHIVE_BYTE_MATCH"
|
||||
if left_size == right_size:
|
||||
return "INNER_ARCHIVE_SIZE_ONLY_MATCH"
|
||||
return "INNER_ARCHIVE_HASH_MISMATCH"
|
||||
|
||||
|
||||
def phase09f_reconsideration_allowed(
|
||||
*,
|
||||
inner_source_bound: bool,
|
||||
mediafire_maker_source_bound: bool,
|
||||
auditable_bootstrap_closure: bool,
|
||||
) -> bool:
|
||||
return auditable_bootstrap_closure and (
|
||||
inner_source_bound or mediafire_maker_source_bound
|
||||
)
|
||||
|
||||
|
||||
def _canonical_hash(records: list[dict[str, Any]]) -> str:
|
||||
encoded = json.dumps(
|
||||
records, sort_keys=True, separators=(",", ":"), ensure_ascii=True
|
||||
).encode("ascii")
|
||||
return hashlib.sha256(encoded).hexdigest()
|
||||
|
||||
|
||||
def _normalized_records(
|
||||
archive: dict[str, Any],
|
||||
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
segment_fields = (
|
||||
"section_id",
|
||||
"part_number",
|
||||
"data_offset",
|
||||
"aligned_length",
|
||||
"unaligned_length",
|
||||
"hash_key_id_or_algorithm_type",
|
||||
"encryption_key_id_or_algorithm_version",
|
||||
"iv_hex",
|
||||
)
|
||||
layout_fields = segment_fields[:-1]
|
||||
hash_fields = ("section_id", "section_type", "section_hash_128_hex")
|
||||
segments = [
|
||||
{field: item[field] for field in segment_fields}
|
||||
for item in sorted(
|
||||
archive["segments"],
|
||||
key=lambda value: (
|
||||
value["section_id"],
|
||||
value["part_number"],
|
||||
value["data_offset"],
|
||||
),
|
||||
)
|
||||
]
|
||||
hashes = [
|
||||
{field: item[field] for field in hash_fields}
|
||||
for item in sorted(
|
||||
archive["section_hashes"],
|
||||
key=lambda value: (value["section_id"], value["section_type"]),
|
||||
)
|
||||
]
|
||||
layout = [{field: item[field] for field in layout_fields} for item in segments]
|
||||
return segments, hashes, layout
|
||||
|
||||
|
||||
def validate_fingerprint_archive(name: str, archive: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if archive.get("classification") != "SIECAF_VALID_STRUCTURE":
|
||||
errors.append(f"{name}: structure is not valid")
|
||||
return errors
|
||||
header = archive.get("header", {})
|
||||
if (
|
||||
"key_or_unknown_16_hex" in header
|
||||
or "raw_hex" in header
|
||||
or header.get("key_or_unknown_16_redacted") is not True
|
||||
or len(header.get("key_or_unknown_16_sha256", "")) != 64
|
||||
):
|
||||
errors.append(f"{name}: cryptographic header material is not redacted")
|
||||
count = header.get("segment_count")
|
||||
if len(archive.get("segments", [])) != count:
|
||||
errors.append(f"{name}: segment count mismatch")
|
||||
if len(archive.get("section_hashes", [])) != count:
|
||||
errors.append(f"{name}: hash count mismatch")
|
||||
segments, hashes, layout = _normalized_records(archive)
|
||||
if _canonical_hash(segments) != archive.get("normalized_segment_table_sha256"):
|
||||
errors.append(f"{name}: normalized segment hash mismatch")
|
||||
if _canonical_hash(hashes) != archive.get("normalized_hash_blocks_sha256"):
|
||||
errors.append(f"{name}: normalized hash-block hash mismatch")
|
||||
layout_value = [
|
||||
{
|
||||
"unknown_u64": header.get("unknown_u64"),
|
||||
"version_i32": header.get("version_i32"),
|
||||
"segment_count": count,
|
||||
"file_offset": header.get("file_offset"),
|
||||
"file_size": header.get("file_size"),
|
||||
},
|
||||
*layout,
|
||||
]
|
||||
if _canonical_hash(layout_value) != archive.get("normalized_layout_sha256"):
|
||||
errors.append(f"{name}: normalized layout hash mismatch")
|
||||
structural = _canonical_hash(
|
||||
[
|
||||
{
|
||||
"header_raw_sha256": header.get("raw_sha256"),
|
||||
"normalized_segment_table_sha256": archive.get(
|
||||
"normalized_segment_table_sha256"
|
||||
),
|
||||
"normalized_hash_blocks_sha256": archive.get(
|
||||
"normalized_hash_blocks_sha256"
|
||||
),
|
||||
}
|
||||
]
|
||||
)
|
||||
if structural != archive.get("structural_fingerprint_sha256"):
|
||||
errors.append(f"{name}: structural fingerprint mismatch")
|
||||
for field in (
|
||||
"duplicate_metadata_section_keys",
|
||||
"duplicate_hash_section_ids",
|
||||
"overlaps",
|
||||
"gaps",
|
||||
"errors",
|
||||
"warnings",
|
||||
):
|
||||
if archive.get(field):
|
||||
errors.append(f"{name}: unexpected {field}")
|
||||
if archive.get("trailing_data_bytes") != 0:
|
||||
errors.append(f"{name}: trailing data is not zero")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_fingerprints(
|
||||
root: Path,
|
||||
value: dict[str, Any],
|
||||
inspector: ModuleType,
|
||||
local_path: Path,
|
||||
) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if value.get("phase") != PHASE or value.get("status") != "HOST_ONLY_EVIDENCE":
|
||||
errors.append("fingerprint manifest phase/status mismatch")
|
||||
evidence = value.get("parser_evidence", {})
|
||||
if (
|
||||
evidence.get("source_commit") != "36d014672bc87577a6e0d750c2cccadc3fae0854"
|
||||
or evidence.get("header_blob") != "fdbc368353a7797464873ada306f0297257eb95e"
|
||||
):
|
||||
errors.append("public SIECAF source binding changed")
|
||||
for field in (
|
||||
"decryption_attempted",
|
||||
"content_extraction_attempted",
|
||||
"ps5_bar_tool_executed",
|
||||
):
|
||||
if evidence.get(field) is not False:
|
||||
errors.append(f"fingerprint manifest permits {field}")
|
||||
archives = value.get("archives", {})
|
||||
expected_names = {
|
||||
"local",
|
||||
"official_y2jb_1_6_4_03",
|
||||
"community_owendswang_v1_4_autoloader_7_61",
|
||||
}
|
||||
if set(archives) != expected_names:
|
||||
errors.append("fingerprint archive set mismatch")
|
||||
return errors
|
||||
for name, archive in archives.items():
|
||||
errors.extend(validate_fingerprint_archive(name, archive))
|
||||
with zipfile.ZipFile(local_path) as local:
|
||||
info = local.getinfo(INNER_NAME)
|
||||
with local.open(info) as stream:
|
||||
observed = inspector.inspect_siecaf(
|
||||
stream, info.file_size, source_label="local-mediafire-inner"
|
||||
)
|
||||
if observed != archives["local"]:
|
||||
errors.append("local SIECAF evidence does not reproduce")
|
||||
pairs = {
|
||||
"local_vs_official_y2jb_1_6_4_03": (
|
||||
archives["local"],
|
||||
archives["official_y2jb_1_6_4_03"],
|
||||
),
|
||||
"local_vs_community_owendswang_v1_4_autoloader_7_61": (
|
||||
archives["local"],
|
||||
archives["community_owendswang_v1_4_autoloader_7_61"],
|
||||
),
|
||||
"official_vs_community": (
|
||||
archives["official_y2jb_1_6_4_03"],
|
||||
archives["community_owendswang_v1_4_autoloader_7_61"],
|
||||
),
|
||||
}
|
||||
comparisons = value.get("comparisons", {})
|
||||
for name, (left, right) in pairs.items():
|
||||
expected = inspector.compare_structures(left, right)
|
||||
if comparisons.get(name) != expected:
|
||||
errors.append(f"{name}: comparison does not reproduce")
|
||||
if expected["classification"] != "SIECAF_LAYOUT_DIFFERENT":
|
||||
errors.append(f"{name}: expected fail-closed layout difference")
|
||||
authorization = value.get("authorization", {})
|
||||
for field in AUTHORIZATION_FIELDS:
|
||||
if authorization.get(field) is not False:
|
||||
errors.append(f"fingerprints authorize {field}")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_manifest(value: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if value.get("phase") != PHASE or value.get("status") != "HOST_ONLY_COMPLETE":
|
||||
errors.append("main manifest phase/status mismatch")
|
||||
if value.get("baseline_commit") != BASELINE:
|
||||
errors.append("baseline commit mismatch")
|
||||
if value.get("prior_phase_classification") != "LOCAL_BACKUP_NOT_CORRELATED":
|
||||
errors.append("prior classification was not preserved")
|
||||
if value.get("primary_provenance_classification") != PRIMARY_CLASSIFICATION:
|
||||
errors.append("primary provenance classification mismatch")
|
||||
for field in (
|
||||
"outer_zip_official",
|
||||
"runtime_deployment_verified",
|
||||
"current_device_contents_verified",
|
||||
"phase09f_offline_design_reconsideration_allowed",
|
||||
):
|
||||
if value.get(field) is not False:
|
||||
errors.append(f"{field} must remain false")
|
||||
if value.get("runtime_firmware_9_60") != "UNPROVEN":
|
||||
errors.append("runtime firmware state was overclaimed")
|
||||
outer = value.get("local_candidate", {}).get("outer", {})
|
||||
inner = value.get("local_candidate", {}).get("inner", {})
|
||||
if (
|
||||
outer.get("name"),
|
||||
outer.get("size"),
|
||||
outer.get("sha256"),
|
||||
) != (OUTER_NAME, OUTER_SIZE, OUTER_SHA256):
|
||||
errors.append("local outer identity mismatch")
|
||||
if (
|
||||
inner.get("entry"),
|
||||
inner.get("size"),
|
||||
inner.get("sha256"),
|
||||
) != (INNER_NAME, INNER_SIZE, INNER_SHA256):
|
||||
errors.append("local inner identity mismatch")
|
||||
provenance = value.get("local_download_provenance", {})
|
||||
zone = provenance.get("zone_identifier", {})
|
||||
if provenance.get("classification") != "MEDIAFIRE_URL_EXACT":
|
||||
errors.append("local URL provenance classification mismatch")
|
||||
if zone.get("mediafire_file_key") != "jq3fcutuwbb1mrb":
|
||||
errors.append("MediaFire key mismatch")
|
||||
if "<redacted-signed-segment>" not in zone.get("host_url", {}).get(
|
||||
"path_redacted", ""
|
||||
):
|
||||
errors.append("signed MediaFire path is not redacted")
|
||||
browser = provenance.get("browser_history", {})
|
||||
if (
|
||||
browser.get("matching_records") != 0
|
||||
or browser.get("full_history_exported") is not False
|
||||
or browser.get("original_databases_modified") is not False
|
||||
):
|
||||
errors.append("browser-history boundary mismatch")
|
||||
mediafire = value.get("mediafire_object", {})
|
||||
if mediafire.get("classification") != "MEDIAFIRE_OBJECT_METADATA_BOUND":
|
||||
errors.append("MediaFire object classification mismatch")
|
||||
if mediafire.get("redownload_performed") is not False:
|
||||
errors.append("MediaFire duplicate download was recorded")
|
||||
official = value.get("official_candidate", {})
|
||||
if (
|
||||
official.get("asset_id") != 442358421
|
||||
or official.get("official_size") != 504395044
|
||||
or official.get("official_sha256")
|
||||
!= "b01b4f442327f9eca90ffc4506dfa58249e4ac70cb9d7488c856c9e8dfaf37b4"
|
||||
or official.get("download_attempts") != 1
|
||||
or official.get("resume") is not False
|
||||
or official.get("automatic_retry") is not False
|
||||
or official.get("download_valid") is not True
|
||||
):
|
||||
errors.append("official download evidence mismatch")
|
||||
comparison = official.get("inner_comparison", {})
|
||||
observed_class = classify_inner_comparison(
|
||||
download_valid=official.get("download_valid") is True,
|
||||
inner_present=True,
|
||||
left_size=comparison.get("official_inner_size", -1),
|
||||
right_size=comparison.get("local_inner_size", -1),
|
||||
left_sha256=comparison.get("official_inner_sha256", ""),
|
||||
right_sha256=comparison.get("local_inner_sha256", ""),
|
||||
full_byte_equal=comparison.get("full_byte_equal") is True,
|
||||
)
|
||||
if comparison.get("classification") != observed_class:
|
||||
errors.append("official inner classification does not reproduce")
|
||||
families = value.get("community_families", [])
|
||||
if len(families) != 3:
|
||||
errors.append("community family count mismatch")
|
||||
downloaded = [item for item in families if item.get("large_backup_downloaded")]
|
||||
if len(downloaded) != 1:
|
||||
errors.append("community large-download count mismatch")
|
||||
elif (
|
||||
downloaded[0].get("downloaded_asset", {}).get("inner_comparison")
|
||||
!= "INNER_ARCHIVE_HASH_MISMATCH"
|
||||
):
|
||||
errors.append("community inner mismatch is not preserved")
|
||||
siecaf = value.get("siecaf", {})
|
||||
for field in (
|
||||
"local_vs_official",
|
||||
"local_vs_community",
|
||||
"official_vs_community",
|
||||
):
|
||||
if siecaf.get(field) != "SIECAF_LAYOUT_DIFFERENT":
|
||||
errors.append(f"{field}: structural result overclaimed")
|
||||
for field in (
|
||||
"decryption_attempted",
|
||||
"content_extraction_attempted",
|
||||
"ps5_bar_tool_executed",
|
||||
):
|
||||
if siecaf.get(field) is not False:
|
||||
errors.append(f"main manifest permits {field}")
|
||||
authorization = value.get("authorization", {})
|
||||
for field in AUTHORIZATION_FIELDS:
|
||||
if authorization.get(field) is not False:
|
||||
errors.append(f"main manifest authorizes {field}")
|
||||
actions = value.get("actions", {})
|
||||
prohibited_true = (
|
||||
"download_resume_used",
|
||||
"automatic_retry_used",
|
||||
"downloaded_file_executed",
|
||||
"downloaded_file_restored",
|
||||
"ps5_bar_tool_executed",
|
||||
"ps5_connected",
|
||||
"ps5_ip_used",
|
||||
"device_request_performed",
|
||||
"files_transferred_to_or_from_ps5",
|
||||
"target_build_performed",
|
||||
"target_code_created",
|
||||
"target_artifact_created",
|
||||
"payload_created",
|
||||
"device_client_created",
|
||||
)
|
||||
for field in prohibited_true:
|
||||
if actions.get(field) is not False:
|
||||
errors.append(f"prohibited action recorded: {field}")
|
||||
storage = value.get("temporary_research_storage", {})
|
||||
if (
|
||||
storage.get("cleaned") is not True
|
||||
or storage.get("cleanup_verified") is not True
|
||||
or storage.get("large_files_tracked") is not False
|
||||
or storage.get("browser_database_copies_tracked") is not False
|
||||
):
|
||||
errors.append("temporary-storage final state mismatch")
|
||||
if value.get("blocking_state", {}).get("payload_manager_backup") != (
|
||||
"HARD_BLOCKER_FOR_INSTALLATION"
|
||||
):
|
||||
errors.append("Payload Manager installation blocker changed")
|
||||
if phase09f_reconsideration_allowed(
|
||||
inner_source_bound=False,
|
||||
mediafire_maker_source_bound=False,
|
||||
auditable_bootstrap_closure=False,
|
||||
):
|
||||
errors.append("Phase 0.9F fail-closed decision failed")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_checksums(root: Path) -> list[str]:
|
||||
errors: list[str] = []
|
||||
path = root / "packaging/phase09er2/SHA256SUMS.txt"
|
||||
if not path.is_file():
|
||||
return ["Phase-0.9E-R2 checksum inventory is missing"]
|
||||
entries = 0
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
match = re.fullmatch(r"([0-9a-f]{64}) (.+)", line)
|
||||
if not match:
|
||||
errors.append(f"malformed checksum line: {line}")
|
||||
continue
|
||||
expected, relative = match.groups()
|
||||
target = root / relative
|
||||
if not target.is_file():
|
||||
errors.append(f"checksum target missing: {relative}")
|
||||
elif sha256_file(target) != expected:
|
||||
errors.append(f"checksum mismatch: {relative}")
|
||||
entries += 1
|
||||
if entries < 12:
|
||||
errors.append("checksum inventory is unexpectedly small")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_repository(root: Path) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if git(root, "branch", "--show-current") != BRANCH:
|
||||
errors.append("current branch mismatch")
|
||||
ancestors = git(root, "merge-base", "--is-ancestor", BASELINE, "HEAD")
|
||||
if ancestors:
|
||||
errors.append("unexpected merge-base output")
|
||||
for relative, expected in IMMUTABLE_HASHES.items():
|
||||
path = root / relative
|
||||
if not path.is_file() or sha256_file(path) != expected:
|
||||
errors.append(f"immutable evidence changed: {relative}")
|
||||
denylist = root / "manifests/artifact-denylist.json"
|
||||
if sha256_file(denylist) != DENYLIST_SHA256:
|
||||
errors.append("permanent artifact denylist changed")
|
||||
for relative in DELIVERABLES:
|
||||
if not (root / relative).is_file():
|
||||
errors.append(f"deliverable missing: {relative}")
|
||||
tracked = git(root, "ls-files").splitlines()
|
||||
for relative in tracked:
|
||||
path = root / relative
|
||||
if path.is_file() and path.stat().st_size > MAX_TRACKED_FILE_SIZE:
|
||||
errors.append(f"large tracked file: {relative}")
|
||||
suffix = Path(relative).suffix.lower()
|
||||
if relative.startswith("packaging/phase09er2/") and suffix in {
|
||||
".zip",
|
||||
".rar",
|
||||
".7z",
|
||||
".dat",
|
||||
}:
|
||||
errors.append(f"backup material tracked: {relative}")
|
||||
changed = git(
|
||||
root, "diff", "--name-only", "--diff-filter=ACMR", f"{BASELINE}...HEAD"
|
||||
).splitlines()
|
||||
changed.extend(git(root, "diff", "--name-only", "--diff-filter=ACMR").splitlines())
|
||||
untracked = git(root, "ls-files", "--others", "--exclude-standard").splitlines()
|
||||
changed.extend(untracked)
|
||||
for relative in sorted(set(filter(None, changed))):
|
||||
normalized = relative.replace("\\", "/")
|
||||
suffix = Path(normalized).suffix.lower()
|
||||
if normalized.startswith(FORBIDDEN_PREFIXES) or suffix in FORBIDDEN_SUFFIXES:
|
||||
errors.append(f"target/binary source forbidden in phase: {normalized}")
|
||||
path = root / relative
|
||||
if path.is_file() and path.stat().st_size <= 2 * 1024 * 1024:
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
errors.append(f"unexpected binary tracked or staged: {normalized}")
|
||||
continue
|
||||
if re.search(r"download2434\.mediafire\.com/[^<\s]+", text):
|
||||
errors.append(f"unredacted signed MediaFire URL: {normalized}")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_local_candidate(root: Path) -> tuple[Path, list[str]]:
|
||||
errors: list[str] = []
|
||||
path = candidate_archive_path(root)
|
||||
if not path.is_file():
|
||||
return path, [f"local candidate missing: {path}"]
|
||||
if path.stat().st_size != OUTER_SIZE or sha256_file(path) != OUTER_SHA256:
|
||||
errors.append("local outer candidate identity changed")
|
||||
return path, errors
|
||||
try:
|
||||
size, digest = sha256_zip_entry(path, INNER_NAME)
|
||||
except (KeyError, OSError, zipfile.BadZipFile) as error:
|
||||
errors.append(f"local inner candidate cannot be read: {error}")
|
||||
else:
|
||||
if size != INNER_SIZE or digest != INNER_SHA256:
|
||||
errors.append("local inner candidate identity changed")
|
||||
return path, errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
root = args.root.resolve()
|
||||
errors: list[str] = []
|
||||
local_path, candidate_errors = validate_local_candidate(root)
|
||||
errors.extend(candidate_errors)
|
||||
errors.extend(validate_repository(root))
|
||||
main_manifest = load_json(
|
||||
root / "manifests/runtime/phase-0.9e-r2-inner-correlation.json"
|
||||
)
|
||||
fingerprints = load_json(
|
||||
root / "manifests/runtime/phase-0.9e-r2-siecaf-fingerprints.json"
|
||||
)
|
||||
errors.extend(validate_manifest(main_manifest))
|
||||
if local_path.is_file():
|
||||
inspector = load_module(
|
||||
"phase09er2_siecaf_inspector",
|
||||
root / "tools/inspect_siecaf_header.py",
|
||||
)
|
||||
errors.extend(validate_fingerprints(root, fingerprints, inspector, local_path))
|
||||
errors.extend(validate_checksums(root))
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"ERROR: {error}")
|
||||
return 1
|
||||
print(
|
||||
"Phase-0.9E-R2 validation PASS: "
|
||||
"LOCAL_BACKUP_UNCORRELATED; no device action authorized"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,621 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate the host-only Phase-0.9E-R official Y2JB correlation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
from typing import Any
|
||||
import zipfile
|
||||
|
||||
|
||||
BASELINE = "3d3151bd12ff0f786c1e1b9af75d7174408e3d2f"
|
||||
BRANCH = "codex/chimera-gfx-phase09e-r-y2jb-correlation"
|
||||
PHASE = "PHASE_0_9E_R_OFFICIAL_Y2JB_CORRELATION"
|
||||
OUTER_NAME = "Y2JB-Autoloader-403-1240.zip"
|
||||
OUTER_SIZE = 504159435
|
||||
OUTER_SHA256 = "805e3f87f0c371223619ffc7d3a7b3c0d41a1fae8a8b1171d9e2f162659e8291"
|
||||
INNER_NAME = "PS5/EXPORT/BACKUP/202606102126_00/archive.dat"
|
||||
INNER_SIZE = 504365056
|
||||
INNER_SHA256 = "6439834e8856d45b6d6fe699b74c35ca6985a199ea8ecf3e398c018d37be2d55"
|
||||
DENYLIST_SHA256 = "e9603b0e3792781ad5b511afb22ef61e3d4fd4c5a16bf928f8609193bcd97783"
|
||||
UPSTREAM_COMMIT = "0dbbf4e7e0203af7e5d101a3256c634edf4e3ba2"
|
||||
UPSTREAM_TREE = "c4344f43af7c268337437e6419548dba6f6bc211"
|
||||
UPSTREAM_REMOTE = "https://github.com/Gezine/Y2JB.git"
|
||||
PORT_9020_CLASSIFICATION = "PORT_9020_REFERENCE_ONLY"
|
||||
FINAL_CLASSIFICATION = "LOCAL_BACKUP_NOT_CORRELATED"
|
||||
PROVENANCE_FIELDS = (
|
||||
"logical_name",
|
||||
"artifact_role",
|
||||
"local_relative_path",
|
||||
"size",
|
||||
"sha256",
|
||||
"file_type",
|
||||
"source_repository",
|
||||
"source_commit",
|
||||
"build_identity",
|
||||
"version",
|
||||
"obtained_from",
|
||||
"evidence_that_it_is_deployed_or_used",
|
||||
"confidence",
|
||||
"immutable",
|
||||
"executable",
|
||||
"persistent_on_device",
|
||||
"transferred_per_session",
|
||||
"required_for_bootstrap",
|
||||
"required_for_recovery",
|
||||
)
|
||||
|
||||
AUTHORIZATION_FIELDS = (
|
||||
"device_action_authorized",
|
||||
"target_build_authorized",
|
||||
"transfer_authorized",
|
||||
"execution_authorized",
|
||||
"installation_authorized",
|
||||
"lifecycle_authorized",
|
||||
"autoload_authorized",
|
||||
"device_write_authorized",
|
||||
"automatic_retry",
|
||||
)
|
||||
IMMUTABLE_HASHES = {
|
||||
"manifests/runtime/phase-0.8-read-only-preflight.json":
|
||||
"47d7f452f8799979fe99b3e6d56859f03544112725bf7e5b349eba5ed81b3322",
|
||||
"manifests/runtime/phase-0.8-remediation.json":
|
||||
"a9dafed8c83722c43709dcf90ea117c21d6f996ff8fe233810ed08aa20cbe071",
|
||||
"manifests/runtime/phase-0.9-anti-brick-design.json":
|
||||
"39fd7c70cae998d9d74e7caf1ff3c19f9f76f7de2e5a50bc09baede4ba53e9e9",
|
||||
"manifests/runtime/phase-0.9b-observer.json":
|
||||
"104c4a667ad17f9827fc7276852c6faeef96effb8e3561a5ebc19a62c7d51634",
|
||||
"manifests/runtime/phase-0.9c-feasibility.json":
|
||||
"84eb737ff3486d0c5d8b5ecf06809f93bc573497da4919595b4c1817fd34247c",
|
||||
"manifests/runtime/phase-0.9d-existing-stack-readback.json":
|
||||
"86e5aaf034685dbe058b71ffeec645b682f0a8cc7d249e8ac0397155233991de",
|
||||
"manifests/runtime/phase-0.9e-bootstrap-provenance.json":
|
||||
"5dfa9bfe2ae751b2f0ea0e03c60c1a4471a35452389cf471456e6c54eb4601cf",
|
||||
"manifests/runtime/phase-0.9e-loader-protocol.json":
|
||||
"a7ca8b4e8072cb60ad4cd4869f6c508e8014059c8d81ad3b94a8d9e8cd0db4cb",
|
||||
}
|
||||
UPSTREAM_FILE_HASHES = {
|
||||
"README.md": (7500, "16bfdaa624b8b04f4a6a4a7d512ca8473ad2db73e80df39a974267403e34f751"),
|
||||
"payload_sender.py": (1064, "8c87920c41dbdbd66b9f36ca9509f0d6bef9170f351dd97ff831cfb98e642ec6"),
|
||||
"log_server.py": (929, "463114fd46479a7286706de13beb3f52221f36f3cbe37dc5a27bdb6104787a98"),
|
||||
"appinfo_editor.py": (2008, "c1bcb453660f597cbc9026dba76519a4929fc3e967c183deec8a1ca56912e2e8"),
|
||||
"download0/cache/splash_screen/aHR0cHM6Ly93d3cueW91dHViZS5jb20vdHY=/remotejsloader.js":
|
||||
(7132, "30cc6d1535549b2a49b47a9e0c85a3444cf84177be54398691694b7c6505f38e"),
|
||||
"download0/cache/splash_screen/aHR0cHM6Ly93d3cueW91dHViZS5jb20vdHY=/elfldr-ps5-1340.elf":
|
||||
(397000, "30478bcadb6439e1247451c4ac706b6e1385044dd0f12d6486b6d7057929453b"),
|
||||
}
|
||||
DELIVERABLES = (
|
||||
"docs/runtime/phase-0.9e-r-official-release-correlation.md",
|
||||
"docs/runtime/phase-0.9e-r-release-source-binding.md",
|
||||
"docs/runtime/phase-0.9e-r-port9020-source-audit.md",
|
||||
"docs/runtime/phase-0.9e-r-official-hostsender-audit.md",
|
||||
"docs/runtime/phase-0.9e-r-provenance-gaps.md",
|
||||
"docs/approvals/phase-0.9e-r-y2jb-deployed-use-attestation.md",
|
||||
"manifests/runtime/phase-0.9e-r-release-correlation.json",
|
||||
"manifests/runtime/phase-0.9e-r-release-correlation.schema.json",
|
||||
"manifests/runtime/phase-0.9e-r-port9020-audit.json",
|
||||
"manifests/runtime/phase-0.9e-r-port9020-audit.schema.json",
|
||||
"tools/validate_phase09er_provenance.py",
|
||||
"tests/test_phase09er_provenance.py",
|
||||
"packaging/phase09er/SHA256SUMS.txt",
|
||||
)
|
||||
FORBIDDEN_SUFFIXES = {
|
||||
".c", ".cc", ".cpp", ".s", ".asm", ".ld", ".elf", ".self", ".sprx",
|
||||
".pkg", ".bin", ".wasm",
|
||||
}
|
||||
FORBIDDEN_PREFIXES = ("include/", "src/", "adapters/", "samples/")
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
value = json.load(stream)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{path} does not contain a JSON object")
|
||||
return value
|
||||
|
||||
|
||||
def sha256_file(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 git(root: Path, *args: str) -> str:
|
||||
result = subprocess.run(
|
||||
["git", *args],
|
||||
cwd=root,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(result.stderr.strip() or "git command failed")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def candidate_archive_path(root: Path) -> Path:
|
||||
direct = Path.home() / "Downloads" / OUTER_NAME
|
||||
if direct.is_file():
|
||||
return direct
|
||||
for ancestor in (root, *root.parents):
|
||||
if ancestor.parent.name.lower() == "users":
|
||||
return ancestor / "Downloads" / OUTER_NAME
|
||||
return direct
|
||||
|
||||
|
||||
def official_byte_match(
|
||||
*,
|
||||
official_source: bool,
|
||||
local_name: str,
|
||||
local_size: int,
|
||||
local_sha256: str,
|
||||
asset_name: str,
|
||||
asset_size: int,
|
||||
asset_sha256: str,
|
||||
) -> bool:
|
||||
"""Only exact official name, byte count, and hash establish a match."""
|
||||
return (
|
||||
official_source
|
||||
and local_name == asset_name
|
||||
and local_size == asset_size
|
||||
and local_sha256 == asset_sha256
|
||||
)
|
||||
|
||||
|
||||
def source_binding(
|
||||
*, release_associated: bool, inner_bytes_matched: bool, inner_opaque: bool
|
||||
) -> str:
|
||||
if inner_bytes_matched:
|
||||
return "REPRODUCIBLE_CONTENT_BINDING"
|
||||
if release_associated and inner_opaque:
|
||||
return "SOURCE_ONLY_ASSOCIATION"
|
||||
if release_associated:
|
||||
return "OFFICIAL_RELEASE_ASSOCIATION"
|
||||
return "NO_LOCAL_RELEASE_ASSOCIATION"
|
||||
|
||||
|
||||
def port_implementation_sufficient(classification: str) -> bool:
|
||||
return classification in {
|
||||
"PORT_9020_IMPLEMENTATION_FOUND",
|
||||
"PORT_9020_IMPLEMENTATION_PARTIAL",
|
||||
}
|
||||
|
||||
|
||||
def phase09f_design_allowed(
|
||||
*,
|
||||
correlation: str,
|
||||
release_commit_known: bool,
|
||||
upstream_clean: bool,
|
||||
port_classification: str,
|
||||
sender_identified: bool,
|
||||
attestation_available: bool,
|
||||
all_authorizations_false: bool,
|
||||
) -> bool:
|
||||
return all(
|
||||
(
|
||||
correlation == "OFFICIAL_RELEASE_BYTE_MATCH",
|
||||
release_commit_known,
|
||||
upstream_clean,
|
||||
port_implementation_sufficient(port_classification),
|
||||
sender_identified,
|
||||
attestation_available,
|
||||
all_authorizations_false,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def sender_duplex(*, sends: bool, receives: bool) -> bool:
|
||||
return sends and receives
|
||||
|
||||
|
||||
def sender_short_send_deficiency(*, uses_sendall: bool, explicit_send_loop: bool) -> bool:
|
||||
return not (uses_sendall or explicit_send_loop)
|
||||
|
||||
|
||||
def attestation_is_runtime_proof(*, attested: bool, hardware_observed: bool) -> bool:
|
||||
return attested and hardware_observed
|
||||
|
||||
|
||||
def _type_matches(value: Any, expected: str | list[str]) -> bool:
|
||||
if isinstance(expected, list):
|
||||
return any(_type_matches(value, item) for item in expected)
|
||||
checks = {
|
||||
"object": isinstance(value, dict),
|
||||
"array": isinstance(value, list),
|
||||
"string": isinstance(value, str),
|
||||
"boolean": isinstance(value, bool),
|
||||
"integer": isinstance(value, int) and not isinstance(value, bool),
|
||||
"null": value is None,
|
||||
}
|
||||
return checks.get(expected, True)
|
||||
|
||||
|
||||
def validate_schema_instance(
|
||||
schema: dict[str, Any], instance: Any, path: str = "$"
|
||||
) -> list[str]:
|
||||
errors: list[str] = []
|
||||
expected_type = schema.get("type")
|
||||
if expected_type is not None and not _type_matches(instance, expected_type):
|
||||
return [f"{path}: type mismatch"]
|
||||
if "const" in schema and instance != schema["const"]:
|
||||
errors.append(f"{path}: const mismatch")
|
||||
if "enum" in schema and instance not in schema["enum"]:
|
||||
errors.append(f"{path}: outside enum")
|
||||
if isinstance(instance, dict):
|
||||
for required in schema.get("required", []):
|
||||
if required not in instance:
|
||||
errors.append(f"{path}: missing {required}")
|
||||
properties = schema.get("properties", {})
|
||||
for key, value in instance.items():
|
||||
if key in properties:
|
||||
errors.extend(
|
||||
validate_schema_instance(properties[key], value, f"{path}.{key}")
|
||||
)
|
||||
elif isinstance(schema.get("additionalProperties"), dict):
|
||||
errors.extend(
|
||||
validate_schema_instance(
|
||||
schema["additionalProperties"], value, f"{path}.{key}"
|
||||
)
|
||||
)
|
||||
if isinstance(instance, list) and len(instance) < schema.get("minItems", 0):
|
||||
errors.append(f"{path}: too few items")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_release_manifest(manifest: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
expected = {
|
||||
"phase": PHASE,
|
||||
"status": "DESIGN_ONLY",
|
||||
"baseline_commit": BASELINE,
|
||||
"release_correlation": "OFFICIAL_RELEASE_NO_MATCH",
|
||||
"final_classification": FINAL_CLASSIFICATION,
|
||||
"identified_release": None,
|
||||
"identified_release_asset": None,
|
||||
"inner_content_binding": "SIECAF_OPAQUE_UNBOUND",
|
||||
"phase09f_offline_design_allowed": False,
|
||||
}
|
||||
for field, value in expected.items():
|
||||
if manifest.get(field) != value:
|
||||
errors.append(f"{field} changed")
|
||||
for field in AUTHORIZATION_FIELDS:
|
||||
if manifest.get("authorization", {}).get(field) is not False:
|
||||
errors.append(f"authorization.{field} must be false")
|
||||
local = manifest.get("local_candidate", {})
|
||||
if (
|
||||
local.get("file_name") != OUTER_NAME
|
||||
or local.get("size") != OUTER_SIZE
|
||||
or local.get("sha256") != OUTER_SHA256
|
||||
):
|
||||
errors.append("local candidate identity changed")
|
||||
inner = manifest.get("inner_archive", {})
|
||||
if (
|
||||
inner.get("size") != INNER_SIZE
|
||||
or inner.get("sha256") != INNER_SHA256
|
||||
or inner.get("classification") != "OPAQUE_UNBOUND"
|
||||
or inner.get("further_reverse_engineering_performed") is not False
|
||||
):
|
||||
errors.append("inner SIECAF boundary changed")
|
||||
assets = manifest.get("assets", [])
|
||||
if len(assets) != 7:
|
||||
errors.append("official asset inventory must contain seven assets")
|
||||
for asset in assets:
|
||||
if not re.fullmatch(r"[0-9a-f]{64}", str(asset.get("sha256", ""))):
|
||||
errors.append(f"official asset digest malformed: {asset.get('name')}")
|
||||
if asset.get("classification") != "NO_MATCH" or asset.get("downloaded"):
|
||||
errors.append(f"asset was not metadata-excluded: {asset.get('name')}")
|
||||
if official_byte_match(
|
||||
official_source=True,
|
||||
local_name=OUTER_NAME,
|
||||
local_size=OUTER_SIZE,
|
||||
local_sha256=OUTER_SHA256,
|
||||
asset_name=str(asset.get("name")),
|
||||
asset_size=int(asset.get("size", -1)),
|
||||
asset_sha256=str(asset.get("sha256")),
|
||||
):
|
||||
errors.append(f"unrecorded official byte match: {asset.get('name')}")
|
||||
tags = manifest.get("tags", [])
|
||||
if len(tags) != 5:
|
||||
errors.append("official tag inventory must contain five tags")
|
||||
for tag in tags:
|
||||
if not re.fullmatch(r"[0-9a-f]{40}", str(tag.get("commit", ""))):
|
||||
errors.append(f"tag commit is not exact: {tag.get('tag')}")
|
||||
if not re.fullmatch(r"[0-9a-f]{64}", str(tag.get("source_archive_sha256", ""))):
|
||||
errors.append(f"source archive hash missing: {tag.get('tag')}")
|
||||
if len(manifest.get("release_inventory", [])) != 6:
|
||||
errors.append("release inventory must cover versions 1.2 through 1.6")
|
||||
provenance = manifest.get("artifact_provenance", [])
|
||||
if len(provenance) < 6:
|
||||
errors.append("artifact provenance inventory is incomplete")
|
||||
for artifact in provenance:
|
||||
missing = [field for field in PROVENANCE_FIELDS if field not in artifact]
|
||||
if missing:
|
||||
errors.append(
|
||||
f"{artifact.get('logical_name')}: missing provenance fields {missing}"
|
||||
)
|
||||
if artifact.get("confidence") == "EXACT_USED":
|
||||
errors.append(
|
||||
f"{artifact.get('logical_name')}: deployed-use provenance overclaimed"
|
||||
)
|
||||
if artifact.get("logical_name") == "official_embedded_elfldr_1_6":
|
||||
if artifact.get("source_commit") is not None:
|
||||
errors.append("embedded elfldr received an invented source commit")
|
||||
worktree = manifest.get("upstream_worktree", {})
|
||||
if (
|
||||
worktree.get("commit") != UPSTREAM_COMMIT
|
||||
or worktree.get("tree") != UPSTREAM_TREE
|
||||
or worktree.get("clean") is not True
|
||||
or worktree.get("submodules") != 0
|
||||
or worktree.get("git_lfs_pointers") != 0
|
||||
):
|
||||
errors.append("official upstream worktree record changed")
|
||||
sender = manifest.get("official_host_sender", {})
|
||||
if (
|
||||
sender.get("response_read") is not False
|
||||
or sender.get("duplex") is not False
|
||||
or sender.get("deployed_use_attested") is not False
|
||||
):
|
||||
errors.append("official sender was overclaimed")
|
||||
if manifest.get("operator_attestation", {}).get("attested") is not False:
|
||||
errors.append("operator attestation must remain empty")
|
||||
actions = manifest.get("actions", {})
|
||||
expected_false_actions = (
|
||||
"downloaded_code_executed",
|
||||
"dependency_installed",
|
||||
"ps5_connected",
|
||||
"ps5_ip_used",
|
||||
"device_request_performed",
|
||||
"files_transferred_to_or_from_ps5",
|
||||
"target_build_performed",
|
||||
"target_code_created",
|
||||
"target_artifact_created",
|
||||
"payload_created",
|
||||
"device_client_created",
|
||||
)
|
||||
for field in expected_false_actions:
|
||||
if actions.get(field) is not False:
|
||||
errors.append(f"actions.{field} must be false")
|
||||
if (
|
||||
actions.get("large_release_assets_downloaded") != 0
|
||||
or actions.get("large_release_asset_bytes_downloaded") != 0
|
||||
):
|
||||
errors.append("large release asset download was recorded")
|
||||
if manifest.get("blocking_state", {}).get("payload_manager_backup") != (
|
||||
"HARD_BLOCKER_FOR_INSTALLATION"
|
||||
):
|
||||
errors.append("Payload Manager installation blocker changed")
|
||||
if phase09f_design_allowed(
|
||||
correlation=str(manifest.get("release_correlation")),
|
||||
release_commit_known=False,
|
||||
upstream_clean=True,
|
||||
port_classification=PORT_9020_CLASSIFICATION,
|
||||
sender_identified=True,
|
||||
attestation_available=True,
|
||||
all_authorizations_false=True,
|
||||
):
|
||||
errors.append("Phase 0.9F incorrectly passed")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_port_manifest(manifest: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if manifest.get("phase") != PHASE or manifest.get("status") != "DESIGN_ONLY":
|
||||
errors.append("port manifest phase/status changed")
|
||||
if manifest.get("release_commit") != UPSTREAM_COMMIT:
|
||||
errors.append("port manifest release commit changed")
|
||||
if manifest.get("port_9020_classification") != PORT_9020_CLASSIFICATION:
|
||||
errors.append("port-9020 implementation was overclaimed")
|
||||
if (
|
||||
manifest.get("port_9020_listener_found") is not False
|
||||
or manifest.get("port_9020_parser_found") is not False
|
||||
or manifest.get("port_9020_mapping_found") is not False
|
||||
):
|
||||
errors.append("port-9020 source was invented")
|
||||
loader = manifest.get("official_remote_js_loader", {})
|
||||
if (
|
||||
loader.get("desired_dynamic_port") != 50000
|
||||
or loader.get("maximum_receive_bytes") != 512000
|
||||
or loader.get("framing") != "CONNECTION_EOF_OR_BUFFER_LIMIT"
|
||||
or loader.get("declared_length") is not False
|
||||
or loader.get("timeout") is not False
|
||||
or loader.get("native_elf_mapping") is not False
|
||||
):
|
||||
errors.append("Remote JS Loader contract changed")
|
||||
if loader.get("automatic_retry_authorized") is not False:
|
||||
errors.append("automatic retry was authorized")
|
||||
relation = manifest.get("port_9021_relation", {})
|
||||
if relation.get("listener_source_in_y2jb") is not False:
|
||||
errors.append("embedded 9021 listener source was invented")
|
||||
if relation.get("source_commit") is not None:
|
||||
errors.append("embedded elfldr received an invented source commit")
|
||||
sender = manifest.get("official_sender", {})
|
||||
if sender.get("response_read") is not False or sender.get("duplex") is not False:
|
||||
errors.append("one-way sender was classified duplex")
|
||||
for field in AUTHORIZATION_FIELDS:
|
||||
if manifest.get("authorization", {}).get(field) is not False:
|
||||
errors.append(f"port authorization.{field} must be false")
|
||||
if manifest.get("phase09f_offline_design_allowed") is not False:
|
||||
errors.append("Phase 0.9F was authorized")
|
||||
return errors
|
||||
|
||||
|
||||
def changed_paths(root: Path) -> set[str]:
|
||||
paths = set(git(root, "diff", "--name-only", BASELINE).splitlines())
|
||||
for line in git(root, "status", "--porcelain=v1", "--untracked-files=all").splitlines():
|
||||
path = line[3:]
|
||||
if " -> " in path:
|
||||
path = path.split(" -> ", 1)[1]
|
||||
paths.add(path.replace("\\", "/"))
|
||||
return {path for path in paths if path}
|
||||
|
||||
|
||||
def phase09er_path_errors(paths: set[str]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
for path in paths:
|
||||
normalized = path.replace("\\", "/")
|
||||
lowered = normalized.lower()
|
||||
if normalized.startswith(FORBIDDEN_PREFIXES):
|
||||
errors.append(f"target/production path changed: {normalized}")
|
||||
if Path(normalized).suffix.lower() in FORBIDDEN_SUFFIXES:
|
||||
errors.append(f"target artifact/source added: {normalized}")
|
||||
if any(
|
||||
token in lowered
|
||||
for token in (
|
||||
"rescue.elf", "observer.elf", "readback.elf", "executionpackage",
|
||||
"installpackage", "transferpackage",
|
||||
)
|
||||
):
|
||||
errors.append(f"forbidden Phase-0.9E-R output: {normalized}")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_checksum_file(root: Path) -> list[str]:
|
||||
checksum_path = root / "packaging/phase09er/SHA256SUMS.txt"
|
||||
if not checksum_path.is_file():
|
||||
return ["missing packaging/phase09er/SHA256SUMS.txt"]
|
||||
errors: list[str] = []
|
||||
for line in checksum_path.read_text(encoding="utf-8").splitlines():
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
parts = line.split(" ", 1)
|
||||
if len(parts) != 2 or not re.fullmatch(r"[0-9a-f]{64}", parts[0]):
|
||||
errors.append(f"invalid checksum line: {line}")
|
||||
continue
|
||||
expected, relative = parts
|
||||
path = root / relative
|
||||
if not path.is_file() or sha256_file(path) != expected:
|
||||
errors.append(f"checksum mismatch or missing: {relative}")
|
||||
return errors
|
||||
|
||||
|
||||
def collect_errors(root: Path) -> list[str]:
|
||||
errors: list[str] = []
|
||||
release_path = root / "manifests/runtime/phase-0.9e-r-release-correlation.json"
|
||||
port_path = root / "manifests/runtime/phase-0.9e-r-port9020-audit.json"
|
||||
release_schema_path = (
|
||||
root / "manifests/runtime/phase-0.9e-r-release-correlation.schema.json"
|
||||
)
|
||||
port_schema_path = (
|
||||
root / "manifests/runtime/phase-0.9e-r-port9020-audit.schema.json"
|
||||
)
|
||||
try:
|
||||
release = load_json(release_path)
|
||||
port = load_json(port_path)
|
||||
release_schema = load_json(release_schema_path)
|
||||
port_schema = load_json(port_schema_path)
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
return [str(error)]
|
||||
errors.extend(validate_schema_instance(release_schema, release))
|
||||
errors.extend(validate_schema_instance(port_schema, port))
|
||||
errors.extend(validate_release_manifest(release))
|
||||
errors.extend(validate_port_manifest(port))
|
||||
|
||||
for relative in DELIVERABLES:
|
||||
if not (root / relative).is_file():
|
||||
errors.append(f"missing deliverable: {relative}")
|
||||
if git(root, "rev-parse", "--abbrev-ref", "HEAD") != BRANCH:
|
||||
errors.append("current branch is not the Phase-0.9E-R branch")
|
||||
if git(root, "merge-base", BASELINE, "HEAD") != BASELINE:
|
||||
errors.append("branch no longer descends from the canonical baseline")
|
||||
if sha256_file(root / "manifests/artifact-denylist.json") != DENYLIST_SHA256:
|
||||
errors.append("permanent artifact denylist changed")
|
||||
for relative, expected in IMMUTABLE_HASHES.items():
|
||||
path = root / relative
|
||||
if not path.is_file() or sha256_file(path) != expected:
|
||||
errors.append(f"immutable evidence changed: {relative}")
|
||||
|
||||
outer = candidate_archive_path(root)
|
||||
if not outer.is_file():
|
||||
errors.append(f"local Y2JB candidate missing: {outer}")
|
||||
elif outer.stat().st_size != OUTER_SIZE or sha256_file(outer) != OUTER_SHA256:
|
||||
errors.append("local Y2JB outer identity changed")
|
||||
else:
|
||||
try:
|
||||
with zipfile.ZipFile(outer) as archive:
|
||||
info = archive.getinfo(INNER_NAME)
|
||||
if (
|
||||
len(archive.infolist()) != 5
|
||||
or archive.comment != b""
|
||||
or info.file_size != INNER_SIZE
|
||||
or info.compress_size != 504158629
|
||||
or info.CRC != int("522808c8", 16)
|
||||
):
|
||||
errors.append("local ZIP metadata changed")
|
||||
digest = hashlib.sha256()
|
||||
magic = b""
|
||||
with archive.open(info) as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
if not magic:
|
||||
magic = chunk[:6]
|
||||
digest.update(chunk)
|
||||
if magic != b"SIECAF" or digest.hexdigest() != INNER_SHA256:
|
||||
errors.append("inner SIECAF identity changed")
|
||||
except (OSError, KeyError, zipfile.BadZipFile) as error:
|
||||
errors.append(f"local ZIP validation failed: {error}")
|
||||
|
||||
upstream = root / "work/upstream/Y2JB-official"
|
||||
if not upstream.is_dir():
|
||||
errors.append("official immutable upstream worktree missing")
|
||||
else:
|
||||
if git(upstream, "rev-parse", "HEAD") != UPSTREAM_COMMIT:
|
||||
errors.append("official upstream HEAD changed")
|
||||
if git(upstream, "rev-parse", "HEAD^{tree}") != UPSTREAM_TREE:
|
||||
errors.append("official upstream tree changed")
|
||||
if git(upstream, "status", "--porcelain=v1"):
|
||||
errors.append("official upstream worktree is dirty")
|
||||
remotes = git(upstream, "remote", "-v")
|
||||
if UPSTREAM_REMOTE not in remotes:
|
||||
errors.append("official upstream remote changed")
|
||||
if git(upstream, "submodule", "status"):
|
||||
errors.append("unexpected official upstream submodule")
|
||||
if len(git(upstream, "ls-files").splitlines()) != 19:
|
||||
errors.append("official upstream tracked file inventory changed")
|
||||
for relative, (expected_size, expected_hash) in UPSTREAM_FILE_HASHES.items():
|
||||
path = upstream / relative
|
||||
if (
|
||||
not path.is_file()
|
||||
or path.stat().st_size != expected_size
|
||||
or sha256_file(path) != expected_hash
|
||||
):
|
||||
errors.append(f"official upstream file identity changed: {relative}")
|
||||
|
||||
errors.extend(phase09er_path_errors(changed_paths(root)))
|
||||
for path in root.rglob("*"):
|
||||
if (
|
||||
path.is_file()
|
||||
and ".git" not in path.parts
|
||||
and "work" not in path.parts
|
||||
and path.stat().st_size > 50 * 1024 * 1024
|
||||
):
|
||||
errors.append(f"large release-like object present in repository: {path}")
|
||||
errors.extend(validate_checksum_file(root))
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
errors = collect_errors(args.root.resolve())
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"ERROR: {error}")
|
||||
return 1
|
||||
print("Phase-0.9E-R official Y2JB provenance validation: PASS")
|
||||
print(f"Release correlation: OFFICIAL_RELEASE_NO_MATCH")
|
||||
print(f"Port 9020: {PORT_9020_CLASSIFICATION}")
|
||||
print(f"Decision: {FINAL_CLASSIFICATION}")
|
||||
print("Device actions: NONE")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,326 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate the offline Phase-1.0A RetroArch PS5 port evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
PHASE = "PHASE_1_0A_RETROARCH_PS5_PORT_BOOTSTRAP"
|
||||
STATUS = "RETROARCH_PS5_SOFTWARE_PORT_BUILT"
|
||||
BASELINE = "4c086f84ab2e7c53e6750b66b3b3e1f591ff6a06"
|
||||
BRANCH = "codex/chimera-gfx-phase10a-retroarch-port-bootstrap"
|
||||
RETROARCH_BRANCH = "codex/ps5-port-bootstrap"
|
||||
RETROARCH_TAG = "v1.22.2"
|
||||
RETROARCH_COMMIT = "69a4f0ea1e8aaf442ae4858f2e7f2b31a1776576"
|
||||
RETROARCH_TREE = "33babf9eb7699b5d571a3063ea21c3e488c159fe"
|
||||
RETROARCH_ARCHIVE_SHA256 = (
|
||||
"245ef18c8fa8fbd9fbb5eb25cf43e17c6aace2f95c1ed99873cbd794012bb232"
|
||||
)
|
||||
SDK_COMMIT = "d2e2e585740362976a39fdd5ccf390f199a7bc37"
|
||||
SDL_COMMIT = "0baf4ac49382b537ba449901b5b6d0d189bb1fbb"
|
||||
PACBREW_COMMIT = "c2abcfcb60f569128abd0e8e70ad03a67bee5ea7"
|
||||
DENYLIST_SHA256 = "e9603b0e3792781ad5b511afb22ef61e3d4fd4c5a16bf928f8609193bcd97783"
|
||||
|
||||
OFFLINE_AUTHORIZATION_FIELDS = (
|
||||
"offline_source_acquisition_authorized",
|
||||
"offline_source_modification_authorized",
|
||||
"offline_target_build_authorized",
|
||||
"offline_artifact_analysis_authorized",
|
||||
"private_gitea_push_authorized",
|
||||
)
|
||||
DEVICE_AUTHORIZATION_FIELDS = (
|
||||
"ps5_connection_authorized",
|
||||
"device_transfer_authorized",
|
||||
"device_execution_authorized",
|
||||
"installation_authorized",
|
||||
"lifecycle_authorized",
|
||||
"autoload_authorized",
|
||||
"device_write_authorized",
|
||||
"automatic_retry",
|
||||
)
|
||||
DELIVERABLES = (
|
||||
"docs/retroarch/phase-1.0a-port-plan.md",
|
||||
"docs/retroarch/phase-1.0a-upstream-analysis.md",
|
||||
"docs/retroarch/phase-1.0a-ps4-reference-delta.md",
|
||||
"docs/retroarch/phase-1.0a-pacbrew-sdl-analysis.md",
|
||||
"docs/retroarch/phase-1.0a-build-results.md",
|
||||
"docs/retroarch/phase-1.0a-driver-status.md",
|
||||
"docs/retroarch/phase-1.0a-next-device-smoke-test.md",
|
||||
"manifests/retroarch/upstreams.json",
|
||||
"manifests/retroarch/phase-1.0a-build.json",
|
||||
"manifests/retroarch/phase-1.0a-artifacts.json",
|
||||
"tests/test_retroarch_phase10a.py",
|
||||
"tools/validate_retroarch_phase10a.py",
|
||||
"packaging/retroarch/phase10a/SHA256SUMS.txt",
|
||||
)
|
||||
REQUIRED_REAL_SYMBOLS = {
|
||||
"rarch_main",
|
||||
"retroarch_main_init",
|
||||
"runloop_iterate",
|
||||
"retro_init",
|
||||
"retro_deinit",
|
||||
"retro_run",
|
||||
"frontend_ctx_ps5",
|
||||
}
|
||||
DISABLED_FEATURES = {
|
||||
"networking",
|
||||
"online_updater",
|
||||
"dynamic_cores",
|
||||
"autoload",
|
||||
"installation",
|
||||
"gnm",
|
||||
"opengl",
|
||||
"vulkan",
|
||||
}
|
||||
FORBIDDEN_MARKERS = (
|
||||
"chimera_lifecycle_probe",
|
||||
"prospero-deploy",
|
||||
"payload_sender",
|
||||
"sceSystemServiceLoadExec",
|
||||
)
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
value = json.load(stream)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{path} is not a JSON object")
|
||||
return value
|
||||
|
||||
|
||||
def sha256_file(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 artifact_execution_allowed(record: dict[str, Any]) -> bool:
|
||||
return bool(record.get("execution_eligible", False))
|
||||
|
||||
|
||||
def route_is_forbidden(text: str) -> bool:
|
||||
lowered = text.lower()
|
||||
return any(marker.lower() in lowered for marker in FORBIDDEN_MARKERS)
|
||||
|
||||
|
||||
def profile_is_closed(profile: dict[str, Any]) -> bool:
|
||||
disabled = set(profile.get("disabled_features", []))
|
||||
return (
|
||||
DISABLED_FEATURES.issubset(disabled)
|
||||
and profile.get("automatic_retry") is False
|
||||
and profile.get("filesystem_writes_on_default_path") is False
|
||||
)
|
||||
|
||||
|
||||
def is_real_retroarch_artifact(record: dict[str, Any]) -> bool:
|
||||
return REQUIRED_REAL_SYMBOLS.issubset(set(record.get("required_symbols", [])))
|
||||
|
||||
|
||||
def has_personal_path(strings: list[str]) -> bool:
|
||||
patterns = (
|
||||
re.compile(r"[A-Za-z]:\\Users\\", re.IGNORECASE),
|
||||
re.compile(r"/(?:mnt/[a-z]/)?Users/[^/]+/", re.IGNORECASE),
|
||||
re.compile(r"/home/[^/]+/", re.IGNORECASE),
|
||||
)
|
||||
return any(pattern.search(value) for value in strings for pattern in patterns)
|
||||
|
||||
|
||||
def has_ip_address(strings: list[str]) -> bool:
|
||||
pattern = re.compile(r"(?<!\d)(?:\d{1,3}\.){3}\d{1,3}(?!\d)")
|
||||
return any(pattern.search(value) for value in strings)
|
||||
|
||||
|
||||
def validate_artifact(record: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
name = record.get("name", "<unnamed>")
|
||||
if artifact_execution_allowed(record):
|
||||
errors.append(f"{name}: execution_eligible must be false")
|
||||
if record.get("target_execution_performed") is not False:
|
||||
errors.append(f"{name}: target execution state is not false")
|
||||
if record.get("reproducibility") != "BYTE_IDENTICAL_TWO_CLEAN_BUILDS":
|
||||
errors.append(f"{name}: reproducibility is not byte exact")
|
||||
if not is_real_retroarch_artifact(record):
|
||||
errors.append(f"{name}: real RetroArch symbols are incomplete")
|
||||
if len(record.get("sha256", "")) != 64 or record.get("size", 0) <= 0:
|
||||
errors.append(f"{name}: invalid size/hash identity")
|
||||
if not record.get("linker_map_sha256"):
|
||||
errors.append(f"{name}: linker map identity is missing")
|
||||
if not isinstance(record.get("undefined_symbols"), list):
|
||||
errors.append(f"{name}: undefined-symbol inventory is missing")
|
||||
if record.get("embedded_personal_paths") is not False:
|
||||
errors.append(f"{name}: personal path audit did not pass")
|
||||
if not isinstance(record.get("embedded_ip_addresses"), list):
|
||||
errors.append(f"{name}: IP-address inventory is missing")
|
||||
if record.get("embedded_device_ip_addresses") is not False:
|
||||
errors.append(f"{name}: a device/private address is embedded")
|
||||
if record.get("forbidden_markers_found"):
|
||||
errors.append(f"{name}: forbidden marker found")
|
||||
return errors
|
||||
|
||||
|
||||
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 command failed")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
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:
|
||||
build = load_json(root / "manifests/retroarch/phase-1.0a-build.json")
|
||||
artifacts_doc = load_json(
|
||||
root / "manifests/retroarch/phase-1.0a-artifacts.json"
|
||||
)
|
||||
upstreams = load_json(root / "manifests/retroarch/upstreams.json")
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
return errors + [str(error)]
|
||||
|
||||
if build.get("phase") != PHASE or build.get("status") != STATUS:
|
||||
errors.append("phase/status mismatch")
|
||||
if build.get("baseline_commit") != BASELINE:
|
||||
errors.append("baseline mismatch")
|
||||
if build.get("branch") != BRANCH:
|
||||
errors.append("branch mismatch")
|
||||
for field in OFFLINE_AUTHORIZATION_FIELDS:
|
||||
if build.get(field) is not True:
|
||||
errors.append(f"{field} must record the explicit offline grant")
|
||||
for field in DEVICE_AUTHORIZATION_FIELDS:
|
||||
if build.get(field) is not False:
|
||||
errors.append(f"{field} must remain false")
|
||||
for field in (
|
||||
"ps5_connected",
|
||||
"device_request_performed",
|
||||
"files_transferred",
|
||||
"device_write_performed",
|
||||
"target_execution_performed",
|
||||
"install_package_created",
|
||||
"execution_package_created",
|
||||
):
|
||||
if build.get(field) is not False:
|
||||
errors.append(f"{field} must remain false")
|
||||
|
||||
fork = build.get("chimera_retroarch", {})
|
||||
if fork.get("branch") != RETROARCH_BRANCH or len(fork.get("commit", "")) != 40:
|
||||
errors.append("private fork identity is incomplete")
|
||||
if fork.get("working_tree_clean") is not True:
|
||||
errors.append("private fork was not clean at final build")
|
||||
|
||||
profiles = build.get("profiles", {})
|
||||
for name in (
|
||||
"ps5-headless-smokecore",
|
||||
"ps5-software-rgui-smokecore",
|
||||
"host-smokecore-integration",
|
||||
):
|
||||
if name not in profiles or not profile_is_closed(profiles[name]):
|
||||
errors.append(f"profile is not closed: {name}")
|
||||
|
||||
host = build.get("host_tests", {})
|
||||
if (
|
||||
host.get("frames") != 600
|
||||
or host.get("video_fnv1a64") != "43f920496eb5f435"
|
||||
or host.get("audio_fnv1a64") != "a48f47dc08c56625"
|
||||
or host.get("asan_ubsan") != "PASS"
|
||||
or host.get("input_mapping") != "PASS"
|
||||
or host.get("clean_shutdown") != "PASS"
|
||||
):
|
||||
errors.append("host smoke-core evidence mismatch")
|
||||
|
||||
expected_upstreams = {
|
||||
"retroarch": RETROARCH_COMMIT,
|
||||
"ps5_payload_sdk": SDK_COMMIT,
|
||||
"ps5_sdl": SDL_COMMIT,
|
||||
"pacbrew": PACBREW_COMMIT,
|
||||
}
|
||||
for name, commit in expected_upstreams.items():
|
||||
if upstreams.get("sources", {}).get(name, {}).get("commit") != commit:
|
||||
errors.append(f"upstream mismatch: {name}")
|
||||
retroarch = upstreams.get("sources", {}).get("retroarch", {})
|
||||
if (
|
||||
retroarch.get("tag") != RETROARCH_TAG
|
||||
or retroarch.get("tree") != RETROARCH_TREE
|
||||
or retroarch.get("source_archive_sha256") != RETROARCH_ARCHIVE_SHA256
|
||||
):
|
||||
errors.append("RetroArch release provenance mismatch")
|
||||
|
||||
records = artifacts_doc.get("artifacts", [])
|
||||
if {item.get("name") for item in records} != {
|
||||
"retroarch_ps5_headless.elf",
|
||||
"retroarch_ps5_software.elf",
|
||||
}:
|
||||
errors.append("artifact set mismatch")
|
||||
for record in records:
|
||||
errors.extend(validate_artifact(record))
|
||||
|
||||
denylist = root / "manifests/artifact-denylist.json"
|
||||
if not denylist.is_file() or sha256_file(denylist) != DENYLIST_SHA256:
|
||||
errors.append("permanent denylist changed")
|
||||
|
||||
tracked = git(root, "ls-files").splitlines()
|
||||
forbidden_suffixes = (".elf", ".self", ".sprx", ".pkg")
|
||||
for relative in tracked:
|
||||
if relative.lower().endswith(forbidden_suffixes):
|
||||
errors.append(f"tracked target artifact: {relative}")
|
||||
new_text = "\n".join(
|
||||
(root / relative).read_text(encoding="utf-8", errors="replace")
|
||||
for relative in DELIVERABLES
|
||||
if (root / relative).is_file()
|
||||
)
|
||||
unfinished_template = "{" * 2
|
||||
unfinished_label = "".join(("T", "B", "D"))
|
||||
if unfinished_template in new_text or unfinished_label in new_text:
|
||||
errors.append("unfinished placeholder in deliverables")
|
||||
|
||||
if retroarch_root is not None:
|
||||
for record in records:
|
||||
path = retroarch_root / record["local_relative_path"]
|
||||
if not path.is_file():
|
||||
errors.append(f"local artifact missing: {path}")
|
||||
continue
|
||||
if path.stat().st_size != record["size"] or sha256_file(path) != record[
|
||||
"sha256"
|
||||
]:
|
||||
errors.append(f"local artifact identity mismatch: {path.name}")
|
||||
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.resolve() if args.retroarch_root else None,
|
||||
)
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(error, file=sys.stderr)
|
||||
return 1
|
||||
print("Phase 1.0A RetroArch evidence validated")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AA offline fake-adapter integration evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
PHASE = "PHASE_1_0AA_OFFLINE_FAKE_ADAPTER_INTEGRATION"
|
||||
STATUS = "OFFLINE_FAKE_BATCH_INTEGRATION_COMPLETE_LIVE_ADAPTER_BLOCKED"
|
||||
START_COMMIT = "57ff9a1c5937575b00df05f2cd9897118eab1f2a"
|
||||
SOURCE_BINDINGS = {
|
||||
"phase10x_transport_size": 8040,
|
||||
"phase10x_transport_sha256": "568d7578482ecf2fcd9e29085b2eb9d8705fc699611508acdf22afd30f2ddd23",
|
||||
"phase10z_contract_size": 10487,
|
||||
"phase10z_contract_sha256": "0728c2be7f368e0a7f4b68efe86f6e0c5c2f50704a41d0e1992b0bfec19dde06",
|
||||
"phase10aa_integration_size": 11200,
|
||||
"phase10aa_integration_sha256": "8e1cac255f85d2cd14baf8fbc27d631c9b607fc7d19fc57c65462089f0574055",
|
||||
"phase10aa_integration_tests_size": 10667,
|
||||
"phase10aa_integration_tests_sha256": "18a5470a651fcbe4b23f2a68cba499dd19d3da15a229623c054be0b598025699",
|
||||
}
|
||||
SOURCE_FILES = {
|
||||
"phase10x_transport": "tools/phase10x_inactive_transport.py",
|
||||
"phase10z_contract": "tools/phase10z_passive_batch_contract.py",
|
||||
"phase10aa_integration": "tools/phase10aa_offline_fake_batch.py",
|
||||
"phase10aa_integration_tests": "tests/test_phase10aa_offline_fake_batch.py",
|
||||
}
|
||||
AUTHORIZATION_FIELDS = {
|
||||
"target_build_authorized", "ps5_connection_authorized",
|
||||
"device_request_authorized", "result_receive_authorized",
|
||||
"device_transfer_authorized", "device_execution_authorized",
|
||||
"installation_authorized", "autoload_authorized",
|
||||
"device_write_authorized", "automatic_retry", "reconnect_authorized",
|
||||
"resume_authorized",
|
||||
}
|
||||
NETWORK_MODULES = {
|
||||
"socket", "asyncio", "selectors", "urllib", "http", "ftplib",
|
||||
"requests", "telnetlib", "paramiko",
|
||||
}
|
||||
|
||||
|
||||
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.0AA manifest is not an object")
|
||||
return value
|
||||
|
||||
|
||||
def exact_file(path: Path, size: int, digest: str) -> bool:
|
||||
try: payload = path.read_bytes()
|
||||
except OSError: return False
|
||||
return len(payload) == size and hashlib.sha256(payload).hexdigest() == digest
|
||||
|
||||
|
||||
def _imports(tree: ast.AST) -> set[str]:
|
||||
values: set[str] = set()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
values.update(alias.name.split(".")[0] for alias in node.names)
|
||||
elif isinstance(node, ast.ImportFrom) and node.module:
|
||||
values.add(node.module.split(".")[0])
|
||||
return values
|
||||
|
||||
|
||||
def validate_record(record: dict[str, Any], root: Path | None = None) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if record.get("phase") != PHASE or record.get("status") != STATUS:
|
||||
errors.append("phase/status mismatch")
|
||||
if record.get("start_commit") != START_COMMIT:
|
||||
errors.append("start commit mismatch")
|
||||
if record.get("activation") != {
|
||||
"active": False, "integration_sha256": None, "run_id": None,
|
||||
"target_address": None, "target_port": None, "window": None,
|
||||
"deadline_seconds": None,
|
||||
}:
|
||||
errors.append("activation is not inert")
|
||||
if record.get("source_bindings") != SOURCE_BINDINGS:
|
||||
errors.append("source bindings mismatch")
|
||||
if record.get("fake_boundary") != {
|
||||
"exact_builtin_adapter_required": True,
|
||||
"adapter_subclasses_allowed": False,
|
||||
"exact_builtin_clock_required": True,
|
||||
"clock_subclasses_allowed": False,
|
||||
"exact_fake_evidence_store_required": True,
|
||||
"live_adapter_protocol_present": False,
|
||||
"network_import_present": False,
|
||||
"real_clock_present": False,
|
||||
"target_present": False,
|
||||
"cli_present": False,
|
||||
"maximum_fake_events": 257,
|
||||
"event_kinds": ["DATA", "HARD_DEADLINE", "REMOTE_EOF", "BLOCKED"],
|
||||
}:
|
||||
errors.append("fake boundary mismatch")
|
||||
if record.get("ordering_contract") != {
|
||||
"receipt_before_fake_open": True, "fake_open_count": 1,
|
||||
"complete_batch_send_count": 1, "fake_close_count": 1,
|
||||
"completion_event": "SYNTHETIC_HARD_DEADLINE_ONLY",
|
||||
"early_deadline": "INVALID", "remote_eof": "INVALID",
|
||||
"blocked_receive": "INVALID", "missing_deadline": "INVALID",
|
||||
"data_at_or_after_deadline": "INVALID", "partial_result": "INVALID",
|
||||
"incoming_iac": "INVALID", "retry_allowed": False,
|
||||
"reconnect_allowed": False, "resume_allowed": False,
|
||||
}:
|
||||
errors.append("ordering contract mismatch")
|
||||
evidence = record.get("evidence_contract", {})
|
||||
if evidence != {
|
||||
"exclusive_create": True, "consumed_receipt_retained_on_failure": True,
|
||||
"failure_output_created": False, "sanitized_output_receipt_bound": True,
|
||||
"batch_sha256_recorded": True, "batch_size_recorded": True,
|
||||
"target_retained": False, "raw_transcript_persisted": False,
|
||||
"logical_event_buffer_discarded": True,
|
||||
"physical_memory_erasure_proven": False,
|
||||
"directory_entry_durability_proven": False,
|
||||
"device_behavior_proven": False, "exact_identity_proven": False,
|
||||
}:
|
||||
errors.append("evidence contract mismatch")
|
||||
authority = record.get("authorizations", {})
|
||||
if set(authority) != AUTHORIZATION_FIELDS or any(
|
||||
authority.get(field) is not False for field in AUTHORIZATION_FIELDS):
|
||||
errors.append("authorization fields are not exactly false")
|
||||
if record.get("decision") != {
|
||||
"offline_fake_batch_integration_complete": True,
|
||||
"live_adapter_created": False, "live_adapter_allowed": False,
|
||||
"live_collection_allowed": False, "device_action_allowed": False,
|
||||
"phase10ab_offline_live_adapter_feasibility_review_allowed": True,
|
||||
"next_step": "OFFLINE_LIVE_ADAPTER_TIMEOUT_AND_CLEANUP_FEASIBILITY_REVIEW",
|
||||
}:
|
||||
errors.append("decision mismatch")
|
||||
performed = record.get("performed_actions", {})
|
||||
if not performed or any(value is not False for value in performed.values()):
|
||||
errors.append("performed actions are missing or true")
|
||||
if record.get("tests") != {
|
||||
"chimera_gfx_ctest": "89_OF_89_PASS", "phase10aa_guardrails": 20,
|
||||
"phase10aa_integration_tests": 25, "safety_audit": "PASS",
|
||||
"secret_scan": "PASS", "network_required_by_tests": False,
|
||||
"hardware_claim_from_host_test": False,
|
||||
}:
|
||||
errors.append("test evidence mismatch")
|
||||
|
||||
if root is not None:
|
||||
for prefix, relative in SOURCE_FILES.items():
|
||||
if not exact_file(root / relative, SOURCE_BINDINGS[f"{prefix}_size"],
|
||||
SOURCE_BINDINGS[f"{prefix}_sha256"]):
|
||||
errors.append(f"source identity mismatch: {relative}")
|
||||
source_path = root / SOURCE_FILES["phase10aa_integration"]
|
||||
try:
|
||||
source = source_path.read_text(encoding="utf-8")
|
||||
tree = ast.parse(source)
|
||||
except (OSError, SyntaxError, UnicodeError):
|
||||
errors.append("integration source cannot be parsed")
|
||||
else:
|
||||
if _imports(tree) & NETWORK_MODULES or "time" in _imports(tree):
|
||||
errors.append("integration imports network or real-clock support")
|
||||
names = {node.name for node in ast.walk(tree)
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))}
|
||||
if {"main", "connect", "recv", "open_socket"} & names:
|
||||
errors.append("integration exposes a live API or CLI")
|
||||
if "target_address" in source or "target_port" in source:
|
||||
errors.append("integration contains target fields")
|
||||
required_shapes = (
|
||||
"type(adapter) is not OfflineFakeBatchAdapter",
|
||||
"type(clock) is not OfflineFakeClock",
|
||||
"type(evidence) is not OfflineFakeEvidenceStore",
|
||||
"receipt = evidence.create_fake_consumed_receipt",
|
||||
"adapter.send_one_batch(batch)",
|
||||
"accumulator.seal_at_hard_deadline(True)",
|
||||
)
|
||||
if any(shape not in source for shape in required_shapes):
|
||||
errors.append("exact fake ordering source shape is missing")
|
||||
approval = (root / "docs/approvals/phase-1.0aa-offline-fake-adapter.md").read_text(encoding="utf-8")
|
||||
if "active=false" not in approval or "attested=false" not in approval or \
|
||||
"ps5_connection_authorized=false" not in approval:
|
||||
errors.append("approval template is not inert")
|
||||
for base in (root / "tools", root / "tests", root / "docs", root / "manifests"):
|
||||
if any(path.is_file() and path.suffix.lower() in {".elf", ".self", ".sprx", ".pkg"} for path in base.rglob("*")):
|
||||
errors.append("target artifact exists in a Phase-1.0AA output area")
|
||||
break
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
args = parser.parse_args(); root = args.root.resolve()
|
||||
try:
|
||||
record = load_json(root / "manifests/retroarch/phase-1.0aa-offline-fake-adapter.json")
|
||||
errors = validate_record(record, root)
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
errors = [f"validation input failed: {error}"]
|
||||
if errors:
|
||||
for error in errors: print(f"ERROR: {error}")
|
||||
return 1
|
||||
print("Phase-1.0AA offline fake-adapter validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AB offline live-adapter feasibility evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
PHASE = "PHASE_1_0AB_OFFLINE_LIVE_ADAPTER_FEASIBILITY"
|
||||
STATUS = "PARTIAL_FEASIBILITY_LIVE_IMPLEMENTATION_BLOCKED"
|
||||
START_COMMIT = "06d2fe831959b71562401722ae36821faa197636"
|
||||
MODEL_SIZE = 8738
|
||||
MODEL_SHA256 = "7d1aa32d49b91b1e5cf3a085dda033767bdf17ab34389ff044f7403f86287959"
|
||||
MODEL_TEST_SIZE = 7558
|
||||
MODEL_TEST_SHA256 = "39597e991b15bcfa9aa28cb2f68c87ed048c38482c6dbf81a28c56ccfceb0a48"
|
||||
NETWORK_MODULES = {"socket", "selectors", "select", "asyncio", "urllib", "http", "requests", "telnetlib"}
|
||||
AUTHORIZATION_FIELDS = {
|
||||
"target_build_authorized", "ps5_connection_authorized",
|
||||
"device_request_authorized", "result_receive_authorized",
|
||||
"device_transfer_authorized", "device_execution_authorized",
|
||||
"installation_authorized", "autoload_authorized",
|
||||
"device_write_authorized", "automatic_retry", "reconnect_authorized",
|
||||
"resume_authorized",
|
||||
}
|
||||
|
||||
|
||||
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.0AB manifest is not an object")
|
||||
return value
|
||||
|
||||
|
||||
def exact_file(path: Path, size: int, digest: str) -> bool:
|
||||
try: payload = path.read_bytes()
|
||||
except OSError: return False
|
||||
return len(payload) == size and hashlib.sha256(payload).hexdigest() == digest
|
||||
|
||||
|
||||
def _imports(tree: ast.AST) -> set[str]:
|
||||
values=set()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import): values.update(alias.name.split(".")[0] for alias in node.names)
|
||||
elif isinstance(node, ast.ImportFrom) and node.module: values.add(node.module.split(".")[0])
|
||||
return values
|
||||
|
||||
|
||||
def validate_record(record: dict[str, Any], root: Path | None = None,
|
||||
python_root: Path | None = None) -> list[str]:
|
||||
errors=[]
|
||||
if record.get("phase") != PHASE or record.get("status") != STATUS: errors.append("phase/status mismatch")
|
||||
if record.get("start_commit") != START_COMMIT: errors.append("start commit mismatch")
|
||||
if record.get("activation") != {"active": False, "trace_model_sha256": None, "target_address": None, "target_port": None, "run_id": None}: errors.append("activation is not inert")
|
||||
runtime=record.get("local_runtime", {})
|
||||
if runtime != {
|
||||
"python_version": "3.13.2", "compiler": "MSC_V_1942_64_BIT_AMD64",
|
||||
"platform": "WINDOWS", "default_selector": "SelectSelector",
|
||||
"monotonic_implementation": "QueryPerformanceCounter()", "monotonic": True,
|
||||
"monotonic_adjustable": False, "reported_resolution_seconds": 1e-7,
|
||||
"socket_py_size": 38741, "socket_py_sha256": "523695ac3383799547b421b4fe18004de1e80181e97181b6d7a10533b47f4c49",
|
||||
"selectors_py_size": 20060, "selectors_py_sha256": "b3d6cebd4a3a03b4a614f12f171622ce4e4ba3295b9e8b89e2bde051003106eb",
|
||||
"socket_pyd_size": 84984, "socket_pyd_sha256": "8daefaff53e6956f5aea5279a7c71f17d8c63e2b0d54031c3b9e82fcb0fb84db",
|
||||
"select_pyd_size": 32248, "select_pyd_sha256": "baee284995b22d495fd12fa8378077e470978db1522c61bfb9af37fb827f33d1",
|
||||
}: errors.append("local runtime record mismatch")
|
||||
if record.get("source_bindings") != {
|
||||
"phase10aa_integration_sha256": "8e1cac255f85d2cd14baf8fbc27d631c9b607fc7d19fc57c65462089f0574055",
|
||||
"phase10ab_trace_model_size": MODEL_SIZE, "phase10ab_trace_model_sha256": MODEL_SHA256,
|
||||
"phase10ab_trace_tests_size": MODEL_TEST_SIZE, "phase10ab_trace_tests_sha256": MODEL_TEST_SHA256,
|
||||
}: errors.append("source bindings mismatch")
|
||||
if record.get("feasibility") != {
|
||||
"receipt_before_socket": "FEASIBLE_FROM_EXISTING_HOST_MODEL",
|
||||
"numeric_address_only": "DESIGN_REQUIRED",
|
||||
"nonblocking_before_connect": "FEASIBLE_FROM_LOCAL_RUNTIME",
|
||||
"pending_connect": "PARTIAL", "complete_send_loop": "FEASIBLE_FROM_LOCAL_RUNTIME",
|
||||
"bounded_receive_memory": "FEASIBLE_FROM_EXISTING_MODEL",
|
||||
"hard_wall_clock_deadline": "PARTIAL",
|
||||
"prompt_independent_completion": "FEASIBLE_FROM_Z",
|
||||
"remote_eof": "FEASIBLE_FAIL_CLOSED",
|
||||
"local_descriptor_cleanup": "FEASIBLE_BY_DESIGN",
|
||||
"remote_shell_cleanup": "UNPROVEN", "retry_reconnect_resume": "EXCLUDED",
|
||||
}: errors.append("feasibility matrix mismatch")
|
||||
if record.get("trace_model") != {
|
||||
"synthetic_input_only": True, "maximum_events": 512,
|
||||
"maximum_batch_bytes": 1035, "maximum_receive_bytes": 65536,
|
||||
"maximum_deadline_seconds": 10, "network_import_present": False,
|
||||
"selector_import_present": False, "real_clock_present": False,
|
||||
"address_present": False, "cli_present": False, "file_output_present": False,
|
||||
"exact_identity_proven": False, "device_behavior_proven": False,
|
||||
}: errors.append("trace model record mismatch")
|
||||
stops=record.get("hard_stops", {})
|
||||
if set(stops) != {"live_adapter_implementation","socket_creation","dns","target_retention","connection","request","retry","device_action"} or any(value is not True for value in stops.values()): errors.append("hard stops mismatch")
|
||||
auth=record.get("authorizations", {})
|
||||
if set(auth) != AUTHORIZATION_FIELDS or any(auth.get(field) is not False for field in AUTHORIZATION_FIELDS): errors.append("authorization fields are not exactly false")
|
||||
if record.get("decision") != {
|
||||
"overall": STATUS, "live_adapter_created": False, "live_adapter_allowed": False,
|
||||
"device_action_allowed": False, "phase10ac_offline_dormant_syscall_facade_allowed": True,
|
||||
"next_step": "OFFLINE_DORMANT_TARGET_FREE_ADAPTER_WITH_FAKE_SYSCALLS",
|
||||
}: errors.append("decision mismatch")
|
||||
performed=record.get("performed_actions", {})
|
||||
if not performed or any(value is not False for value in performed.values()): errors.append("performed actions are missing or true")
|
||||
if record.get("tests") != {
|
||||
"chimera_gfx_ctest": "92_OF_92_PASS", "phase10ab_guardrails": 20,
|
||||
"phase10ab_trace_tests": 25, "safety_audit": "PASS", "secret_scan": "PASS",
|
||||
"network_required_by_tests": False, "hardware_claim_from_host_test": False,
|
||||
}: errors.append("test evidence mismatch")
|
||||
|
||||
if root is not None:
|
||||
model=root / "tools/phase10ab_nonblocking_trace_model.py"; tests=root / "tests/test_phase10ab_nonblocking_trace_model.py"
|
||||
if not exact_file(model, MODEL_SIZE, MODEL_SHA256): errors.append("trace model identity mismatch")
|
||||
if not exact_file(tests, MODEL_TEST_SIZE, MODEL_TEST_SHA256): errors.append("trace tests identity mismatch")
|
||||
try: source=model.read_text(encoding="utf-8"); tree=ast.parse(source)
|
||||
except (OSError,SyntaxError,UnicodeError): errors.append("trace model cannot be parsed")
|
||||
else:
|
||||
if _imports(tree) & NETWORK_MODULES or "time" in _imports(tree): errors.append("trace model imports live capability")
|
||||
names={node.name for node in ast.walk(tree) if isinstance(node,(ast.FunctionDef,ast.AsyncFunctionDef))}
|
||||
if {"main","connect","send","recv"} & names: errors.append("trace model exposes live API")
|
||||
if "target_address" in source or "target_port" in source: errors.append("trace model contains target fields")
|
||||
approval=(root / "docs/approvals/phase-1.0ab-live-adapter-feasibility.md").read_text(encoding="utf-8")
|
||||
if "active=false" not in approval or "ps5_connection_authorized=false" not in approval: errors.append("approval is not inert")
|
||||
if python_root is not None:
|
||||
paths={
|
||||
"socket_py": python_root / "Lib/socket.py", "selectors_py": python_root / "Lib/selectors.py",
|
||||
"socket_pyd": python_root / "DLLs/_socket.pyd", "select_pyd": python_root / "DLLs/select.pyd",
|
||||
}
|
||||
for prefix,path in paths.items():
|
||||
if not exact_file(path, runtime[f"{prefix}_size"], runtime[f"{prefix}_sha256"]): errors.append(f"local runtime identity mismatch: {prefix}")
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser=argparse.ArgumentParser();parser.add_argument("--root",type=Path,required=True);parser.add_argument("--python-root",type=Path)
|
||||
args=parser.parse_args();root=args.root.resolve()
|
||||
try: record=load_json(root / "manifests/retroarch/phase-1.0ab-live-adapter-feasibility.json");errors=validate_record(record,root,args.python_root)
|
||||
except (OSError,ValueError,json.JSONDecodeError) as error: errors=[f"validation input failed: {error}"]
|
||||
if errors:
|
||||
for error in errors: print(f"ERROR: {error}")
|
||||
return 1
|
||||
print("Phase-1.0AB offline live-adapter feasibility validation passed");return 0
|
||||
|
||||
|
||||
if __name__ == "__main__": raise SystemExit(main())
|
||||
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate the inactive Phase-1.0AC dormant-adapter evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
PHASE = "PHASE_1_0AC_OFFLINE_DORMANT_ADAPTER"
|
||||
STATUS = "OFFLINE_DORMANT_FAKE_SYSCALL_ADAPTER_COMPLETE_LIVE_ADAPTER_BLOCKED"
|
||||
START_COMMIT = "3bc8ac09615dda3c4ee3ad02f19440ca9f4f8f96"
|
||||
ADAPTER_SIZE = 13282
|
||||
ADAPTER_SHA256 = "6f28926b59fd9afa6de1ff36d4fa9b013d7e027c9adc0bffd9445d7c89acf939"
|
||||
ADAPTER_TEST_SIZE = 11681
|
||||
ADAPTER_TEST_SHA256 = "9c8c611dbab5e43df71d523169d9a1bf7579ed1918943df7531bf75ef4b9abfc"
|
||||
NETWORK_MODULES = {
|
||||
"socket", "selectors", "select", "asyncio", "urllib", "http",
|
||||
"requests", "ftplib", "telnetlib", "subprocess",
|
||||
}
|
||||
AUTHORIZATION_FIELDS = {
|
||||
"target_build_authorized", "ps5_connection_authorized",
|
||||
"device_request_authorized", "result_receive_authorized",
|
||||
"device_transfer_authorized", "device_execution_authorized",
|
||||
"installation_authorized", "autoload_authorized",
|
||||
"device_write_authorized", "automatic_retry", "reconnect_authorized",
|
||||
"resume_authorized",
|
||||
}
|
||||
|
||||
|
||||
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.0AC manifest is not an object")
|
||||
return value
|
||||
|
||||
|
||||
def exact_file(path: Path, size: int, digest: str) -> bool:
|
||||
try:
|
||||
payload = path.read_bytes()
|
||||
except OSError:
|
||||
return False
|
||||
return len(payload) == size and hashlib.sha256(payload).hexdigest() == digest
|
||||
|
||||
|
||||
def _imports(tree: ast.AST) -> set[str]:
|
||||
values: set[str] = set()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
values.update(alias.name.split(".")[0] for alias in node.names)
|
||||
elif isinstance(node, ast.ImportFrom) and node.module:
|
||||
values.add(node.module.split(".")[0])
|
||||
return values
|
||||
|
||||
|
||||
def validate_record(record: dict[str, Any], root: Path | None = None) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if record.get("phase") != PHASE or record.get("status") != STATUS:
|
||||
errors.append("phase/status mismatch")
|
||||
if record.get("start_commit") != START_COMMIT:
|
||||
errors.append("start commit mismatch")
|
||||
if record.get("activation") != {
|
||||
"active": False, "adapter_sha256": None, "target_address": None,
|
||||
"target_port": None, "run_id": None,
|
||||
}:
|
||||
errors.append("activation is not inert")
|
||||
if record.get("source_bindings") != {
|
||||
"phase10ab_trace_model_sha256": "7d1aa32d49b91b1e5cf3a085dda033767bdf17ab34389ff044f7403f86287959",
|
||||
"phase10ac_adapter_size": ADAPTER_SIZE,
|
||||
"phase10ac_adapter_sha256": ADAPTER_SHA256,
|
||||
"phase10ac_tests_size": ADAPTER_TEST_SIZE,
|
||||
"phase10ac_tests_sha256": ADAPTER_TEST_SHA256,
|
||||
}:
|
||||
errors.append("source bindings mismatch")
|
||||
if record.get("adapter") != {
|
||||
"exact_builtin_fake_facade_required": True,
|
||||
"exact_builtin_fake_clock_required": True,
|
||||
"precommitted_receipt_required": True,
|
||||
"maximum_fake_steps": 1024, "maximum_receive_bytes": 65536,
|
||||
"maximum_batch_bytes": 1035, "maximum_deadline_seconds": 10,
|
||||
"network_import_present": False, "selector_import_present": False,
|
||||
"dns_present": False, "real_clock_present": False,
|
||||
"address_present": False, "cli_present": False,
|
||||
"file_output_present": False,
|
||||
"live_adapter_protocol_present": False, "target_retained": False,
|
||||
"device_behavior_proven": False,
|
||||
}:
|
||||
errors.append("adapter boundary mismatch")
|
||||
if record.get("lifecycle") != {
|
||||
"create_count": 1, "nonblocking_before_connect": True,
|
||||
"pending_connect_requires_write_ready": True,
|
||||
"pending_connect_requires_zero_so_error": True,
|
||||
"partial_write_loop": True, "zero_write_rejected": True,
|
||||
"remote_eof_rejected": True,
|
||||
"deadline_wins_readiness_race": True,
|
||||
"deadline_only_completion": True,
|
||||
"local_close_on_success": True, "local_close_on_failure": True,
|
||||
"remote_cleanup_proven": False, "automatic_retry": False,
|
||||
"reconnect": False, "resume": False,
|
||||
}:
|
||||
errors.append("lifecycle contract mismatch")
|
||||
stops = record.get("hard_stops", {})
|
||||
if set(stops) != {
|
||||
"live_adapter_implementation", "socket_creation", "dns",
|
||||
"target_retention", "connection", "device_request", "retry",
|
||||
"device_action",
|
||||
} or any(value is not True for value in stops.values()):
|
||||
errors.append("hard stops mismatch")
|
||||
authorizations = record.get("authorizations", {})
|
||||
if set(authorizations) != AUTHORIZATION_FIELDS or any(
|
||||
authorizations.get(field) is not False
|
||||
for field in AUTHORIZATION_FIELDS):
|
||||
errors.append("authorization fields are not exactly false")
|
||||
performed = record.get("performed_actions", {})
|
||||
if not performed or any(value is not False for value in performed.values()):
|
||||
errors.append("performed actions are missing or true")
|
||||
if record.get("decision") != {
|
||||
"overall": STATUS, "dormant_fake_adapter_created": True,
|
||||
"live_adapter_created": False, "live_adapter_allowed": False,
|
||||
"device_action_allowed": False,
|
||||
"phase10ad_offline_inactive_activation_design_allowed": True,
|
||||
"next_step": "OFFLINE_NUMERIC_TARGET_AND_INACTIVE_ACTIVATION_CONTRACT",
|
||||
}:
|
||||
errors.append("decision mismatch")
|
||||
if record.get("tests") != {
|
||||
"chimera_gfx_ctest": "95_OF_95_PASS", "phase10ac_guardrails": 20,
|
||||
"phase10ac_adapter_tests": 32, "safety_audit": "PASS",
|
||||
"secret_scan": "PASS", "network_required_by_tests": False,
|
||||
"hardware_claim_from_host_test": False,
|
||||
}:
|
||||
errors.append("test evidence mismatch")
|
||||
|
||||
if root is not None:
|
||||
adapter = root / "tools/phase10ac_dormant_adapter.py"
|
||||
tests = root / "tests/test_phase10ac_dormant_adapter.py"
|
||||
if not exact_file(adapter, ADAPTER_SIZE, ADAPTER_SHA256):
|
||||
errors.append("dormant adapter identity mismatch")
|
||||
if not exact_file(tests, ADAPTER_TEST_SIZE, ADAPTER_TEST_SHA256):
|
||||
errors.append("dormant adapter tests identity mismatch")
|
||||
try:
|
||||
source = adapter.read_text(encoding="utf-8")
|
||||
tree = ast.parse(source)
|
||||
except (OSError, SyntaxError, UnicodeError):
|
||||
errors.append("dormant adapter cannot be parsed")
|
||||
else:
|
||||
imports = _imports(tree)
|
||||
if imports & NETWORK_MODULES or "time" in imports:
|
||||
errors.append("dormant adapter imports a live capability")
|
||||
names = {
|
||||
node.name for node in ast.walk(tree)
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||
}
|
||||
if {"connect", "send", "recv", "main"} & names:
|
||||
errors.append("dormant adapter exposes a live API")
|
||||
if re.search(r"(?<!\d)(?:\d{1,3}\.){3}\d{1,3}(?!\d)", source):
|
||||
errors.append("dormant adapter embeds an address")
|
||||
if "target_address" in source or "target_port" in source:
|
||||
errors.append("dormant adapter contains target fields")
|
||||
approval = (root / "docs/approvals/phase-1.0ac-dormant-adapter.md").read_text(
|
||||
encoding="utf-8")
|
||||
if "active=false" not in approval or \
|
||||
"ps5_connection_authorized=false" not in approval or \
|
||||
"automatic_retry=false" not in approval:
|
||||
errors.append("approval record is not inert")
|
||||
forbidden = list(root.glob("**/*phase10ac*.elf")) + \
|
||||
list(root.glob("**/*phase-1.0ac*.elf"))
|
||||
if forbidden:
|
||||
errors.append("Phase-1.0AC target artifact exists")
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
root = args.root.resolve()
|
||||
try:
|
||||
record = load_json(
|
||||
root / "manifests/retroarch/phase-1.0ac-dormant-adapter.json")
|
||||
errors = validate_record(record, root)
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
errors = [f"validation input failed: {error}"]
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"ERROR: {error}")
|
||||
return 1
|
||||
print("Phase-1.0AC dormant-adapter validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate the inactive Phase-1.0AD record and byte bindings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def digest(path: Path) -> tuple[int, str]:
|
||||
data = path.read_bytes()
|
||||
return len(data), hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
root = parser.parse_args().root.resolve()
|
||||
manifest = json.loads((root / "manifests/retroarch/phase-1.0ad-inactive-activation.json").read_text(encoding="utf-8"))
|
||||
activation = manifest["activation"]
|
||||
authorization = manifest["authorizations"]
|
||||
assert manifest["phase"] == "PHASE_1_0AD_INACTIVE_NUMERIC_TARGET_CONTRACT"
|
||||
assert activation["active"] is False
|
||||
assert all(activation[name] is None for name in (
|
||||
"target_address", "target_port", "run_id", "not_before",
|
||||
"expires_at", "launcher_sha256", "payload_sha256", "approval_sha256"))
|
||||
assert activation["one_shot"] is True
|
||||
assert activation["automatic_retry"] is False
|
||||
assert activation["reconnect"] is False
|
||||
assert activation["resume"] is False
|
||||
assert not any(authorization.values())
|
||||
assert manifest["decision"]["device_action_allowed"] is False
|
||||
assert manifest["decision"]["bigapp_launcher_implementation_allowed"] is False
|
||||
|
||||
bindings = manifest["source_bindings"]
|
||||
files = {
|
||||
"contract": root / "tools/phase10ad_activation_contract.py",
|
||||
"tests": root / "tests/test_phase10ad_activation_contract.py",
|
||||
"documentation": root / "docs/retroarch/phase-1.0ad-inactive-activation-contract.md",
|
||||
}
|
||||
for name, path in files.items():
|
||||
size, sha256 = digest(path)
|
||||
assert bindings[f"{name}_size"] == size
|
||||
assert bindings[f"{name}_sha256"] == sha256
|
||||
|
||||
tree = ast.parse(files["contract"].read_text(encoding="utf-8"))
|
||||
imports = {alias.name.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Import) for alias in node.names}
|
||||
imports.update(node.module.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.ImportFrom) and node.module)
|
||||
assert not imports.intersection({"socket", "selectors", "subprocess", "urllib", "http", "requests"})
|
||||
calls = {node.func.id for node in ast.walk(tree) if isinstance(node, ast.Call)
|
||||
and isinstance(node.func, ast.Name)}
|
||||
assert not calls.intersection({"open", "exec", "eval", "compile", "input"})
|
||||
print("Phase-1.0AD inactive activation contract validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AE against exact official shsrv worktrees."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def git(root: Path, value: str) -> str:
|
||||
return subprocess.run(["git", "-C", str(root), "rev-parse", value],
|
||||
check=True, capture_output=True, text=True).stdout.strip()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
parser.add_argument("--shsrv-root", type=Path, required=True)
|
||||
parser.add_argument("--shsrv-v07-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
data = json.loads((args.root / "manifests/retroarch/phase-1.0ae-launcher-architecture.json").read_text(encoding="utf-8"))
|
||||
sources = data["official_sources"]
|
||||
for label, root in (("v019", args.shsrv_root), ("v07", args.shsrv_v07_root)):
|
||||
assert git(root, "HEAD") == sources[f"{label}_commit"]
|
||||
assert git(root, "HEAD^{tree}") == sources[f"{label}_tree"]
|
||||
assert sha256(args.shsrv_root / "bundles/hbldr/hbldr.c") == sources["v019_hbldr_sha256"]
|
||||
assert sha256(args.shsrv_root / "elfldr.c") == sources["v019_elfldr_sha256"]
|
||||
assert sha256(args.shsrv_root / "pt.c") == sources["v019_pt_sha256"]
|
||||
assert sha256(args.shsrv_v07_root / "bundles/hbldr/main.c") == sources["v07_hbldr_sha256"]
|
||||
assert sha256(args.shsrv_v07_root / "elfldr.c") == sources["v07_elfldr_sha256"]
|
||||
assert sha256(args.shsrv_v07_root / "pt.c") == sources["v07_pt_sha256"]
|
||||
v019 = (args.shsrv_root / "bundles/hbldr/hbldr.c").read_text(encoding="utf-8")
|
||||
v07 = (args.shsrv_v07_root / "bundles/hbldr/main.c").read_text(encoding="utf-8")
|
||||
assert "FAKE00000" in v019 and "remount_system_ex" in v019
|
||||
assert "PPSA01659" in v07 and "FAKE00000" not in v07 and "nmount" not in v07
|
||||
assert data["lineage_decision"]["selected_reference"] == "OFFICIAL_SHSRV_V0_7"
|
||||
assert data["mandatory_policy"]["kill_existing_bigapp"] is False
|
||||
assert data["decision"]["target_implementation_allowed"] is False
|
||||
assert data["decision"]["device_action_allowed"] is False
|
||||
assert not any(data["authorizations"].values())
|
||||
print("Phase-1.0AE launcher architecture validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AF identities and offline-only boundary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def identity(path: Path) -> tuple[int, str]:
|
||||
data = path.read_bytes()
|
||||
return len(data), hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
root = parser.parse_args().root
|
||||
data = json.loads((root / "manifests/retroarch/phase-1.0af-bigapp-lifecycle-model.json").read_text(encoding="utf-8"))
|
||||
bindings = data["source_bindings"]
|
||||
assert hashlib.sha256((root / "manifests/retroarch/phase-1.0ae-launcher-architecture.json").read_bytes()).hexdigest() == bindings["phase10ae_manifest_sha256"]
|
||||
for prefix, relative in (("model", "tools/phase10af_bigapp_lifecycle_model.py"),
|
||||
("tests", "tests/test_phase10af_bigapp_lifecycle_model.py")):
|
||||
size, digest = identity(root / relative)
|
||||
assert bindings[f"{prefix}_size"] == size
|
||||
assert bindings[f"{prefix}_sha256"] == digest
|
||||
source = (root / "tools/phase10af_bigapp_lifecycle_model.py").read_text(encoding="utf-8")
|
||||
tree = ast.parse(source)
|
||||
imports = {alias.name.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Import) for alias in node.names}
|
||||
imports.update(node.module.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.ImportFrom) and node.module)
|
||||
assert not imports.intersection({"socket", "subprocess", "os", "sys", "ctypes", "time", "pathlib"})
|
||||
assert "PPSA01659" in source and "FAKE00000" not in source
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["target_implementation_allowed"] is False
|
||||
assert data["decision"]["device_action_allowed"] is False
|
||||
print("Phase-1.0AF offline lifecycle validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AG byte identities and closed parser boundary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def identity(path: Path) -> tuple[int, str]:
|
||||
data = path.read_bytes()
|
||||
return len(data), hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
root = parser.parse_args().root
|
||||
data = json.loads((root / "manifests/retroarch/phase-1.0ag-bounded-elf-contract.json").read_text(encoding="utf-8"))
|
||||
bindings = data["source_bindings"]
|
||||
assert hashlib.sha256((root / "manifests/retroarch/phase-1.0af-bigapp-lifecycle-model.json").read_bytes()).hexdigest() == bindings["phase10af_manifest_sha256"]
|
||||
for prefix, relative in (("validator", "tools/phase10ag_bounded_elf.py"),
|
||||
("tests", "tests/test_phase10ag_bounded_elf.py")):
|
||||
size, digest = identity(root / relative)
|
||||
assert size == bindings[f"{prefix}_size"]
|
||||
assert digest == bindings[f"{prefix}_sha256"]
|
||||
source = (root / "tools/phase10ag_bounded_elf.py").read_text(encoding="utf-8")
|
||||
tree = ast.parse(source)
|
||||
imports = {alias.name.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Import) for alias in node.names}
|
||||
imports.update(node.module.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.ImportFrom) and node.module)
|
||||
assert not imports.intersection({"pathlib", "os", "sys", "subprocess", "socket", "ctypes", "mmap"})
|
||||
assert data["historical_reference"]["validated_by_phase10ag"] is False
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["target_implementation_allowed"] is False
|
||||
assert data["decision"]["device_action_allowed"] is False
|
||||
print("Phase-1.0AG bounded ELF contract validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AH identities and official source bindings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
|
||||
def digest(path: Path) -> tuple[int, str]:
|
||||
data = path.read_bytes()
|
||||
return len(data), hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def git(root: Path) -> str:
|
||||
return subprocess.run(["git", "-C", str(root), "rev-parse", "HEAD"],
|
||||
check=True, capture_output=True, text=True).stdout.strip()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
parser.add_argument("--shsrv-v07-root", type=Path, required=True)
|
||||
parser.add_argument("--sdk-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
data = json.loads((args.root / "manifests/retroarch/phase-1.0ah-dynamic-contract.json").read_text(encoding="utf-8"))
|
||||
bindings = data["source_bindings"]
|
||||
assert hashlib.sha256((args.root / "manifests/retroarch/phase-1.0ag-bounded-elf-contract.json").read_bytes()).hexdigest() == bindings["phase10ag_manifest_sha256"]
|
||||
for prefix, relative in (("contract", "tools/phase10ah_dynamic_contract.py"),
|
||||
("tests", "tests/test_phase10ah_dynamic_contract.py")):
|
||||
size, sha256 = digest(args.root / relative)
|
||||
assert size == bindings[f"{prefix}_size"] and sha256 == bindings[f"{prefix}_sha256"]
|
||||
assert git(args.shsrv_v07_root) == bindings["official_shsrv_v07_commit"]
|
||||
assert digest(args.shsrv_v07_root / "elfldr.c")[1] == bindings["official_shsrv_v07_elfldr_sha256"]
|
||||
assert git(args.sdk_root) == bindings["official_sdk_v041_commit"]
|
||||
assert digest(args.sdk_root / "crt/rtld_payload.c")[1] == bindings["official_sdk_v041_rtld_payload_sha256"]
|
||||
tree = ast.parse((args.root / "tools/phase10ah_dynamic_contract.py").read_text(encoding="utf-8"))
|
||||
imports = {alias.name.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Import) for alias in node.names}
|
||||
imports.update(node.module.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.ImportFrom) and node.module)
|
||||
assert not imports.intersection({"pathlib", "os", "sys", "subprocess", "socket", "ctypes", "mmap"})
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["historical_phase10m_reference"]["validated_by_phase10ah"] is False
|
||||
assert data["decision"]["target_mapping_allowed"] is False
|
||||
assert data["decision"]["device_action_allowed"] is False
|
||||
print("Phase-1.0AH dynamic contract validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AI byte bindings and closed model boundary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def digest(path: Path) -> tuple[int, str]:
|
||||
data = path.read_bytes()
|
||||
return len(data), hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
root = parser.parse_args().root
|
||||
data = json.loads((root / "manifests/retroarch/phase-1.0ai-mapping-model.json").read_text(encoding="utf-8"))
|
||||
bindings = data["source_bindings"]
|
||||
assert hashlib.sha256((root / "manifests/retroarch/phase-1.0ah-dynamic-contract.json").read_bytes()).hexdigest() == bindings["phase10ah_manifest_sha256"]
|
||||
for prefix, relative in (("model", "tools/phase10ai_mapping_model.py"),
|
||||
("tests", "tests/test_phase10ai_mapping_model.py")):
|
||||
size, sha256 = digest(root / relative)
|
||||
assert size == bindings[f"{prefix}_size"] and sha256 == bindings[f"{prefix}_sha256"]
|
||||
tree = ast.parse((root / "tools/phase10ai_mapping_model.py").read_text(encoding="utf-8"))
|
||||
imports = {alias.name.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Import) for alias in node.names}
|
||||
imports.update(node.module.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.ImportFrom) and node.module)
|
||||
assert not imports.intersection({"os", "sys", "pathlib", "subprocess", "socket", "ctypes", "mmap"})
|
||||
transaction = data["transaction"]
|
||||
assert transaction["partial_mapping_retained_on_failure"] is False
|
||||
assert transaction["deadline_preempts_operation_atomically"] is True
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["target_mapping_allowed"] is False
|
||||
assert data["decision"]["device_action_allowed"] is False
|
||||
print("Phase-1.0AI mapping transaction validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AJ external source bindings and closed boundary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def commit(root: Path) -> str:
|
||||
return subprocess.run(
|
||||
["git", "rev-parse", "HEAD"], cwd=root, check=True,
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
parser.add_argument("--shsrv-v07-root", type=Path, required=True)
|
||||
parser.add_argument("--hardened-elfldr-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
data = json.loads((args.root / "manifests/retroarch/phase-1.0aj-primitive-audit.json").read_text(encoding="utf-8"))
|
||||
bindings = data["source_bindings"]
|
||||
assert sha256(args.root / "manifests/retroarch/phase-1.0ai-mapping-model.json") == bindings["phase10ai_manifest_sha256"]
|
||||
assert commit(args.shsrv_v07_root) == bindings["shsrv_v07_commit"]
|
||||
assert sha256(args.shsrv_v07_root / "elfldr.c") == bindings["shsrv_v07_elfldr_sha256"]
|
||||
assert sha256(args.shsrv_v07_root / "pt.c") == bindings["shsrv_v07_pt_sha256"]
|
||||
assert commit(args.hardened_elfldr_root) == bindings["hardened_elfldr_commit"]
|
||||
for key, relative in (
|
||||
("controlled_runtime_c_sha256", "controlled_runtime.c"),
|
||||
("controlled_runtime_h_sha256", "controlled_runtime.h"),
|
||||
("hardened_pt_sha256", "pt.c"),
|
||||
("ps5_controlled_sha256", "ps5_controlled.c"),
|
||||
):
|
||||
assert sha256(args.hardened_elfldr_root / relative) == bindings[key]
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["target_implementation_allowed"] is False
|
||||
assert data["decision"]["device_action_allowed"] is False
|
||||
print("Phase-1.0AJ primitive source audit validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AK byte bindings and capability-free model."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def digest(path: Path) -> tuple[int, str]:
|
||||
data = path.read_bytes()
|
||||
return len(data), hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
root = parser.parse_args().root
|
||||
data = json.loads((root / "manifests/retroarch/phase-1.0ak-hybrid-composition.json").read_text(encoding="utf-8"))
|
||||
bindings = data["source_bindings"]
|
||||
assert hashlib.sha256((root / "manifests/retroarch/phase-1.0aj-primitive-audit.json").read_bytes()).hexdigest() == bindings["phase10aj_manifest_sha256"]
|
||||
for prefix, relative in (("model", "tools/phase10ak_hybrid_composition.py"),
|
||||
("tests", "tests/test_phase10ak_hybrid_composition.py")):
|
||||
size, sha256 = digest(root / relative)
|
||||
assert size == bindings[f"{prefix}_size"]
|
||||
assert sha256 == bindings[f"{prefix}_sha256"]
|
||||
tree = ast.parse((root / "tools/phase10ak_hybrid_composition.py").read_text(encoding="utf-8"))
|
||||
imports = {alias.name.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Import) for alias in node.names}
|
||||
imports.update(node.module.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.ImportFrom) and node.module)
|
||||
assert not imports.intersection({"os", "sys", "pathlib", "subprocess", "socket", "ctypes", "mmap", "time"})
|
||||
ownership = data["ownership"]
|
||||
assert ownership["cleanup_failure_requires_child_termination"] is True
|
||||
assert ownership["failed_child_termination_is_hard_error"] is True
|
||||
assert ownership["partial_success_allowed"] is False
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["target_implementation_allowed"] is False
|
||||
print("Phase-1.0AK hybrid composition validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AL against the exact pinned SDK source."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def digest(path: Path) -> tuple[int, str]:
|
||||
data = path.read_bytes()
|
||||
return len(data), hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
parser.add_argument("--sdk-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
data = json.loads((args.root / "manifests/retroarch/phase-1.0al-mdbg-copy-audit.json").read_text(encoding="utf-8"))
|
||||
bindings = data["source_bindings"]
|
||||
assert hashlib.sha256((args.root / "manifests/retroarch/phase-1.0ak-hybrid-composition.json").read_bytes()).hexdigest() == bindings["phase10ak_manifest_sha256"]
|
||||
sdk_commit = subprocess.run(["git", "rev-parse", "HEAD"], cwd=args.sdk_root,
|
||||
check=True, capture_output=True, text=True).stdout.strip()
|
||||
assert sdk_commit == bindings["sdk_commit"]
|
||||
for prefix, relative in (("mdbg_c", "crt/mdbg.c"),
|
||||
("public_header", "include/ps5/mdbg.h")):
|
||||
size, sha256 = digest(args.sdk_root / relative)
|
||||
assert size == bindings[f"{prefix}_size"]
|
||||
assert sha256 == bindings[f"{prefix}_sha256"]
|
||||
findings = data["findings"]
|
||||
assert findings["target_may_be_partially_mutated_on_error"] is True
|
||||
assert findings["return_zero_proves_complete_copy"] is False
|
||||
assert findings["monotonic_deadline_present"] is False
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["direct_sdk_mdbg_copy_reuse_allowed"] is False
|
||||
print("Phase-1.0AL mdbg copy audit validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AM bindings and capability-free boundary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def digest(path: Path) -> tuple[int, str]:
|
||||
data = path.read_bytes()
|
||||
return len(data), hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
root = parser.parse_args().root
|
||||
data = json.loads((root / "manifests/retroarch/phase-1.0am-bounded-copy-model.json").read_text(encoding="utf-8"))
|
||||
bindings = data["source_bindings"]
|
||||
assert hashlib.sha256((root / "manifests/retroarch/phase-1.0al-mdbg-copy-audit.json").read_bytes()).hexdigest() == bindings["phase10al_manifest_sha256"]
|
||||
for prefix, relative in (("model", "tools/phase10am_bounded_copy_model.py"),
|
||||
("tests", "tests/test_phase10am_bounded_copy_model.py")):
|
||||
size, sha256 = digest(root / relative)
|
||||
assert size == bindings[f"{prefix}_size"]
|
||||
assert sha256 == bindings[f"{prefix}_sha256"]
|
||||
tree = ast.parse((root / "tools/phase10am_bounded_copy_model.py").read_text(encoding="utf-8"))
|
||||
imports = {alias.name.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Import) for alias in node.names}
|
||||
imports.update(node.module.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.ImportFrom) and node.module)
|
||||
assert not imports.intersection({"os", "sys", "pathlib", "subprocess", "socket", "ctypes", "mmap", "time"})
|
||||
contract = data["contract"]
|
||||
assert contract["partial_copy_kills_and_reaps_child"] is True
|
||||
assert contract["restore_failure_kills_child_and_terminates_service"] is True
|
||||
assert contract["terminal_cleanup_failure_is_hard_error"] is True
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["target_implementation_allowed"] is False
|
||||
print("Phase-1.0AM bounded copy/restore validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AN against the hardened elfldr source."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def digest(path: Path) -> tuple[int, str]:
|
||||
data = path.read_bytes()
|
||||
return len(data), hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
parser.add_argument("--hardened-elfldr-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
data = json.loads((args.root / "manifests/retroarch/phase-1.0an-service-lifecycle-audit.json").read_text(encoding="utf-8"))
|
||||
bindings = data["source_bindings"]
|
||||
assert hashlib.sha256((args.root / "manifests/retroarch/phase-1.0am-bounded-copy-model.json").read_bytes()).hexdigest() == bindings["phase10am_manifest_sha256"]
|
||||
commit = subprocess.run(["git", "rev-parse", "HEAD"],
|
||||
cwd=args.hardened_elfldr_root, check=True,
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
assert commit == bindings["hardened_elfldr_commit"]
|
||||
for prefix, relative in (("socksrv", "socksrv.c"), ("pt", "pt.c"),
|
||||
("main", "main.c"), ("elfldr", "elfldr.c")):
|
||||
size, sha256 = digest(args.hardened_elfldr_root / relative)
|
||||
assert size == bindings[f"{prefix}_size"]
|
||||
assert sha256 == bindings[f"{prefix}_sha256"]
|
||||
assert data["service_lifecycle"]["request_handler_process_exits_125"] is True
|
||||
assert data["service_lifecycle"]["service_restart_owner_present"] is False
|
||||
assert data["copy_path"]["hard_deadline_or_preemption_present"] is False
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["hardened_pt_copyin_reuse_allowed"] is False
|
||||
print("Phase-1.0AN service lifecycle audit validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AO bindings and capability-free boundary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def digest(path: Path) -> tuple[int, str]:
|
||||
data = path.read_bytes()
|
||||
return len(data), hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
root = parser.parse_args().root
|
||||
data = json.loads((root / "manifests/retroarch/phase-1.0ao-worker-supervisor-model.json").read_text(encoding="utf-8"))
|
||||
bindings = data["source_bindings"]
|
||||
assert hashlib.sha256((root / "manifests/retroarch/phase-1.0an-service-lifecycle-audit.json").read_bytes()).hexdigest() == bindings["phase10an_manifest_sha256"]
|
||||
for prefix, relative in (("model", "tools/phase10ao_worker_supervisor_model.py"),
|
||||
("tests", "tests/test_phase10ao_worker_supervisor_model.py")):
|
||||
size, sha256 = digest(root / relative)
|
||||
assert size == bindings[f"{prefix}_size"]
|
||||
assert sha256 == bindings[f"{prefix}_sha256"]
|
||||
tree = ast.parse((root / "tools/phase10ao_worker_supervisor_model.py").read_text(encoding="utf-8"))
|
||||
imports = {alias.name.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Import) for alias in node.names}
|
||||
imports.update(node.module.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.ImportFrom) and node.module)
|
||||
assert not imports.intersection({"os", "sys", "pathlib", "subprocess", "socket", "ctypes", "signal", "time"})
|
||||
architecture = data["architecture"]
|
||||
assert architecture["automatic_restart"] is False
|
||||
assert architecture["retry"] is False
|
||||
assert architecture["terminal_cleanup_failure_is_hard_error"] is True
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["target_architecture_feasible"] is False
|
||||
print("Phase-1.0AO worker supervisor validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AP against current official Git objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def git(repo: Path, *args: str) -> str:
|
||||
return subprocess.run(["git", *args], cwd=repo, check=True,
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
|
||||
|
||||
def check_blob(repo: Path, commit: str, path: str,
|
||||
expected_blob: str, expected_size: int) -> None:
|
||||
spec = f"{commit}:{path}"
|
||||
assert git(repo, "rev-parse", spec) == expected_blob
|
||||
assert int(git(repo, "cat-file", "-s", spec)) == expected_size
|
||||
|
||||
|
||||
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("--shsrv-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
data = json.loads((args.root / "manifests/retroarch/phase-1.0ap-worker-feasibility-audit.json").read_text(encoding="utf-8"))
|
||||
bindings = data["source_bindings"]
|
||||
assert hashlib.sha256((args.root / "manifests/retroarch/phase-1.0ao-worker-supervisor-model.json").read_bytes()).hexdigest() == bindings["phase10ao_manifest_sha256"]
|
||||
check_blob(args.sdk_root, bindings["sdk_commit"], "include/freebsd/unistd.h",
|
||||
bindings["sdk_unistd_blob"], bindings["sdk_unistd_size"])
|
||||
check_blob(args.sdk_root, bindings["sdk_commit"], "crt/syscall.h",
|
||||
bindings["sdk_syscall_blob"], bindings["sdk_syscall_size"])
|
||||
for prefix, path in (("shsrv_builtin", "builtin.c"),
|
||||
("shsrv_elfldr", "elfldr.c"),
|
||||
("shsrv_shell", "sh.c"), ("shsrv_pt", "pt.c")):
|
||||
check_blob(args.shsrv_root, bindings["shsrv_commit"], path,
|
||||
bindings[f"{prefix}_blob"], bindings[f"{prefix}_size"])
|
||||
assert data["worker_creation"]["classification"] == "STRONG_SOURCE_CANDIDATE_NOT_RUNTIME_PROOF"
|
||||
assert data["preemption"]["waitpid_calls_are_bounded"] is False
|
||||
assert data["result_channel"]["fixed_size_worker_result_record_present"] is False
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["target_worker_architecture_feasible"] is False
|
||||
print("Phase-1.0AP worker feasibility audit validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AQ byte bindings and transport-free boundary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def digest(path: Path) -> tuple[int, str]:
|
||||
data = path.read_bytes()
|
||||
return len(data), hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
root = parser.parse_args().root
|
||||
data = json.loads((root / "manifests/retroarch/phase-1.0aq-worker-result-record.json").read_text(encoding="utf-8"))
|
||||
bindings = data["source_bindings"]
|
||||
assert hashlib.sha256((root / "manifests/retroarch/phase-1.0ap-worker-feasibility-audit.json").read_bytes()).hexdigest() == bindings["phase10ap_manifest_sha256"]
|
||||
for prefix, relative in (("model", "tools/phase10aq_worker_result_record.py"),
|
||||
("tests", "tests/test_phase10aq_worker_result_record.py")):
|
||||
size, sha256 = digest(root / relative)
|
||||
assert size == bindings[f"{prefix}_size"]
|
||||
assert sha256 == bindings[f"{prefix}_sha256"]
|
||||
tree = ast.parse((root / "tools/phase10aq_worker_result_record.py").read_text(encoding="utf-8"))
|
||||
imports = {alias.name.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Import) for alias in node.names}
|
||||
imports.update(node.module.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.ImportFrom) and node.module)
|
||||
assert not imports.intersection({"os", "sys", "pathlib", "subprocess", "socket", "ctypes", "secrets", "random", "time"})
|
||||
assert data["record"]["size"] == 128
|
||||
assert data["identity"]["pid_alone_is_identity"] is False
|
||||
assert data["result"]["digest_is_authentication"] is False
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["transport_implementation_allowed"] is False
|
||||
print("Phase-1.0AQ worker result record validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AR bindings and absence of live channel capability."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def digest(path: Path) -> tuple[int, str]:
|
||||
data = path.read_bytes()
|
||||
return len(data), hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
root = parser.parse_args().root
|
||||
data = json.loads((root / "manifests/retroarch/phase-1.0ar-result-channel-model.json").read_text(encoding="utf-8"))
|
||||
bindings = data["source_bindings"]
|
||||
assert hashlib.sha256((root / "manifests/retroarch/phase-1.0aq-worker-result-record.json").read_bytes()).hexdigest() == bindings["phase10aq_manifest_sha256"]
|
||||
for prefix, relative in (("model", "tools/phase10ar_result_channel_model.py"),
|
||||
("tests", "tests/test_phase10ar_result_channel_model.py")):
|
||||
size, sha256 = digest(root / relative)
|
||||
assert size == bindings[f"{prefix}_size"]
|
||||
assert sha256 == bindings[f"{prefix}_sha256"]
|
||||
tree = ast.parse((root / "tools/phase10ar_result_channel_model.py").read_text(encoding="utf-8"))
|
||||
imports = {alias.name.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Import) for alias in node.names}
|
||||
imports.update(node.module.split(".")[0] for node in ast.walk(tree)
|
||||
if isinstance(node, ast.ImportFrom) and node.module)
|
||||
assert not imports.intersection({"os", "sys", "pathlib", "subprocess", "socket", "selectors", "select", "ctypes", "time"})
|
||||
channel = data["channel"]
|
||||
assert channel["eof_is_success"] is False
|
||||
assert channel["record_completion_is_success_boundary"] is True
|
||||
assert channel["live_pipe_present"] is False
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["live_channel_implementation_allowed"] is False
|
||||
print("Phase-1.0AR result channel validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AS against exact current upstream Git objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def git(repo: Path, *args: str) -> str:
|
||||
return subprocess.run(["git", *args], cwd=repo, check=True,
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
|
||||
|
||||
def check(repo: Path, commit: str, path: str, blob: str, size: int) -> None:
|
||||
spec = f"{commit}:{path}"
|
||||
assert git(repo, "rev-parse", spec) == blob
|
||||
assert int(git(repo, "cat-file", "-s", spec)) == size
|
||||
|
||||
|
||||
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("--shsrv-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
data = json.loads((args.root / "manifests/retroarch/phase-1.0as-channel-primitive-audit.json").read_text(encoding="utf-8"))
|
||||
b = data["source_bindings"]
|
||||
assert hashlib.sha256((args.root / "manifests/retroarch/phase-1.0ar-result-channel-model.json").read_bytes()).hexdigest() == b["phase10ar_manifest_sha256"]
|
||||
for prefix, path in (("sdk_sys_unistd", "include/freebsd/sys/unistd.h"),
|
||||
("sdk_unistd", "include/freebsd/unistd.h"),
|
||||
("sdk_poll", "include/freebsd/sys/poll.h"),
|
||||
("sdk_time", "include/freebsd/sys/time.h")):
|
||||
check(args.sdk_root, b["sdk_commit"], path,
|
||||
b[f"{prefix}_blob"], b[f"{prefix}_size"])
|
||||
for prefix, path in (("shsrv_shell", "sh.c"),
|
||||
("shsrv_builtin", "builtin.c")):
|
||||
check(args.shsrv_root, b["shsrv_commit"], path,
|
||||
b[f"{prefix}_blob"], b[f"{prefix}_size"])
|
||||
assert data["fd_ownership"]["official_shsrv_worker_uses_rfcfdg"] is True
|
||||
assert data["fd_ownership"]["result_fd_inherited_by_worker"] is False
|
||||
assert data["deadline_and_cleanup"]["absolute_monotonic_deadline_present"] is False
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["live_channel_architecture_feasible"] is False
|
||||
print("Phase-1.0AS channel primitive audit validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate exact Phase-1.0AT offline model bindings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def digest(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
root = args.root
|
||||
data = json.loads((root / "manifests/retroarch/phase-1.0at-fd-deadline-model.json").read_text(encoding="utf-8"))
|
||||
bindings = data["source_bindings"]
|
||||
assert digest(root / "manifests/retroarch/phase-1.0as-channel-primitive-audit.json") == bindings["phase10as_manifest_sha256"]
|
||||
assert digest(root / "tools/phase10at_fd_deadline_model.py") == bindings["model_sha256"]
|
||||
assert digest(root / "tests/test_phase10at_fd_deadline_model.py") == bindings["failure_tests_sha256"]
|
||||
assert data["model_boundary"]["fake_facade_only"] is True
|
||||
assert data["ownership_contract"]["all_acquired_ends_closed_on_failure"] is True
|
||||
assert data["deadline_contract"]["trailing_read_event_fails"] is True
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["live_channel_architecture_feasible"] is False
|
||||
print("Phase-1.0AT FD/deadline model validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AU against exact official Git objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def git(repo: Path, *args: str) -> str:
|
||||
return subprocess.run(["git", *args], cwd=repo, check=True,
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
|
||||
|
||||
def check(repo: Path, commit: str, path: str, blob: str, size: int) -> None:
|
||||
spec = f"{commit}:{path}"
|
||||
assert git(repo, "rev-parse", spec) == blob
|
||||
assert int(git(repo, "cat-file", "-s", spec)) == size
|
||||
|
||||
|
||||
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("--shsrv-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
path = args.root / "manifests/retroarch/phase-1.0au-live-channel-feasibility.json"
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
binding = data["source_bindings"]
|
||||
at_path = args.root / "manifests/retroarch/phase-1.0at-fd-deadline-model.json"
|
||||
assert hashlib.sha256(at_path.read_bytes()).hexdigest() == binding["phase10at_manifest_sha256"]
|
||||
sdk_paths = {
|
||||
"sdk_sys_unistd": "include/freebsd/sys/unistd.h",
|
||||
"sdk_unistd": "include/freebsd/unistd.h",
|
||||
"sdk_sys_fcntl": "include/freebsd/sys/fcntl.h",
|
||||
"sdk_time": "include/freebsd/time.h",
|
||||
"sdk_wait": "include/freebsd/sys/wait.h",
|
||||
"sdk_signal": "include/freebsd/signal.h",
|
||||
}
|
||||
for prefix, source in sdk_paths.items():
|
||||
check(args.sdk_root, binding["sdk_commit"], source,
|
||||
binding[f"{prefix}_blob"], binding[f"{prefix}_size"])
|
||||
shsrv_paths = {"shsrv_builtin": "builtin.c", "shsrv_shell": "sh.c",
|
||||
"shsrv_service": "shsrv.c"}
|
||||
for prefix, source in shsrv_paths.items():
|
||||
check(args.shsrv_root, binding["shsrv_commit"], source,
|
||||
binding[f"{prefix}_blob"], binding[f"{prefix}_size"])
|
||||
assert data["public_source_contracts"]["source_level_design_inputs_complete"] is True
|
||||
assert data["official_composition_audit"]["worker_uses_rffdg"] is False
|
||||
assert data["official_composition_audit"]["worker_uses_rfcfdg_close_all"] is True
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["live_result_channel_implementation_allowed"] is False
|
||||
print("Phase-1.0AU live-channel feasibility validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate exact Phase-1.0AV offline canary bindings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def digest(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
root = args.root
|
||||
path = root / "manifests/retroarch/phase-1.0av-launch-context-canary-contract.json"
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
binding = data["source_bindings"]
|
||||
assert digest(root / "manifests/retroarch/phase-1.0au-live-channel-feasibility.json") == binding["phase10au_manifest_sha256"]
|
||||
assert digest(root / "tools/phase10av_launch_context_canary.py") == binding["contract_sha256"]
|
||||
assert digest(root / "tests/test_phase10av_launch_context_canary.py") == binding["tests_sha256"]
|
||||
assert data["pair_contract"]["same_payload_sha256_required"] is True
|
||||
assert data["result_contract"]["distinct_terminal_after_d04_required"] is True
|
||||
assert data["interpretation_limits"]["submit_zero_means_visible_flip"] is False
|
||||
assert not any(data["authorizations"].values())
|
||||
assert not any(value is not None for key, value in data["tracked_state"].items()
|
||||
if key.endswith("sha256") or key in {"raw_run_id", "bigapp_run_id",
|
||||
"target_address", "target_port"})
|
||||
assert data["decision"]["causal_hardware_comparison_ready"] is False
|
||||
print("Phase-1.0AV launch-context canary validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0AW against exact RetroArch and shsrv Git objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def git(repo: Path, *args: str) -> str:
|
||||
return subprocess.run(["git", *args], cwd=repo, check=True,
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
|
||||
|
||||
def check(repo: Path, commit: str, path: str, blob: str, size: int) -> None:
|
||||
spec = f"{commit}:{path}"
|
||||
assert git(repo, "rev-parse", spec) == blob
|
||||
assert int(git(repo, "cat-file", "-s", spec)) == size
|
||||
|
||||
|
||||
def show(repo: Path, commit: str, path: str) -> str:
|
||||
return git(repo, "show", f"{commit}:{path}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
parser.add_argument("--retroarch-root", type=Path, required=True)
|
||||
parser.add_argument("--shsrv-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
root = args.root
|
||||
path = root / "manifests/retroarch/phase-1.0aw-canary-source-delta-audit.json"
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
b = data["source_bindings"]
|
||||
av = root / "manifests/retroarch/phase-1.0av-launch-context-canary-contract.json"
|
||||
assert hashlib.sha256(av.read_bytes()).hexdigest() == b["phase10av_manifest_sha256"]
|
||||
retroarch_paths = {
|
||||
"retroarch_c": "retroarch.c", "sdl2_gfx": "gfx/drivers/sdl2_gfx.c",
|
||||
"platform_smoke_c": "frontend/drivers/platform_ps5_smoke.c",
|
||||
"platform_smoke_h": "frontend/drivers/platform_ps5_smoke.h",
|
||||
"diag_c": "pkg/ps5/chimera_ps5_diag.c",
|
||||
"diag_h": "pkg/ps5/chimera_ps5_diag.h",
|
||||
"stream_c": "pkg/ps5/chimera_ps5_diag_stream.c",
|
||||
"stream_h": "pkg/ps5/chimera_ps5_diag_stream.h",
|
||||
"makefile": "Makefile.ps5",
|
||||
"sdl_hardening_patch": "pkg/ps5/sdl2-ps5-smoke-hardening.patch",
|
||||
}
|
||||
for prefix, source in retroarch_paths.items():
|
||||
check(args.retroarch_root, b["retroarch_commit"], source,
|
||||
b[f"{prefix}_blob"], b[f"{prefix}_size"])
|
||||
shsrv_paths = {"v07_hbldr": "bundles/hbldr/main.c",
|
||||
"v07_elfldr": "elfldr.c", "v07_shell": "sh.c",
|
||||
"v07_service": "shsrv.c"}
|
||||
for prefix, source in shsrv_paths.items():
|
||||
check(args.shsrv_root, b["shsrv_v07_commit"], source,
|
||||
b[f"{prefix}_blob"], b[f"{prefix}_size"])
|
||||
retroarch_c = show(args.retroarch_root, b["retroarch_commit"], "retroarch.c")
|
||||
diag_c = show(args.retroarch_root, b["retroarch_commit"], "pkg/ps5/chimera_ps5_diag.c")
|
||||
hbldr = show(args.shsrv_root, b["shsrv_v07_commit"], "bundles/hbldr/main.c")
|
||||
elfldr = show(args.shsrv_root, b["shsrv_v07_commit"], "elfldr.c")
|
||||
assert "main_exit(data);" in retroarch_c and "_Exit(result);" in retroarch_c
|
||||
assert "CHIMERA_PS5_DIAG_D12" in diag_c and "CHIMERA_PS5_DIAG_FRAME_TERMINAL" in diag_c
|
||||
assert "elfldr_exec(STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO" in hbldr
|
||||
assert "stdout_fd = pt_rdup(pid, getpid(), stdout_fd);" in elfldr
|
||||
assert "while(1)" in hbldr and "sceSystemServiceKillApp" in hbldr
|
||||
assert data["historical_payload"]["reusable_as_av_canary"] is False
|
||||
assert data["required_payload_source_delta"]["new_cleanup_failure_counter_required"] is True
|
||||
assert data["bigapp_result_path"]["live_result_path_proven"] is False
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["target_artifact_build_allowed"] is False
|
||||
print("Phase-1.0AW canary source-delta audit validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate exact Phase-1.0AX host reference bindings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def digest(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
root = args.root
|
||||
path = root / "manifests/retroarch/phase-1.0ax-canary-protocol-model.json"
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
binding = data["source_bindings"]
|
||||
assert digest(root / "manifests/retroarch/phase-1.0aw-canary-source-delta-audit.json") == binding["phase10aw_manifest_sha256"]
|
||||
assert digest(root / "tools/phase10ax_canary_protocol_model.py") == binding["model_sha256"]
|
||||
assert digest(root / "tests/test_phase10ax_canary_protocol_model.py") == binding["tests_sha256"]
|
||||
assert data["frame_contract"]["only_d14_terminal"] is True
|
||||
assert data["frame_contract"]["d12_terminal_forbidden"] is True
|
||||
assert data["cleanup_terminal_contract"]["cleanup_failure_count_zero_required"] is True
|
||||
assert data["trace_contract"]["host_trace_means_firmware_behavior"] is False
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["target_source_matches_model"] is False
|
||||
print("Phase-1.0AX canary protocol model validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate the exact Phase-1.0AY private source base selection."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def git(repo: Path, *args: str) -> str:
|
||||
return subprocess.run(["git", *args], cwd=repo, check=True,
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
|
||||
|
||||
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()
|
||||
root = args.root
|
||||
path = root / "manifests/retroarch/phase-1.0ay-target-source-base.json"
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
b = data["source_bindings"]
|
||||
ax = root / "manifests/retroarch/phase-1.0ax-canary-protocol-model.json"
|
||||
assert hashlib.sha256(ax.read_bytes()).hexdigest() == b["phase10ax_manifest_sha256"]
|
||||
assert git(args.retroarch_root, "rev-parse", b["selected_remote_ref"]) == b["selected_retroarch_commit"]
|
||||
assert git(args.retroarch_root, "merge-base", b["phase10m_source_commit"],
|
||||
b["selected_retroarch_commit"]) == b["phase10m_source_commit"]
|
||||
paths = {
|
||||
"retroarch_c": "retroarch.c", "sdl2_gfx": "gfx/drivers/sdl2_gfx.c",
|
||||
"platform_smoke_c": "frontend/drivers/platform_ps5_smoke.c",
|
||||
"platform_smoke_h": "frontend/drivers/platform_ps5_smoke.h",
|
||||
"diag_c": "pkg/ps5/chimera_ps5_diag.c",
|
||||
"diag_h": "pkg/ps5/chimera_ps5_diag.h",
|
||||
"stream_c": "pkg/ps5/chimera_ps5_diag_stream.c",
|
||||
"stream_h": "pkg/ps5/chimera_ps5_diag_stream.h",
|
||||
"makefile": "Makefile.ps5",
|
||||
}
|
||||
for prefix, source in paths.items():
|
||||
assert git(args.retroarch_root, "rev-parse",
|
||||
f'{b["selected_retroarch_commit"]}:{source}') == b[f"{prefix}_blob"]
|
||||
assert data["base_selection"]["existing_runner_must_remain_inactive"] is True
|
||||
assert data["permitted_patch_scope"]["target_profile"] is False
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["target_artifact_build_allowed"] is False
|
||||
print("Phase-1.0AY target-source base validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate the local, host-only Phase-1.0AZ RetroArch source commit."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def git(repo: Path, *args: str) -> str:
|
||||
return subprocess.run(["git", *args], cwd=repo, check=True,
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
|
||||
|
||||
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()
|
||||
path = args.root / "manifests/retroarch/phase-1.0az-host-av-source.json"
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
binding = data["source_bindings"]
|
||||
ay = args.root / "manifests/retroarch/phase-1.0ay-target-source-base.json"
|
||||
assert hashlib.sha256(ay.read_bytes()).hexdigest() == binding["phase10ay_manifest_sha256"]
|
||||
commit = binding["retroarch_local_commit"]
|
||||
assert git(args.retroarch_root, "rev-parse", "HEAD") == commit
|
||||
assert git(args.retroarch_root, "rev-parse", binding["verified_remote_ref"]) == commit
|
||||
assert git(args.retroarch_root, "rev-parse", f"{commit}^") == binding["retroarch_base_commit"]
|
||||
expected = {
|
||||
"Makefile.ps5", "docs/ps5-phase10az-host-av-source.md",
|
||||
"frontend/drivers/platform_ps5_smoke.c",
|
||||
"frontend/drivers/platform_ps5_smoke.h", "pkg/ps5/chimera_ps5_diag.c",
|
||||
"pkg/ps5/chimera_ps5_diag.h", "pkg/ps5/chimera_ps5_diag_stream.c",
|
||||
"tests/chimera_ps5_av_diag_test.c",
|
||||
}
|
||||
assert set(git(args.retroarch_root, "show", "--format=", "--name-only", commit).splitlines()) == expected
|
||||
blobs = {
|
||||
"makefile": "Makefile.ps5",
|
||||
"platform_smoke_c": "frontend/drivers/platform_ps5_smoke.c",
|
||||
"platform_smoke_h": "frontend/drivers/platform_ps5_smoke.h",
|
||||
"diag_c": "pkg/ps5/chimera_ps5_diag.c",
|
||||
"diag_h": "pkg/ps5/chimera_ps5_diag.h",
|
||||
"stream_c": "pkg/ps5/chimera_ps5_diag_stream.c",
|
||||
"host_test": "tests/chimera_ps5_av_diag_test.c",
|
||||
"source_doc": "docs/ps5-phase10az-host-av-source.md",
|
||||
}
|
||||
for key, source in blobs.items():
|
||||
assert git(args.retroarch_root, "rev-parse", f"{commit}:{source}") == binding[f"{key}_blob"]
|
||||
makefile = git(args.retroarch_root, "show", f"{commit}:Makefile.ps5")
|
||||
assert makefile.count("-DCHIMERA_PS5_AV_DIAG=1") == 1
|
||||
assert "PS5_PROFILE=av" not in makefile and "retroarch_ps5_av" not in makefile
|
||||
assert git(args.retroarch_root, "diff", "--quiet", f'{binding["retroarch_base_commit"]}', commit, "--", "retroarch.c") == ""
|
||||
assert binding["remote_push_verified"] is True
|
||||
assert not any(data["authorizations"].values())
|
||||
assert data["decision"]["target_profile_reassessment_allowed"] is True
|
||||
assert data["decision"]["target_artifact_build_allowed"] is False
|
||||
print("Phase-1.0AZ host-only source validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,558 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0B bounded RetroArch smoke evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
PHASE = "PHASE_1_0B_FIRST_DEVICE_SMOKE_CANDIDATE_HARDENING"
|
||||
STATUS = "RETROARCH_PS5_DEVICE_SMOKE_CANDIDATE_BUILT_WITH_DECLARED_RISKS"
|
||||
GFX_BRANCH = "codex/chimera-gfx-phase10b-device-smoke-hardening"
|
||||
RETROARCH_BRANCH = "codex/ps5-device-smoke-hardening"
|
||||
RETROARCH_BASE = "ca1b45680577befc743e1c92fa40687e1b1745e7"
|
||||
RETROARCH_RELEASE = "69a4f0ea1e8aaf442ae4858f2e7f2b31a1776576"
|
||||
RETROARCH_TREE = "33babf9eb7699b5d571a3063ea21c3e488c159fe"
|
||||
SDK_COMMIT = "d2e2e585740362976a39fdd5ccf390f199a7bc37"
|
||||
PACBREW_COMMIT = "c2abcfcb60f569128abd0e8e70ad03a67bee5ea7"
|
||||
SDL_COMMIT = "0baf4ac49382b537ba449901b5b6d0d189bb1fbb"
|
||||
DENYLIST_SHA256 = "e9603b0e3792781ad5b511afb22ef61e3d4fd4c5a16bf928f8609193bcd97783"
|
||||
|
||||
DEVICE_AUTHORIZATION_FIELDS = (
|
||||
"ps5_connection_authorized",
|
||||
"device_transfer_authorized",
|
||||
"device_execution_authorized",
|
||||
"installation_authorized",
|
||||
"lifecycle_authorized",
|
||||
"autoload_authorized",
|
||||
"device_write_authorized",
|
||||
"automatic_retry",
|
||||
)
|
||||
DEVICE_ACTION_FIELDS = (
|
||||
"ps5_connected",
|
||||
"device_request_performed",
|
||||
"files_transferred",
|
||||
"device_write_performed",
|
||||
"target_execution_performed",
|
||||
"transfer_package_created",
|
||||
"execution_package_created",
|
||||
"installation_package_created",
|
||||
)
|
||||
DISABLED_FEATURES = {
|
||||
"networking",
|
||||
"online_updater",
|
||||
"dynamic_cores",
|
||||
"content_browser_data",
|
||||
"playlists",
|
||||
"history",
|
||||
"screenshots",
|
||||
"recording",
|
||||
"achievements",
|
||||
"shaders",
|
||||
"databases",
|
||||
"compressed_content",
|
||||
"sram",
|
||||
"savestates",
|
||||
"config_save",
|
||||
"remap_save",
|
||||
"autoconfig_save",
|
||||
"log_file",
|
||||
"temporary_files",
|
||||
"keyboard_ime",
|
||||
"haptics",
|
||||
"rumble",
|
||||
"lightbar",
|
||||
"gnm",
|
||||
"retroarch_dynamic_core_loading",
|
||||
"installation",
|
||||
"payload_launch",
|
||||
"autoload",
|
||||
}
|
||||
FORBIDDEN_IMPORT_MARKERS = (
|
||||
"scenet",
|
||||
"scehttp",
|
||||
"scessl",
|
||||
"scegnm",
|
||||
"scekernelloadstartmodule",
|
||||
"scepadsetvibration",
|
||||
"scepadsetlightbar",
|
||||
"sceime",
|
||||
"scekeyboard",
|
||||
"dlopen",
|
||||
"dlsym",
|
||||
"socket",
|
||||
"connect",
|
||||
"listen",
|
||||
"accept",
|
||||
"send",
|
||||
"recv",
|
||||
)
|
||||
REQUIRED_SYMBOLS = {
|
||||
"rarch_main",
|
||||
"retroarch_main_init",
|
||||
"runloop_iterate",
|
||||
"retro_init",
|
||||
"retro_deinit",
|
||||
"retro_run",
|
||||
"frontend_ctx_ps5",
|
||||
"chimera_ps5_smoke_tick",
|
||||
"chimera_ps5_smoke_block_write",
|
||||
"__wrap_open",
|
||||
"__wrap_fopen",
|
||||
"__wrap_fwrite",
|
||||
}
|
||||
DELIVERABLES = (
|
||||
"docs/retroarch/phase-1.0b-smoke-candidate-design.md",
|
||||
"docs/retroarch/phase-1.0b-runtime-and-exit-contract.md",
|
||||
"docs/retroarch/phase-1.0b-persistent-write-audit.md",
|
||||
"docs/retroarch/phase-1.0b-linker-and-wx-analysis.md",
|
||||
"docs/retroarch/phase-1.0b-device-risk-assessment.md",
|
||||
"docs/retroarch/phase-1.0b-proposed-one-shot-test.md",
|
||||
"docs/approvals/phase-1.0b-device-smoke-template.md",
|
||||
"manifests/retroarch/phase-1.0b-build.json",
|
||||
"manifests/retroarch/phase-1.0b-artifact.json",
|
||||
"manifests/retroarch/phase-1.0b-runtime-contract.json",
|
||||
"tests/test_retroarch_phase10b.py",
|
||||
"tools/validate_retroarch_phase10b.py",
|
||||
"packaging/retroarch/phase10b/SHA256SUMS.txt",
|
||||
)
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
value = json.load(stream)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{path} is not a JSON object")
|
||||
return value
|
||||
|
||||
|
||||
def sha256_file(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 profile_is_closed(profile: dict[str, Any]) -> bool:
|
||||
return (
|
||||
DISABLED_FEATURES.issubset(set(profile.get("disabled_features", [])))
|
||||
and profile.get("persistent_writes_allowed") is False
|
||||
and profile.get("content_required") is False
|
||||
and profile.get("config_required") is False
|
||||
and profile.get("networking") is False
|
||||
and profile.get("autoload") is False
|
||||
and profile.get("automatic_retry") is False
|
||||
)
|
||||
|
||||
|
||||
def route_is_read_only(route: dict[str, Any]) -> bool:
|
||||
return (
|
||||
route.get("write") is False
|
||||
and route.get("create") is False
|
||||
and route.get("append") is False
|
||||
and route.get("truncate") is False
|
||||
and route.get("rename") is False
|
||||
and route.get("unlink") is False
|
||||
and route.get("mkdir") is False
|
||||
and route.get("retry") is False
|
||||
)
|
||||
|
||||
|
||||
def program_headers_are_wx_closed(headers: list[dict[str, Any]]) -> bool:
|
||||
loads = [header for header in headers if header.get("type") == "LOAD"]
|
||||
return (
|
||||
len(loads) >= 3
|
||||
and any("E" in str(item.get("flags", "")) for item in loads)
|
||||
and all(
|
||||
not (
|
||||
"W" in str(item.get("flags", ""))
|
||||
and "E" in str(item.get("flags", ""))
|
||||
)
|
||||
for item in loads
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def imports_are_closed(imports: list[str]) -> bool:
|
||||
lowered = [item.lower() for item in imports]
|
||||
return not any(
|
||||
marker in item for marker in FORBIDDEN_IMPORT_MARKERS for item in lowered
|
||||
)
|
||||
|
||||
|
||||
def parse_elf_program_headers(path: Path) -> list[dict[str, Any]]:
|
||||
with path.open("rb") as stream:
|
||||
header = stream.read(64)
|
||||
if len(header) != 64 or header[:5] != b"\x7fELF\x02":
|
||||
raise ValueError(f"{path} is not ELF64")
|
||||
byte_order = "<" if header[5] == 1 else ">"
|
||||
phoff = struct.unpack_from(f"{byte_order}Q", header, 32)[0]
|
||||
phentsize = struct.unpack_from(f"{byte_order}H", header, 54)[0]
|
||||
phnum = struct.unpack_from(f"{byte_order}H", header, 56)[0]
|
||||
if phentsize < 56 or phnum > 64:
|
||||
raise ValueError("invalid ELF program-header table")
|
||||
result: list[dict[str, Any]] = []
|
||||
stream.seek(phoff)
|
||||
for _ in range(phnum):
|
||||
raw = stream.read(phentsize)
|
||||
if len(raw) != phentsize:
|
||||
raise ValueError("truncated ELF program-header table")
|
||||
kind, flags = struct.unpack_from(f"{byte_order}II", raw)
|
||||
names = {1: "LOAD", 2: "DYNAMIC"}
|
||||
text = (
|
||||
("R" if flags & 4 else "")
|
||||
+ ("W" if flags & 2 else "")
|
||||
+ ("E" if flags & 1 else "")
|
||||
)
|
||||
result.append(
|
||||
{
|
||||
"type": names.get(kind, f"0x{kind:x}"),
|
||||
"flags": text,
|
||||
"offset": struct.unpack_from(f"{byte_order}Q", raw, 8)[0],
|
||||
"virtual_address": struct.unpack_from(
|
||||
f"{byte_order}Q", raw, 16
|
||||
)[0],
|
||||
"file_size": struct.unpack_from(f"{byte_order}Q", raw, 32)[0],
|
||||
"memory_size": struct.unpack_from(
|
||||
f"{byte_order}Q", raw, 40
|
||||
)[0],
|
||||
"alignment": struct.unpack_from(f"{byte_order}Q", raw, 48)[0],
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def parse_elf_sections(path: Path) -> tuple[bytes, list[tuple[int, ...]]]:
|
||||
data = path.read_bytes()
|
||||
if len(data) < 64 or data[:6] != b"\x7fELF\x02\x01":
|
||||
raise ValueError(f"{path} is not little-endian ELF64")
|
||||
section_offset = struct.unpack_from("<Q", data, 40)[0]
|
||||
section_size, section_count = struct.unpack_from("<HH", data, 58)
|
||||
if section_size < 64 or section_count > 4096:
|
||||
raise ValueError("invalid ELF section-header table")
|
||||
sections: list[tuple[int, ...]] = []
|
||||
for index in range(section_count):
|
||||
offset = section_offset + index * section_size
|
||||
if offset + 64 > len(data):
|
||||
raise ValueError("truncated ELF section-header table")
|
||||
sections.append(struct.unpack_from("<IIQQQQIIQQ", data, offset))
|
||||
return data, sections
|
||||
|
||||
|
||||
def parse_elf_dynamic_symbols(path: Path) -> list[str]:
|
||||
data, sections = parse_elf_sections(path)
|
||||
undefined: set[str] = set()
|
||||
for section in sections:
|
||||
if section[1] != 11: # SHT_DYNSYM
|
||||
continue
|
||||
symbol_offset, symbol_size = section[4], section[5]
|
||||
string_index, symbol_entry_size = section[6], section[9]
|
||||
if string_index >= len(sections) or symbol_entry_size < 24:
|
||||
raise ValueError("invalid ELF dynamic-symbol table")
|
||||
strings_section = sections[string_index]
|
||||
strings = data[
|
||||
strings_section[4] : strings_section[4] + strings_section[5]
|
||||
]
|
||||
for offset in range(
|
||||
symbol_offset, symbol_offset + symbol_size, symbol_entry_size
|
||||
):
|
||||
if offset + 24 > len(data):
|
||||
raise ValueError("truncated ELF dynamic-symbol table")
|
||||
name_offset, _, _, section_index, _, _ = struct.unpack_from(
|
||||
"<IBBHQQ", data, offset
|
||||
)
|
||||
if section_index != 0 or name_offset >= len(strings):
|
||||
continue
|
||||
name_end = strings.find(b"\0", name_offset)
|
||||
if name_end < 0:
|
||||
raise ValueError("unterminated ELF dynamic-symbol name")
|
||||
name = strings[name_offset:name_end].decode("utf-8")
|
||||
if name:
|
||||
undefined.add(name)
|
||||
return sorted(undefined)
|
||||
|
||||
|
||||
def parse_elf_needed(path: Path) -> list[str]:
|
||||
data, sections = parse_elf_sections(path)
|
||||
needed: list[str] = []
|
||||
for section in sections:
|
||||
if section[1] != 6: # SHT_DYNAMIC
|
||||
continue
|
||||
dynamic_offset, dynamic_size = section[4], section[5]
|
||||
string_index, dynamic_entry_size = section[6], section[9]
|
||||
if string_index >= len(sections) or dynamic_entry_size < 16:
|
||||
raise ValueError("invalid ELF dynamic table")
|
||||
strings_section = sections[string_index]
|
||||
strings = data[
|
||||
strings_section[4] : strings_section[4] + strings_section[5]
|
||||
]
|
||||
for offset in range(
|
||||
dynamic_offset, dynamic_offset + dynamic_size, dynamic_entry_size
|
||||
):
|
||||
if offset + 16 > len(data):
|
||||
raise ValueError("truncated ELF dynamic table")
|
||||
tag, value = struct.unpack_from("<QQ", data, offset)
|
||||
if tag != 1: # DT_NEEDED
|
||||
continue
|
||||
if value >= len(strings):
|
||||
raise ValueError("invalid ELF DT_NEEDED string offset")
|
||||
name_end = strings.find(b"\0", value)
|
||||
if name_end < 0:
|
||||
raise ValueError("unterminated ELF DT_NEEDED name")
|
||||
needed.append(strings[value:name_end].decode("utf-8"))
|
||||
return needed
|
||||
|
||||
|
||||
def parse_elf_relocations(path: Path) -> dict[str, Any]:
|
||||
data, sections = parse_elf_sections(path)
|
||||
writable_ranges = [
|
||||
(
|
||||
header["virtual_address"],
|
||||
header["virtual_address"] + header["memory_size"],
|
||||
)
|
||||
for header in parse_elf_program_headers(path)
|
||||
if header["type"] == "LOAD" and "W" in header["flags"]
|
||||
]
|
||||
total = 0
|
||||
relative = 0
|
||||
outside = 0
|
||||
relative_outside = 0
|
||||
by_type: dict[str, int] = {}
|
||||
for section in sections:
|
||||
if section[1] != 4: # SHT_RELA
|
||||
continue
|
||||
relocation_offset, relocation_size = section[4], section[5]
|
||||
relocation_entry_size = section[9]
|
||||
if relocation_entry_size < 24:
|
||||
raise ValueError("invalid ELF relocation table")
|
||||
for offset in range(
|
||||
relocation_offset,
|
||||
relocation_offset + relocation_size,
|
||||
relocation_entry_size,
|
||||
):
|
||||
if offset + 24 > len(data):
|
||||
raise ValueError("truncated ELF relocation table")
|
||||
target, info, _ = struct.unpack_from("<QQq", data, offset)
|
||||
relocation_type = info & 0xFFFFFFFF
|
||||
key = str(relocation_type)
|
||||
by_type[key] = by_type.get(key, 0) + 1
|
||||
total += 1
|
||||
in_writable_load = any(
|
||||
start <= target and target + 8 <= end
|
||||
for start, end in writable_ranges
|
||||
)
|
||||
if not in_writable_load:
|
||||
outside += 1
|
||||
if relocation_type == 8: # R_X86_64_RELATIVE
|
||||
relative += 1
|
||||
if not in_writable_load:
|
||||
relative_outside += 1
|
||||
return {
|
||||
"total": total,
|
||||
"relative": relative,
|
||||
"by_type": by_type,
|
||||
"outside_rw_load": outside,
|
||||
"relative_outside_rw_load": relative_outside,
|
||||
}
|
||||
|
||||
|
||||
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 command failed")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
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:
|
||||
build = load_json(root / "manifests/retroarch/phase-1.0b-build.json")
|
||||
artifact = load_json(root / "manifests/retroarch/phase-1.0b-artifact.json")
|
||||
runtime = load_json(
|
||||
root / "manifests/retroarch/phase-1.0b-runtime-contract.json"
|
||||
)
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
return errors + [str(error)]
|
||||
|
||||
if build.get("phase") != PHASE or build.get("status") != STATUS:
|
||||
errors.append("phase/status mismatch")
|
||||
if build.get("branch") != GFX_BRANCH:
|
||||
errors.append("GFX branch mismatch")
|
||||
for field in DEVICE_AUTHORIZATION_FIELDS:
|
||||
if build.get(field) is not False:
|
||||
errors.append(f"{field} must remain false")
|
||||
for field in DEVICE_ACTION_FIELDS:
|
||||
if build.get(field) is not False:
|
||||
errors.append(f"{field} must remain false")
|
||||
|
||||
source = build.get("sources", {})
|
||||
expected_sources = {
|
||||
"retroarch_release_commit": RETROARCH_RELEASE,
|
||||
"retroarch_release_tree": RETROARCH_TREE,
|
||||
"retroarch_fork_base_commit": RETROARCH_BASE,
|
||||
"ps5_payload_sdk_commit": SDK_COMMIT,
|
||||
"pacbrew_commit": PACBREW_COMMIT,
|
||||
"sdl_commit": SDL_COMMIT,
|
||||
}
|
||||
for field, expected in expected_sources.items():
|
||||
if source.get(field) != expected:
|
||||
errors.append(f"source mismatch: {field}")
|
||||
if (
|
||||
build.get("chimera_retroarch", {}).get("branch") != RETROARCH_BRANCH
|
||||
or len(build.get("chimera_retroarch", {}).get("source_commit", "")) != 40
|
||||
):
|
||||
errors.append("RetroArch fork identity is incomplete")
|
||||
if not profile_is_closed(build.get("profile", {})):
|
||||
errors.append("smoke profile is not closed")
|
||||
|
||||
record = artifact.get("artifact", {})
|
||||
if record.get("name") != "retroarch_ps5_software_smoke.elf":
|
||||
errors.append("artifact label mismatch")
|
||||
if record.get("size", 0) <= 0 or len(record.get("sha256", "")) != 64:
|
||||
errors.append("artifact identity is incomplete")
|
||||
if len(record.get("linker_map_sha256", "")) != 64:
|
||||
errors.append("linker-map identity is incomplete")
|
||||
hashes = record.get("clean_build_sha256", [])
|
||||
if len(hashes) != 2 or len(set(hashes)) != 1 or hashes[0] != record.get("sha256"):
|
||||
errors.append("two clean builds are not byte-identical")
|
||||
if record.get("selected_exit_method") != "PROCESS__EXIT_AFTER_TEARDOWN":
|
||||
errors.append("exit method mismatch")
|
||||
if record.get("entrypoint") is None:
|
||||
errors.append("entrypoint missing")
|
||||
if not program_headers_are_wx_closed(record.get("program_headers", [])):
|
||||
errors.append("program headers are not W^X-closed")
|
||||
if not imports_are_closed(record.get("imports", [])):
|
||||
errors.append("forbidden import present")
|
||||
if not REQUIRED_SYMBOLS.issubset(set(record.get("defined_symbols", []))):
|
||||
errors.append("required real/smoke symbols are incomplete")
|
||||
if record.get("relative_relocations_outside_rw_load") != 0:
|
||||
errors.append("relative relocation target outside RW load")
|
||||
for field in (
|
||||
"persistent_writes_allowed",
|
||||
"content_required",
|
||||
"config_required",
|
||||
"networking",
|
||||
"autoload",
|
||||
"automatic_retry",
|
||||
"installation_eligible",
|
||||
"device_write_eligible",
|
||||
"transfer_eligible",
|
||||
"execution_eligible",
|
||||
"execution_authorized",
|
||||
):
|
||||
if record.get(field) is not False:
|
||||
errors.append(f"artifact {field} must be false")
|
||||
|
||||
if runtime.get("runtime_limit_ms") != 60000:
|
||||
errors.append("runtime limit mismatch")
|
||||
if runtime.get("frame_limit") != 3600:
|
||||
errors.append("frame limit mismatch")
|
||||
if runtime.get("shutdown_hold_ms") != 2000:
|
||||
errors.append("shutdown hold mismatch")
|
||||
if runtime.get("shutdown_request_max") != 1:
|
||||
errors.append("shutdown request is not single-shot")
|
||||
if runtime.get("exit_reachable_call_count") != 1:
|
||||
errors.append("exit is not exactly once reachable")
|
||||
if runtime.get("audio_submit_bounded") is not False:
|
||||
errors.append("blocking AudioOut risk was hidden")
|
||||
if not all(route_is_read_only(route) for route in runtime.get("write_routes", [])):
|
||||
errors.append("write route is not fail-closed")
|
||||
|
||||
denylist = root / "manifests/artifact-denylist.json"
|
||||
if not denylist.is_file() or sha256_file(denylist) != DENYLIST_SHA256:
|
||||
errors.append("permanent denylist changed")
|
||||
for relative in git(root, "ls-files").splitlines():
|
||||
if relative.lower().endswith((".elf", ".self", ".sprx", ".pkg")):
|
||||
errors.append(f"tracked target artifact: {relative}")
|
||||
|
||||
approval = (root / "docs/approvals/phase-1.0b-device-smoke-template.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
for value in (
|
||||
"authorized: false",
|
||||
"transfer_authorized: false",
|
||||
"execution_authorized: false",
|
||||
"installation_authorized: false",
|
||||
"automatic_retry: false",
|
||||
"autoload_authorized: false",
|
||||
):
|
||||
if value not in approval:
|
||||
errors.append(f"approval template missing {value}")
|
||||
|
||||
if retroarch_root is not None:
|
||||
path = retroarch_root / record.get("local_relative_path", "")
|
||||
map_path = retroarch_root / record.get("linker_map_relative_path", "")
|
||||
if not path.is_file():
|
||||
errors.append("local smoke ELF missing")
|
||||
elif path.stat().st_size != record.get("size") or sha256_file(path) != record.get(
|
||||
"sha256"
|
||||
):
|
||||
errors.append("local smoke ELF identity mismatch")
|
||||
else:
|
||||
try:
|
||||
actual_headers = parse_elf_program_headers(path)
|
||||
if not program_headers_are_wx_closed(actual_headers):
|
||||
errors.append("local smoke ELF has a W+X load segment")
|
||||
if actual_headers != record.get("program_headers"):
|
||||
errors.append("local program headers differ from manifest")
|
||||
actual_needed = parse_elf_needed(path)
|
||||
if actual_needed != record.get("dt_needed"):
|
||||
errors.append("local DT_NEEDED differs from manifest")
|
||||
actual_undefined = parse_elf_dynamic_symbols(path)
|
||||
if actual_undefined != record.get("undefined_symbols"):
|
||||
errors.append("local undefined symbols differ from manifest")
|
||||
if not imports_are_closed(actual_undefined):
|
||||
errors.append("local smoke ELF has a forbidden import")
|
||||
actual_relocations = parse_elf_relocations(path)
|
||||
if actual_relocations != record.get("relocation_audit"):
|
||||
errors.append("local relocation audit differs from manifest")
|
||||
except (OSError, ValueError, struct.error) as error:
|
||||
errors.append(str(error))
|
||||
if not map_path.is_file():
|
||||
errors.append("local linker map missing")
|
||||
elif sha256_file(map_path) != record.get("linker_map_sha256"):
|
||||
errors.append("local linker-map identity mismatch")
|
||||
if git(retroarch_root, "rev-parse", "HEAD") != build.get(
|
||||
"chimera_retroarch", {}
|
||||
).get("source_commit"):
|
||||
errors.append("RetroArch HEAD does not match manifest source")
|
||||
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.resolve() if args.retroarch_root else None,
|
||||
)
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(error, file=sys.stderr)
|
||||
return 1
|
||||
print("Phase 1.0B bounded RetroArch smoke evidence validated")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate the Phase-1.0BA callsite and profile-delta audit."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def git(repo: Path, *args: str) -> str:
|
||||
return subprocess.run(["git", *args], cwd=repo, check=True,
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
|
||||
|
||||
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()
|
||||
path = args.root / "manifests/retroarch/phase-1.0ba-target-profile-callsite-audit.json"
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
bind = data["source_bindings"]
|
||||
az = args.root / "manifests/retroarch/phase-1.0az-host-av-source.json"
|
||||
assert hashlib.sha256(az.read_bytes()).hexdigest() == bind["phase10az_manifest_sha256"]
|
||||
commit = bind["retroarch_commit"]
|
||||
paths = {"retroarch_c": "retroarch.c",
|
||||
"platform_ps5_c": "frontend/drivers/platform_ps5.c",
|
||||
"makefile": "Makefile.ps5"}
|
||||
for key, source in paths.items():
|
||||
assert git(args.retroarch_root, "rev-parse", f"{commit}:{source}") == bind[f"{key}_blob"]
|
||||
source = git(args.retroarch_root, "show", f"{commit}:retroarch.c")
|
||||
sequence = "int result = rarch_main(argc, argv, NULL);"
|
||||
sequence += "\n#if defined(CHIMERA_PS5_SMOKE_MODE)"
|
||||
sequence += "\n chimera_ps5_smoke_set_phase(CHIMERA_SMOKE_S15_COMPLETE);\n _Exit(result);"
|
||||
assert sequence in source
|
||||
assert "main_exit(data);\n#endif\n\n return 0;" in source
|
||||
assert not data["authorizations"]["target_build_authorized"]
|
||||
assert data["decision"]["source_patch_allowed"]
|
||||
assert not data["decision"]["cross_build_allowed"]
|
||||
print("Phase-1.0BA callsite/profile audit validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate the remote-bound Phase-1.0BB source profile."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def git(repo: Path, *args: str) -> str:
|
||||
return subprocess.run(["git", *args], cwd=repo, check=True,
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
|
||||
|
||||
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()
|
||||
data = json.loads((args.root / "manifests/retroarch/phase-1.0bb-source-only-launch-canary-profile.json").read_text(encoding="utf-8"))
|
||||
bind = data["source_bindings"]
|
||||
ba = args.root / "manifests/retroarch/phase-1.0ba-target-profile-callsite-audit.json"
|
||||
assert hashlib.sha256(ba.read_bytes()).hexdigest() == bind["phase10ba_manifest_sha256"]
|
||||
commit = bind["retroarch_commit"]
|
||||
assert git(args.retroarch_root, "rev-parse", "HEAD") == commit
|
||||
assert git(args.retroarch_root, "rev-parse", bind["remote_ref"]) == commit
|
||||
assert git(args.retroarch_root, "rev-parse", f"{commit}^") == bind["retroarch_parent_commit"]
|
||||
paths = {"makefile": "Makefile.ps5", "retroarch_c": "retroarch.c",
|
||||
"validator": "tools/validate_ps5_phase10bb.py",
|
||||
"tests": "tests/test_ps5_phase10bb.py",
|
||||
"source_doc": "docs/ps5-phase10bb-launch-canary-profile.md"}
|
||||
for key, source in paths.items():
|
||||
assert git(args.retroarch_root, "rev-parse", f"{commit}:{source}") == bind[f"{key}_blob"]
|
||||
assert data["verification"]["launch_canary_artifact_absent"]
|
||||
assert data["authorizations"]["build_prerequisite_audit_authorized"]
|
||||
assert not data["authorizations"]["target_build_authorized"]
|
||||
assert not data["decision"]["cross_build_allowed"]
|
||||
print("Phase-1.0BB source-profile binding validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate the remote-bound Phase-1.0BD policy commit."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def git(root: Path, *args: str) -> str:
|
||||
return subprocess.run(["git", *args], cwd=root, check=True,
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
|
||||
|
||||
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()
|
||||
data = json.loads((args.root / "manifests/retroarch/phase-1.0bd-dormant-sdl-materializer-policy.json").read_text(encoding="utf-8"))
|
||||
bind = data["source_bindings"]
|
||||
parent = args.root / "manifests/retroarch/phase-1.0bc-cross-build-prerequisite-audit.json"
|
||||
assert hashlib.sha256(parent.read_bytes()).hexdigest() == bind["phase10bc_manifest_sha256"]
|
||||
commit = bind["retroarch_commit"]
|
||||
assert git(args.retroarch_root, "rev-parse", "HEAD") == commit
|
||||
assert git(args.retroarch_root, "rev-parse", bind["remote_ref"]) == commit
|
||||
assert git(args.retroarch_root, "rev-parse", f"{commit}^") == bind["retroarch_parent_commit"]
|
||||
paths = {"makefile": "Makefile.ps5",
|
||||
"policy": "tools/phase10bd_sdl_materializer_policy.py",
|
||||
"validator": "tools/validate_ps5_phase10bd.py",
|
||||
"tests": "tests/test_ps5_phase10bd_materializer_policy.py",
|
||||
"source_doc": "docs/ps5-phase10bd-sdl-materializer-policy.md"}
|
||||
for key, source in paths.items():
|
||||
assert git(args.retroarch_root, "rev-parse", f"{commit}:{source}") == bind[f"{key}_blob"]
|
||||
assert data["authorizations"]["injected_adapter_source_authorized"]
|
||||
assert not data["authorizations"]["live_adapter_authorized"]
|
||||
assert not data["decision"]["materialization_allowed"]
|
||||
print("Phase-1.0BD dormant policy binding validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,326 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate the offline Phase-1.0D diagnostic ladder evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
PHASE = "PHASE_1_0D_LOADER_TO_ENTRY_DIAGNOSIS"
|
||||
STATUS = "RETROARCH_PS5_ENTRY_DIAGNOSTIC_LADDER_BUILT_OFFLINE"
|
||||
RETROARCH_BRANCH = "codex/ps5-loader-entry-diagnosis"
|
||||
RETROARCH_COMMIT = "69b65858ffaee826d70f5c0df61013cd1b0e2048"
|
||||
DENYLIST_SHA256 = "e9603b0e3792781ad5b511afb22ef61e3d4fd4c5a16bf928f8609193bcd97783"
|
||||
CANARY_IMPORTS = {
|
||||
"_Exit",
|
||||
"memset",
|
||||
"nanosleep",
|
||||
"sceKernelSendNotificationRequest",
|
||||
}
|
||||
CANARY_NEEDED = {"libSceLibcInternal.sprx", "libkernel_web.sprx"}
|
||||
REAL_MODEL_ARTIFACTS = {
|
||||
"retroarch_ps5_software_smoke.elf",
|
||||
"chimera_ps5_crt_entry_canary.elf",
|
||||
"retroarch_ps5_early_diag.elf",
|
||||
"chimera-gfx-lifecycle-probe.elf",
|
||||
}
|
||||
AUTHORIZATION_FIELDS = (
|
||||
"ps5_connection_authorized",
|
||||
"device_transfer_authorized",
|
||||
"device_execution_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",
|
||||
)
|
||||
DELIVERABLES = (
|
||||
"docs/retroarch/phase-1.0d-loader-to-entry-analysis.md",
|
||||
"docs/retroarch/phase-1.0d-crt-entry-canary.md",
|
||||
"docs/retroarch/phase-1.0d-early-diagnostic-design.md",
|
||||
"docs/retroarch/phase-1.0d-startup-import-closure.md",
|
||||
"docs/retroarch/phase-1.0d-loader-static-model.md",
|
||||
"docs/retroarch/phase-1.0d-next-device-test-ladder.md",
|
||||
"manifests/retroarch/phase-1.0d-canary-artifact.json",
|
||||
"manifests/retroarch/phase-1.0d-early-diag-artifact.json",
|
||||
"manifests/retroarch/phase-1.0d-loader-model-results.json",
|
||||
"tools/validate_retroarch_phase10d.py",
|
||||
"tests/test_retroarch_phase10d.py",
|
||||
"packaging/retroarch/phase10d/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 sha256_file(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 reproducible_artifact(record: dict[str, Any]) -> bool:
|
||||
artifact = record.get("artifact", {})
|
||||
elf_hashes = artifact.get("clean_build_sha256", [])
|
||||
map_hashes = artifact.get("clean_map_sha256", [])
|
||||
return (
|
||||
artifact.get("size", 0) > 0
|
||||
and len(artifact.get("sha256", "")) == 64
|
||||
and len(elf_hashes) == 2
|
||||
and len(set(elf_hashes)) == 1
|
||||
and elf_hashes[0] == artifact.get("sha256")
|
||||
and len(map_hashes) == 2
|
||||
and len(set(map_hashes)) == 1
|
||||
and map_hashes[0] == artifact.get("linker_map_sha256")
|
||||
)
|
||||
|
||||
|
||||
def wx_closed(headers: list[dict[str, Any]]) -> bool:
|
||||
loads = [item for item in headers if item.get("type") == "LOAD"]
|
||||
return (
|
||||
len(loads) == 3
|
||||
and any("E" in str(item.get("flags", "")) for item in loads)
|
||||
and all(
|
||||
not (
|
||||
"W" in str(item.get("flags", ""))
|
||||
and "E" in str(item.get("flags", ""))
|
||||
)
|
||||
for item in loads
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def canary_is_minimal(record: dict[str, Any]) -> bool:
|
||||
behavior = record.get("behavior", {})
|
||||
elf = record.get("elf", {})
|
||||
return (
|
||||
set(elf.get("undefined_symbols", [])) == CANARY_IMPORTS
|
||||
and set(elf.get("dt_needed", [])) == CANARY_NEEDED
|
||||
and record.get("notification_abi", {}).get("maximum_attempts") == 1
|
||||
and record.get("notification_abi", {}).get("retry") is False
|
||||
and behavior.get("sleep_attempts") == 1
|
||||
and behavior.get("interrupted_sleep_retry") is False
|
||||
and all(
|
||||
behavior.get(field) is False
|
||||
for field in (
|
||||
"retroarch", "sdl", "videoout", "pad", "audioout",
|
||||
"filesystem", "networking", "threads", "autoload", "installation",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def early_ladder_is_closed(record: dict[str, Any]) -> bool:
|
||||
diagnostic = record.get("diagnostic", {})
|
||||
frame = record.get("first_frame", {})
|
||||
policy = record.get("profile_policy", {})
|
||||
return (
|
||||
diagnostic.get("stages") == [f"D{index:02d}" for index in range(13)]
|
||||
and diagnostic.get("notification_maximum_attempts_per_stage") == 1
|
||||
and diagnostic.get("notification_retry") is False
|
||||
and diagnostic.get("notification_failure_blocks_primary_path") is False
|
||||
and frame.get("background") == "MAGENTA"
|
||||
and frame.get("fixed_rectangle") == "WHITE"
|
||||
and frame.get("embedded_pattern") == "BLACK"
|
||||
and frame.get("submit_attempts") == 1
|
||||
and frame.get("retry") is False
|
||||
and frame.get("second_buffer_initialization") is False
|
||||
and policy.get("write_firewall") is True
|
||||
and all(
|
||||
policy.get(field) is False
|
||||
for field in (
|
||||
"filesystem_writes_allowed", "networking", "dynamic_cores",
|
||||
"gnm", "autoload", "installation", "payload_launch",
|
||||
"automatic_retry",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def model_result_set_is_complete(record: dict[str, Any]) -> bool:
|
||||
artifacts = record.get("real_artifacts", [])
|
||||
names = {item.get("name") for item in artifacts}
|
||||
return (
|
||||
names == REAL_MODEL_ARTIFACTS
|
||||
and all(
|
||||
item.get("classification")
|
||||
in {
|
||||
"ACCEPTED_BY_STATIC_MODEL",
|
||||
"REJECTED_BY_STATIC_MODEL",
|
||||
"MODEL_INCOMPLETE",
|
||||
}
|
||||
for item in artifacts
|
||||
)
|
||||
and all(len(item.get("sha256", "")) == 64 for item in artifacts)
|
||||
and record.get("model", {}).get("hardware_evidence") is False
|
||||
)
|
||||
|
||||
|
||||
def sender_trace_is_bounded(trace: dict[str, Any]) -> bool:
|
||||
return (
|
||||
trace.get("connections") == 1
|
||||
and trace.get("sendall_calls") == 1
|
||||
and trace.get("hash_before_connect") is True
|
||||
and trace.get("shutdown_called") is False
|
||||
and trace.get("response_read") is False
|
||||
and trace.get("retry") is False
|
||||
and trace.get("reconnect") is False
|
||||
and trace.get("probe") is False
|
||||
and "REMOTE_EXECUTION" in trace.get("cannot_prove", [])
|
||||
)
|
||||
|
||||
|
||||
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 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:
|
||||
canary = load_json(
|
||||
root / "manifests/retroarch/phase-1.0d-canary-artifact.json"
|
||||
)
|
||||
early = load_json(
|
||||
root / "manifests/retroarch/phase-1.0d-early-diag-artifact.json"
|
||||
)
|
||||
model = load_json(
|
||||
root / "manifests/retroarch/phase-1.0d-loader-model-results.json"
|
||||
)
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
return errors + [str(error)]
|
||||
|
||||
for label, record in (("canary", canary), ("early", early), ("model", model)):
|
||||
if record.get("phase") != PHASE or record.get("status") != STATUS:
|
||||
errors.append(f"{label} phase/status mismatch")
|
||||
if not all_false(record.get("authorizations", {}), AUTHORIZATION_FIELDS):
|
||||
errors.append(f"{label} authorization must remain false")
|
||||
if not all_false(record.get("actions", {}), ACTION_FIELDS):
|
||||
errors.append(f"{label} device action must remain false")
|
||||
|
||||
for label, record in (("canary", canary), ("early", early)):
|
||||
artifact = record.get("artifact", {})
|
||||
elf = record.get("elf", {})
|
||||
if not reproducible_artifact(record):
|
||||
errors.append(f"{label} is not reproducible")
|
||||
if artifact.get("execution_eligible") is not False:
|
||||
errors.append(f"{label} execution eligibility must be false")
|
||||
if elf.get("rwx_load_segment_count") != 0:
|
||||
errors.append(f"{label} RWX count is not zero")
|
||||
if not wx_closed(elf.get("program_headers", [])):
|
||||
errors.append(f"{label} headers are not W^X closed")
|
||||
if elf.get("init_array_size") != 0 or elf.get("fini_array_size") != 0:
|
||||
errors.append(f"{label} constructor arrays are not empty")
|
||||
if elf.get("tls") is not False:
|
||||
errors.append(f"{label} TLS must be absent")
|
||||
if not canary_is_minimal(canary):
|
||||
errors.append("canary closure is not minimal")
|
||||
if not early_ladder_is_closed(early):
|
||||
errors.append("early diagnostic ladder is not closed")
|
||||
if not model_result_set_is_complete(model):
|
||||
errors.append("loader model real-artifact set is incomplete")
|
||||
if not sender_trace_is_bounded(model.get("sender_trace_contract", {})):
|
||||
errors.append("sender trace contract is not bounded")
|
||||
|
||||
negative = model.get("negative_host_cases", [])
|
||||
if len(negative) != 5 or {
|
||||
item.get("classification") for item in negative
|
||||
} != {
|
||||
"ACCEPTED_BY_STATIC_MODEL",
|
||||
"REJECTED_BY_STATIC_MODEL",
|
||||
"MODEL_INCOMPLETE",
|
||||
}:
|
||||
errors.append("negative loader cases are incomplete")
|
||||
|
||||
denylist = root / "manifests/artifact-denylist.json"
|
||||
if not denylist.is_file() or sha256_file(denylist) != DENYLIST_SHA256:
|
||||
errors.append("permanent denylist changed")
|
||||
tracked = git(root, "ls-files").splitlines()
|
||||
for relative in tracked:
|
||||
if relative.lower().endswith((".elf", ".self", ".sprx", ".pkg")):
|
||||
errors.append(f"tracked target artifact: {relative}")
|
||||
forbidden_address = "192.168.10." + "105"
|
||||
for relative in tracked:
|
||||
path = root / relative
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError):
|
||||
continue
|
||||
if forbidden_address in text:
|
||||
errors.append(f"tracked device address in {relative}")
|
||||
|
||||
if retroarch_root is not None:
|
||||
if git(retroarch_root, "rev-parse", "HEAD") != RETROARCH_COMMIT:
|
||||
errors.append("chimera-retroarch HEAD mismatch")
|
||||
if git(retroarch_root, "branch", "--show-current") != RETROARCH_BRANCH:
|
||||
errors.append("chimera-retroarch branch mismatch")
|
||||
for label, record in (("canary", canary), ("early", early)):
|
||||
artifact = record["artifact"]
|
||||
elf_path = retroarch_root / artifact["local_relative_path"]
|
||||
map_path = retroarch_root / artifact["linker_map_relative_path"]
|
||||
if not elf_path.is_file():
|
||||
errors.append(f"{label} local ELF missing")
|
||||
elif (
|
||||
elf_path.stat().st_size != artifact["size"]
|
||||
or sha256_file(elf_path) != artifact["sha256"]
|
||||
):
|
||||
errors.append(f"{label} local ELF identity mismatch")
|
||||
if not map_path.is_file():
|
||||
errors.append(f"{label} local map missing")
|
||||
elif sha256_file(map_path) != artifact["linker_map_sha256"]:
|
||||
errors.append(f"{label} local map identity mismatch")
|
||||
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.resolve() if args.retroarch_root else None,
|
||||
)
|
||||
if errors:
|
||||
print("\n".join(errors), file=sys.stderr)
|
||||
return 1
|
||||
print("Phase 1.0D offline diagnostic ladder evidence validated")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate Phase-1.0DC identities and inactive policy."""
|
||||
from __future__ import annotations
|
||||
import argparse,ast,hashlib,json
|
||||
from pathlib import Path
|
||||
|
||||
SOURCE=(6096,"b8e965f343a0c3df5cf7026b68f547a81dad08d4b2e0934e4f9721de8b99010f")
|
||||
TESTS=(2630,"ecef8f126e2245d707a776b4fd02aad13553e6e79515287e1002d2855a8db2d2")
|
||||
NETWORK={"socket","select","selectors","subprocess","urllib","http","requests"}
|
||||
def exact(path,identity):
|
||||
data=path.read_bytes();return (len(data),hashlib.sha256(data).hexdigest())==identity
|
||||
def errors(record,root=None):
|
||||
out=[];activation=record.get("activation",{})
|
||||
if record.get("status")!="INACTIVE_DUAL_ARTIFACT_BIGAPP_GATE_COMPLETE_TARGET_IMPLEMENTATION_BLOCKED":out.append("status")
|
||||
if activation.get("active") is not False or any(value is not None for key,value in activation.items() if key!="active"):out.append("activation")
|
||||
if any(record.get("authorizations",{}).values()):out.append("authorization")
|
||||
if any(record.get("implementation_boundary",{}).values()):out.append("implementation")
|
||||
c=record.get("candidate_contract",{})
|
||||
if c.get("exact_payload_sha256")!="8dadce9d9faaef21ea129a3d216c768eea9a3ca9bf8ecb8d852e376b58a9bf95" or c.get("fixed_existing_title")!="PPSA01659" or c.get("sole_cleanup_terminal")!="D14":out.append("candidate")
|
||||
if not all(record.get("forbidden_effects",{}).values()):out.append("forbidden")
|
||||
if record.get("decision")!={"host_gate_contract_complete":True,"candidate_can_currently_pass":False,"target_implementation_allowed":False,"device_action_allowed":False,"next_step":"OFFLINE_MINIMAL_LAUNCHER_SOURCE_PREREQUISITE_CLOSURE"}:out.append("decision")
|
||||
if root:
|
||||
source=root/"tools/phase10dc_bigapp_gate_contract.py";tests=root/"tests/test_phase10dc_bigapp_gate_contract.py"
|
||||
if not exact(source,SOURCE):out.append("source identity")
|
||||
if not exact(tests,TESTS):out.append("test identity")
|
||||
tree=ast.parse(source.read_text());imports={a.name.split('.')[0] for n in ast.walk(tree) if isinstance(n,ast.Import) for a in n.names}|{(n.module or '').split('.')[0] for n in ast.walk(tree) if isinstance(n,ast.ImportFrom)}
|
||||
if imports&NETWORK:out.append("network import")
|
||||
return out
|
||||
def main():
|
||||
p=argparse.ArgumentParser();p.add_argument("--root",type=Path,required=True);root=p.parse_args().root.resolve();record=json.loads((root/"manifests/retroarch/phase-1.0dc-inactive-bigapp-comparison-gate.json").read_text());found=errors(record,root)
|
||||
for item in found:print("ERROR:",item)
|
||||
if found:return 1
|
||||
print("Phase-1.0DC inactive BigApp gate validation passed");return 0
|
||||
if __name__=="__main__":raise SystemExit(main())
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import annotations
|
||||
import argparse,ast,hashlib,json
|
||||
from pathlib import Path
|
||||
SOURCE=(5298,"2679f03617241abf37ca547141a2a51e3f89b75fec957a1da4f13b904d931f2d");TESTS=(2496,"3760fa60e66857b24bf507f196060ec9e4d8b96ebce3843ab02fc185c0f8c0b8");NETWORK={"socket","select","selectors","subprocess","urllib","http","requests"}
|
||||
def exact(path,identity):data=path.read_bytes();return (len(data),hashlib.sha256(data).hexdigest())==identity
|
||||
def errors(record,root=None):
|
||||
out=[];a=record.get("activation",{})
|
||||
if record.get("status")!="HOST_OBSERVER_CONTRACT_COMPLETE_LIVE_REQUEST_UNFORMABLE":out.append("status")
|
||||
if a.get("active") is not False or any(v is not None for k,v in a.items() if k!="active"):out.append("activation")
|
||||
if any(record.get("authorizations",{}).values()) or any(record.get("implementation_boundary",{}).values()):out.append("capability")
|
||||
evidence=record.get("current_evidence",{})
|
||||
if any(evidence.values()):out.append("evidence promoted")
|
||||
contract=record.get("observer_contract",{})
|
||||
if contract.get("allowlisted_methods") != ["SOURCE_BOUND_QUERY","EXACT_PATH_METADATA"] or contract.get("error_is_absence") is not False or contract.get("maximum_result_bytes")!=4096:out.append("contract")
|
||||
if not all(contract.get(k) is True for k in ("shell_forbidden","directory_enumeration_forbidden","title_launch_forbidden","app_termination_forbidden","device_write_forbidden","retry_forbidden","reconnect_forbidden")):out.append("forbidden")
|
||||
if record.get("decision")!={"host_observer_contract_complete":True,"live_observer_implementation_allowed":False,"live_observation_allowed":False,"device_action_allowed":False,"next_step":"OFFLINE_SOURCE_AUDIT_FOR_ONE_NONMUTATING_TITLE_PRESENCE_PRIMITIVE"}:out.append("decision")
|
||||
if root:
|
||||
source=root/"tools/phase10df_title_observer_contract.py";tests=root/"tests/test_phase10df_title_observer_contract.py"
|
||||
if not exact(source,SOURCE):out.append("source identity")
|
||||
if not exact(tests,TESTS):out.append("test identity")
|
||||
tree=ast.parse(source.read_text());imports={x.name.split('.')[0] for n in ast.walk(tree) if isinstance(n,ast.Import) for x in n.names}|{(n.module or '').split('.')[0] for n in ast.walk(tree) if isinstance(n,ast.ImportFrom)}
|
||||
if imports&NETWORK:out.append("network import")
|
||||
return out
|
||||
def main():
|
||||
p=argparse.ArgumentParser();p.add_argument("--root",type=Path,required=True);root=p.parse_args().root.resolve();record=json.loads((root/"manifests/retroarch/phase-1.0df-inactive-title-presence-observer.json").read_text());found=errors(record,root)
|
||||
for item in found:print("ERROR:",item)
|
||||
if found:return 1
|
||||
print("Phase-1.0DF inactive title observer validation passed");return 0
|
||||
if __name__=="__main__":raise SystemExit(main())
|
||||
@@ -0,0 +1,382 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Validate the consumed Phase-1.0E inherited result-channel evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
PHASE = "PHASE_1_0E_INHERITED_RESULT_CHANNEL"
|
||||
STATUS = "ONE_SHOT_DEVICE_TEST_COMPLETED_INCOMPLETE_BEFORE_D03"
|
||||
RETROARCH_BRANCH = "codex/ps5-inherited-result-channel"
|
||||
RETROARCH_COMMIT = "aed1a6e014d56ed25456b8b095955c7d41f7025d"
|
||||
OFFLINE_FOLLOWUP_COMMIT = "f1391c3e6717ff4e2b869007fb9627c9edde52e4"
|
||||
ARTIFACT_SOURCE_COMMIT = "b9fc037304a14199f35f8229edac26fa5c840509"
|
||||
LOADER_COMMIT = "197623058f509eddde18868dafcb92fdcac66464"
|
||||
DENYLIST_SHA256 = "e9603b0e3792781ad5b511afb22ef61e3d4fd4c5a16bf928f8609193bcd97783"
|
||||
ARTIFACT_SHA256 = "1049c78099a60b472a3fb0e2999e3393b6ad76337a28532a7e53872e7772dedf"
|
||||
ARTIFACT_SIZE = 1844880
|
||||
MAP_SHA256 = "ae9739f6f578953bc8dc562bb55967ba587912d161b2d6787438450addec3b44"
|
||||
TRACE_SHA256 = "4ff27a0eac48283cdc4c7ff964226def2689e808e3adea6594d0e77674a676f0"
|
||||
TRACE_SIZE = 1795
|
||||
FORBIDDEN_TARGET_IMPORTS = {"socket", "connect", "bind", "listen", "accept", "recv"}
|
||||
AUTHORIZATION_FIELDS = (
|
||||
"ps5_connection_authorized",
|
||||
"device_transfer_authorized",
|
||||
"device_execution_authorized",
|
||||
"result_receive_authorized",
|
||||
"installation_authorized",
|
||||
"autoload_authorized",
|
||||
"device_write_authorized",
|
||||
"automatic_retry",
|
||||
)
|
||||
PERFORMED_ACTION_FIELDS = (
|
||||
"ps5_connected",
|
||||
"device_request_performed",
|
||||
"files_transferred",
|
||||
"target_execution_performed",
|
||||
"result_received_from_device",
|
||||
)
|
||||
FORBIDDEN_ACTION_FIELDS = (
|
||||
"device_write_performed",
|
||||
"installation_performed",
|
||||
"autoload_performed",
|
||||
)
|
||||
DELIVERABLES = (
|
||||
"docs/retroarch/phase-1.0e-device-observations.md",
|
||||
"docs/retroarch/phase-1.0e-inherited-result-channel.md",
|
||||
"docs/retroarch/phase-1.0e-next-device-test.md",
|
||||
"docs/approvals/phase-1.0e-one-shot-result-test.md",
|
||||
"manifests/retroarch/phase-1.0e-result-channel.json",
|
||||
"tools/validate_retroarch_phase10e.py",
|
||||
"tests/test_retroarch_phase10e.py",
|
||||
"packaging/retroarch/phase10e/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 sha256_file(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 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 reproducible_artifact(record: dict[str, Any]) -> bool:
|
||||
hashes = record.get("clean_build_sha256", [])
|
||||
map_hashes = record.get("clean_map_sha256", [])
|
||||
return (
|
||||
record.get("size") == ARTIFACT_SIZE
|
||||
and record.get("sha256") == ARTIFACT_SHA256
|
||||
and hashes == [ARTIFACT_SHA256, ARTIFACT_SHA256]
|
||||
and record.get("linker_map_sha256") == MAP_SHA256
|
||||
and map_hashes == [MAP_SHA256, MAP_SHA256]
|
||||
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 True
|
||||
)
|
||||
|
||||
|
||||
def wx_closed(headers: list[dict[str, Any]]) -> bool:
|
||||
loads = [item for item in headers if item.get("type") == "LOAD"]
|
||||
return len(loads) == 3 and all(
|
||||
not ({"W", "E"} <= set(str(item.get("flags", "")))) for item in loads
|
||||
)
|
||||
|
||||
|
||||
def transport_is_bounded(record: dict[str, Any]) -> bool:
|
||||
loader = record.get("loader_transport", {})
|
||||
protocol = record.get("result_protocol", {})
|
||||
host = record.get("host_contract", {})
|
||||
return (
|
||||
loader.get("raw_elf_exact_length_read") is True
|
||||
and loader.get("payload_stdout_inherits_connection") is True
|
||||
and loader.get("controlled_route_supported") is False
|
||||
and loader.get("new_loader_change") is False
|
||||
and loader.get("new_target_socket") is False
|
||||
and loader.get("new_target_connection") is False
|
||||
and protocol.get("frame_size") == 64
|
||||
and protocol.get("target_write_attempts_per_stage") == 1
|
||||
and protocol.get("target_write_retry") is False
|
||||
and protocol.get("short_write_retry") is False
|
||||
and protocol.get("target_import_added") == "send"
|
||||
and set(protocol.get("target_forbidden_imports_absent", []))
|
||||
== FORBIDDEN_TARGET_IMPORTS
|
||||
and host.get("hash_before_socket_creation") is True
|
||||
and host.get("exact_size_before_socket_creation") is True
|
||||
and host.get("connection_count") == 1
|
||||
and host.get("sendall_count") == 1
|
||||
and host.get("shutdown_write_count") == 1
|
||||
and host.get("receive_limit_bytes") == 65536
|
||||
and host.get("retry") is False
|
||||
and host.get("reconnect") is False
|
||||
and host.get("resume") is False
|
||||
and host.get("trace_overwrite") is False
|
||||
and host.get("partial_result_is_success") is False
|
||||
)
|
||||
|
||||
|
||||
def observations_are_bounded(record: dict[str, Any]) -> bool:
|
||||
prior = record.get("prior_device_observations", {})
|
||||
run_a = prior.get("run_a", {})
|
||||
run_b = prior.get("run_b", {})
|
||||
return (
|
||||
prior.get("evidence_class") == "OPERATOR_OBSERVED_ARTIFACT_BOUND"
|
||||
and run_a.get("authorization_consumed") is True
|
||||
and run_a.get("classification")
|
||||
== "CRT_MAIN_AND_NOTIFICATION_PROVEN_ON_FW_9_60"
|
||||
and run_b.get("authorization_consumed") is True
|
||||
and run_b.get("classification")
|
||||
== "PAYLOAD_NOTIFICATION_CODE_EXECUTED_STAGE_UNCLASSIFIED"
|
||||
and run_b.get("console_remained_responsive") is True
|
||||
)
|
||||
|
||||
|
||||
def device_run_is_exact(record: dict[str, Any]) -> bool:
|
||||
run = record.get("device_run", {})
|
||||
frames = run.get("validated_frames", [])
|
||||
return (
|
||||
run.get("run_id") == "RUN_C"
|
||||
and run.get("authorization_consumed") is True
|
||||
and run.get("firmware") == "9.60"
|
||||
and run.get("trace_tracked") is False
|
||||
and run.get("trace_size") == TRACE_SIZE
|
||||
and run.get("trace_sha256") == TRACE_SHA256
|
||||
and run.get("connection_count") == 1
|
||||
and run.get("sendall_count") == 1
|
||||
and run.get("shutdown_write_count") == 1
|
||||
and run.get("retry_count") == 0
|
||||
and run.get("reconnect_count") == 0
|
||||
and run.get("close_called") is True
|
||||
and run.get("remote_eof_observed") is True
|
||||
and run.get("timeout_observed") is False
|
||||
and run.get("parser_errors") == []
|
||||
and [frame.get("stage") for frame in frames] == ["D00", "D01", "D02"]
|
||||
and [frame.get("sequence") for frame in frames] == [1, 2, 3]
|
||||
and frames[2].get("raw0") == 0
|
||||
and all(frame.get("notification_result") == 0 for frame in frames)
|
||||
and all(frame.get("terminal") is False for frame in frames)
|
||||
and run.get("terminal_stage") is None
|
||||
and run.get("last_proven_stage") == "D02"
|
||||
and run.get("classification")
|
||||
== "REMOTE_PAYLOAD_OUTPUT_PROVEN_INCOMPLETE_BEFORE_D03"
|
||||
and "D03_SDL_INIT_BEGIN" in run.get("does_not_prove", [])
|
||||
and "SAFE_EXIT_OR_LOADER_CLEANUP" in run.get("does_not_prove", [])
|
||||
)
|
||||
|
||||
|
||||
def offline_followup_is_bounded(record: dict[str, Any]) -> bool:
|
||||
followup = record.get("offline_followup", {})
|
||||
return (
|
||||
followup.get("repository") == "chimera-retroarch"
|
||||
and followup.get("branch") == RETROARCH_BRANCH
|
||||
and followup.get("commit") == OFFLINE_FOLLOWUP_COMMIT
|
||||
and followup.get("classification")
|
||||
== "D02_TO_D03_SOURCE_INTERVAL_BOUNDED_CAUSE_UNPROVEN"
|
||||
and followup.get("bounded_raw_stream_retention") is True
|
||||
and followup.get("bounded_ordinary_stdout_retention") is True
|
||||
and followup.get("encoding") == "base64"
|
||||
and followup.get("sha256_recorded") is True
|
||||
and followup.get("maximum_pre_encoding_bytes") == 65536
|
||||
and followup.get("exclusive_trace_creation") is True
|
||||
and followup.get("consumed_manifest_rejected") is True
|
||||
and followup.get("retry") is False
|
||||
and followup.get("reconnect") is False
|
||||
and followup.get("device_action_performed") is False
|
||||
and followup.get("target_code_changed") is False
|
||||
and followup.get("target_artifact_created") is False
|
||||
and followup.get("authorization_changed") is False
|
||||
)
|
||||
|
||||
|
||||
def validate(
|
||||
root: Path,
|
||||
retroarch_root: Path | None = None,
|
||||
loader_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.0e-result-channel.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") != ARTIFACT_SOURCE_COMMIT:
|
||||
errors.append("source commit mismatch")
|
||||
if record.get("host_client_commit") != RETROARCH_COMMIT:
|
||||
errors.append("host client commit mismatch")
|
||||
authorizations = record.get("authorizations", {})
|
||||
if not all_false(authorizations, AUTHORIZATION_FIELDS):
|
||||
errors.append("consumed Phase-1.0E authorization must be fully false")
|
||||
actions = record.get("phase_actions", {})
|
||||
if not all(actions.get(field) is True for field in PERFORMED_ACTION_FIELDS):
|
||||
errors.append("performed one-shot actions are missing")
|
||||
if not all_false(actions, FORBIDDEN_ACTION_FIELDS):
|
||||
errors.append("a forbidden device action was recorded")
|
||||
if not observations_are_bounded(record):
|
||||
errors.append("prior device observations are overclaimed or incomplete")
|
||||
active = record.get("active_one_shot", {})
|
||||
if not (
|
||||
active.get("artifact_sha256") == ARTIFACT_SHA256
|
||||
and active.get("artifact_size") == ARTIFACT_SIZE
|
||||
and active.get("firmware") == "9.60"
|
||||
and active.get("connection_count") == 1
|
||||
and active.get("transfer_count") == 1
|
||||
and active.get("execution_count") == 1
|
||||
and active.get("result_receive_count") == 1
|
||||
and active.get("timeout_seconds") == 75
|
||||
and active.get("automatic_retry") is False
|
||||
and active.get("reconnect") is False
|
||||
and active.get("installation") is False
|
||||
and active.get("autoload") is False
|
||||
and active.get("device_write") is False
|
||||
and active.get("consumed") is True
|
||||
):
|
||||
errors.append("one-shot contract is not exact or not consumed")
|
||||
if not reproducible_artifact(record.get("artifact", {})):
|
||||
errors.append("artifact is not exact, reproducible and post-run ineligible")
|
||||
if not device_run_is_exact(record):
|
||||
errors.append("RUN C result is missing, malformed or overclaimed")
|
||||
if not offline_followup_is_bounded(record):
|
||||
errors.append("offline D02-to-D03 follow-up is missing or unsafe")
|
||||
|
||||
elf = record.get("elf", {})
|
||||
imports = set(elf.get("undefined_symbols", []))
|
||||
if elf.get("rwx_load_segment_count") != 0 or not wx_closed(
|
||||
elf.get("program_headers", [])
|
||||
):
|
||||
errors.append("ELF load layout is not W^X closed")
|
||||
if elf.get("init_array_size") != 0 or elf.get("fini_array_size") != 0:
|
||||
errors.append("constructor arrays are not empty")
|
||||
if elf.get("tls") is not False:
|
||||
errors.append("TLS must be absent")
|
||||
if "send" not in imports or imports & FORBIDDEN_TARGET_IMPORTS:
|
||||
errors.append("target import closure is not inherited-output-only")
|
||||
if elf.get("undefined_symbol_delta_from_phase10d") != ["send"]:
|
||||
errors.append("Phase-1.0D import delta must be exactly send")
|
||||
if not transport_is_bounded(record):
|
||||
errors.append("transport or host contract is not bounded")
|
||||
|
||||
effects = record.get("startup_effects", {})
|
||||
if not (
|
||||
effects.get("normal_sdk_crt") is True
|
||||
and effects.get("patch_init_reachable_from_start") is True
|
||||
and effects.get("kernel_copy_helpers_statically_linked") is True
|
||||
and effects.get("side_effect_free") is False
|
||||
):
|
||||
errors.append("SDK startup effects are hidden or misclassified")
|
||||
|
||||
denylist = root / "manifests/artifact-denylist.json"
|
||||
if not denylist.is_file() or sha256_file(denylist) != DENYLIST_SHA256:
|
||||
errors.append("permanent denylist changed")
|
||||
tracked = git(root, "ls-files").splitlines()
|
||||
for relative in tracked:
|
||||
if relative.lower().endswith((".elf", ".self", ".sprx", ".pkg")):
|
||||
errors.append(f"tracked target artifact: {relative}")
|
||||
path = root / relative
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError):
|
||||
continue
|
||||
if "192.168.10." + "105" in text:
|
||||
errors.append(f"tracked device address in {relative}")
|
||||
|
||||
if retroarch_root is not None:
|
||||
ancestor = subprocess.run(
|
||||
["git", "merge-base", "--is-ancestor", RETROARCH_COMMIT, "HEAD"],
|
||||
cwd=retroarch_root,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if ancestor.returncode:
|
||||
errors.append("tested chimera-retroarch host client commit is not an ancestor")
|
||||
followup_ancestor = subprocess.run(
|
||||
["git", "merge-base", "--is-ancestor", OFFLINE_FOLLOWUP_COMMIT, "HEAD"],
|
||||
cwd=retroarch_root,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if followup_ancestor.returncode:
|
||||
errors.append("offline stdout-capture commit is not an ancestor")
|
||||
if git(retroarch_root, "branch", "--show-current") != RETROARCH_BRANCH:
|
||||
errors.append("chimera-retroarch branch mismatch")
|
||||
artifact = record["artifact"]
|
||||
elf_path = retroarch_root / artifact["local_relative_path"]
|
||||
map_path = retroarch_root / artifact["linker_map_relative_path"]
|
||||
if not elf_path.is_file() or (
|
||||
elf_path.stat().st_size != ARTIFACT_SIZE
|
||||
or sha256_file(elf_path) != ARTIFACT_SHA256
|
||||
):
|
||||
errors.append("local result ELF missing or changed")
|
||||
if not map_path.is_file() or sha256_file(map_path) != MAP_SHA256:
|
||||
errors.append("local result linker map missing or changed")
|
||||
run = record["device_run"]
|
||||
trace_path = retroarch_root / run["trace_relative_path"]
|
||||
if not trace_path.is_file() or (
|
||||
trace_path.stat().st_size != TRACE_SIZE
|
||||
or sha256_file(trace_path) != TRACE_SHA256
|
||||
):
|
||||
errors.append("local ignored RUN C trace missing or changed")
|
||||
if loader_root is not None:
|
||||
if git(loader_root, "rev-parse", "HEAD") != LOADER_COMMIT:
|
||||
errors.append("hardened elfldr HEAD mismatch")
|
||||
if git(loader_root, "status", "--porcelain"):
|
||||
errors.append("hardened elfldr tree is dirty")
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
parser.add_argument("--retroarch-root", type=Path)
|
||||
parser.add_argument("--loader-root", type=Path)
|
||||
args = parser.parse_args()
|
||||
errors = validate(
|
||||
args.root.resolve(),
|
||||
args.retroarch_root.resolve() if args.retroarch_root else None,
|
||||
args.loader_root.resolve() if args.loader_root else None,
|
||||
)
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"ERROR: {error}", file=sys.stderr)
|
||||
return 1
|
||||
print("Phase-1.0E inherited result-channel validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user