Files
chimera-gfx-Public/docs/runtime/loader-contract.md
T
Chimera GFX release export a6037502d7
phase0-ci / build-and-audit (push) Successful in 2m14s
Publish Chimera GFX source
2026-09-03 03:27:14 +02:00

221 lines
10 KiB
Markdown

# 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.