# PS5 payload loader contract audit Audit date: 2026-07-17. Scope: pinned public PS5 Payload SDK v0.41 at `d2e2e585740362976a39fdd5ccf390f199a7bc37`, inspected offline. No PS5 was contacted and no ELF was built, transferred, or executed. ## Available primary evidence The SDK README names three external loader families: `ps5-payload-dev/elfldr`, `cryonumb/elfloader` through `ps5-jar-loader`, and `shahrilnet/remote_lua_loader`. None of those caller repositories is present in the pinned local evidence set. A `git grep` of every other local primary-source checkout found no `payload_args_t` or `payloadout` caller implementation. The only available contract is therefore the SDK callee side: - `include/ps5/payload.h` defines the six-field `payload_args_t`; - `crt/crt.c` defines `int _start(payload_args_t *args)` and labels it as the entry invoked by the ELF loader; - `crt1.o` disassembly shows the first argument consumed from `RDI` and `payloadout` read at offset `0x28`; - the SDK linker script defines image, BSS, constructor, destructor, and dynamic-section boundaries. These facts prove the callee layout. They do not prove how a particular loader constructs the argument, stack, return address, or post-return state. ## Argument layout For the audited x86-64 target the public fields are six eight-byte values in this order: | Offset | Field | Callee use | |---:|---|---| | `0x00` | `sys_dynlib_dlsym` | SDK syscall/symbol bootstrap | | `0x08` | `rwpipe` | existing kernel read/write transport descriptors | | `0x10` | `rwpair` | existing kernel read/write transport descriptors | | `0x18` | `kpipe_addr` | existing kernel pipe address | | `0x20` | `kdata_base_addr` | existing kernel data-base address | | `0x28` | `payloadout` | result location written by the SDK CRT | The 48-byte layout is `SAFE` as a source and disassembly fact. The lifetime, writability, ownership, nullability, and post-return consumption of every pointer are `UNPROVEN` because the caller is absent. ## Entry ABI and stack The compiled SDK `_start` has a normal x86-64 function prologue, consumes the argument from `RDI`, makes further calls without dynamically realigning `RSP`, and ends one path with `ret`. This establishes what the callee expects, not what the loader guarantees. In particular, the following remain `UNPROVEN`: - the exact stack alignment at entry; - whether a valid return address is present; - which registers or floating-point state the loader expects preserved; - whether the loader calls the entry as an ordinary function or transfers control by another mechanism; - whether the memory behind `payload_args_t` remains valid until return. ## BSS and image initialization The SDK linker script provides `__bss_start` and `__bss_end`. The stock `_start` explicitly zeros that range byte-by-byte before accessing its own BSS state. The loop itself is freestanding and has no import, so a comparable loop is technically possible. It is not evidence that the loader maps every segment with the permissions, alignment, relocation state, or zero-fill behavior a new artifact would require. Those caller/mapper properties are `UNPROVEN`. ## Return, exit, crash, and cleanup The stock CRT does not define one universal return rule. Its `payload_terminate()` first tries to detect a hijacked process with `kernel_dynlib_dlsym(-1, 0x2001, "sceKernelDlsym")`. In that branch it returns from `_start`. Otherwise it resolves and invokes `exit`; if that fails it executes a trap. This heuristic itself depends on the syscall/kernel/rtld stack that Phase 0.5 forbids. Consequently, a custom `_start` that simply executes `ret` cannot be claimed safe from the SDK callee alone. No local primary evidence proves: - who consumes the integer return value; - whether return resumes a hijacked process or exits a dedicated process; - which mappings, descriptors, credentials, or signal state are restored; - what happens after an invalid argument, fault, trap, or partial BSS clear; - whether any cleanup exists after a crash. All five are `UNPROVEN` and are build blockers. ## Loader state versus artifact-caused state The argument fields show that kernel read/write descriptors and kernel addresses already exist when the SDK entry is called. That is a fact about the incoming state, not proof of how the loader created it. The exact loader may have changed credentials, syscall bounds, mappings, modules, or other process state before entry; the available callee source cannot distinguish those possibilities. The stock SDK artifact then independently causes additional writes through `__patch_init`; those writes are attributable to `crt1.o` and are `UNSAFE`. No Phase-0.5 candidate artifact exists, so it caused no new state changes. ## Contract decision The public callee contract is insufficient for a safe-return artifact. Decision: **BLOCKED**. The minimal missing evidence is the exact loader used for firmware 9.60 at a pinned public commit, including its entry call site, argument ownership, return path, pre-entry changes, cleanup, and crash path. Machine evidence: `manifests/runtime/phase-0.5-startup-audit.json`. ## Phase 0.6 exact-loader addendum The Phase-0.5 conclusion above is preserved as the evidence available at that time. Phase 0.6 identified the installed loader exactly through the read-only Payload Manager inventory and a byte-for-byte public release match: - Payload Manager v0.3.1, commit `cfbc70f30f419b09bf2b52283f7409e2d3117ee1`, SHA-256 `518740adbacccb9094fadb07dd424c53ee290f38306449ccc9d6957fdf813c0b`; - `ps5-payload-dev/elfldr` v0.23, commit `699e8bcff03e91e8d6ca6eba281af25c5a58d8c2`, SHA-256 `092d16ee0ede0c494947efd38d1a17bbd7cc4b022d3858ea898833c188c703e8`. The proposed `itsPLK/ps5-elfldr` v0.23.2 candidate is not installed: its release hash is `2c28f847c859d3126b0f089ee23b72b84a733548da222ceafd91ae2f3df8f933`. The exact exploit/autoloader that originally starts the first-stage loader remains unidentified. ### Proven entry setup The exact v0.23 source creates a new process using `rfork_thread(RFPROC | RFCFDG | RFMEM)`. The child enables tracing and executes `/system/vsh/app/NPXS40112/eboot.bin` (`SceSpZeroConf`). The parent stops at an INT3 placed 58 bytes after the mapped eboot entry, restores that byte, maps the payload, and then sets: | Register/state | Exact loader operation | Status | |---|---|---| | `RSP` | writes observed `RIP` at `RSP - 8`, then subtracts 8 | `SAFE` source fact | | `RIP` | payload ELF entry | `SAFE` source fact | | `RDI` | loader-created `payload_args_t` page | `SAFE` source fact | | breakpoint byte | original byte restored before injection | `RESTORED_BY_LOADER` | | breakpoint page permission | changed to RWX and not restored | `PAYLOAD_PROCESS_LOCAL` | This is a synthetic function-call frame. The public source does not establish the meaning of the observed trap `RIP`, the exact original instruction boundary, or the validity of resuming there after payload return. Therefore the continuation is still `UNPROVEN`. ### Argument ownership and lifetime `elfldr_payload_args()` maps one page into the child, creates two IPv6 sockets and a pipe, overlaps the sockets through the SDK kernel helper, and populates the six public fields. On successful detach the loader does not unmap that page or close those child descriptors. They are process-local, but cleanup depends on the child terminating. That termination and reaping path is not bounded or proven. ### Return, exit, crash, and hang The stock SDK v0.41 `_start` may return, resolve and call `exit`, or trap, depending on `payload_terminate()`. Which branch applies to this injected SceSpZeroConf child is not proven. Separately, elfldr: - has no payload wall-clock deadline after `PT_DETACH`; - has unbounded single-step loops in both `pt_call()` and `pt_syscall()`; - ignores `SIGCHLD` in the service and documents no post-detach reaping contract; - kills the child on some preparation errors, but does not supervise normal runtime, return, crash, or hang. These are `UNBOUNDED_OR_UNKNOWN`, not objections to the mere presence of temporary kernel/process writes. They block the requested controlled two-second lifecycle. ### Existing loader state versus artifact-caused state The first-stage loader enables QA flag bits and temporarily changes credentials before the candidate payload exists. Those are existing-loader effects. For each ptrace operation the service temporarily changes its authid and normally restores it. The child receives process-local credentials, mappings, sockets, pipes, executable permissions, and SDK `__patch_init` changes. The artifact would cause SDK startup only after entry; it would not cause the loader bootstrap. The separation does not make the chain ready: UID restoration is absent, authid restoration has an unrecovered failure path, and child lifetime is unbounded. Full evidence is in `manifests/runtime/phase-0.6-loader-runtime-audit.json`. ## Phase 0.7 hardened loader contract Phase 0.7 does not depend on safely returning from `main`. The lifecycle probe calls `_exit` after exactly one notification, so the unresolved SDK `payload_terminate` return/exit/trap branch is outside the reachable application path. The hardened elfldr preserves the public `payload_args_t *` entry convention and adds a controlled versioned transport header with exact artifact ID, firmware 9.60, size, SHA-256, 2000 ms timeout, and no-retry fields. Both sender and receiver validate the metadata; the receiver hashes the actual received bytes and rejects the permanent denylist. The loader's process mutations are exact and bounded: - the initial QA flag change is an existing volatile bootstrap effect; - ptrace service auth-ID and all child UID/jail/root/caps/auth fields are backed up, restored, and checked; - breakpoint byte and page protection are restored; - single-step loops have independent time and step limits; - every incomplete launch path terminates and reaps the child; - the detached success path is replaced with a two-second supervised wait, bounded termination escalation, and reap; - an auth restoration failure permanently disables further ptrace work and stops the service. This is sufficient for the offline `READY_FOR_HARDENED_RUNTIME_DEPLOYMENT` decision. Loader behavior on firmware 9.60 remains `UNPROVEN` until separately authorized installation and execution produce on-device evidence.