Publish Chimera GFX source
phase0-ci / build-and-audit (push) Successful in 2m14s

This commit is contained in:
Chimera GFX release export
2026-09-03 03:27:14 +02:00
commit a6037502d7
828 changed files with 100454 additions and 0 deletions
+612
View File
@@ -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