Files
ps5upload/payload/include/runtime.h
T
phantomptrandClaude Opus 5.5 e90dd6e4b4 Decompress a .ffpfsc on the console, decoded by the kernel (#453)
Files gets "Decompress to .exfat…" on a .ffpfsc again. The first build (pulled
before 7.0.23) inflated the blocks in the helper with vendored zlib, and the helper
died with SIGBUS within minutes of loading it; once the console froze. This one has
no decoder: the helper mounts the container read-only the way ShadowMount+ mounts a
.ffpfsc (LVD image type 9, read-only single-save flags, 4096-byte sectors, pfs with
mkeymode=AC), so the kernel decodes it, and copies the file inside to <name>.part,
then renames it in the same folder. The kernel lists that file rounded up to whole
64 KiB blocks; only the inode's real length is copied.

The checks from the first build stay: the container's block table is read before the
kernel sees it, a destination that exists and a drive without room for the image plus
1 GiB are refused, the size and exFAT boot sector are checked before the rename, and
Stop removes the .part and the mount. One unpack runs at a time. The mount is not in
the mount table the helper sees, so it is recorded on the /mnt tmpfs (owner pid, LVD
unit, .part) and a helper that starts after one died mid-copy releases it; a record of
a live instance is left alone.

Measured on the FW 13.60 Phat: a 315 MB image (127 MB container) in 3.5 s and a 4.5 GB
one (3.0 GB container) in 65 s, both byte-identical to the source; Stop, a takeover
mid-copy and a stale record all left no mount, LVD unit or .part behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3cPwhp3UrzayBfyy9PDcN
2026-10-16 07:09:42 -07:00

175 lines
9.3 KiB
C

#ifndef PS5UPLOAD2_RUNTIME_H
#define PS5UPLOAD2_RUNTIME_H
#include <stdint.h>
#include <stddef.h>
#include <pthread.h>
#include "instance_verdict.h"
typedef struct {
uint64_t instance_id;
uint64_t takeover_nonce; /* random; names this instance in the takeover flag file (takeover_flag.h) */
/* Mutex guarding the counters below, which node.status reads while the management handlers
* (on AVA1 worker threads) update them. Keep critical sections short. */
pthread_mutex_t state_mtx;
int shutdown_requested;
int startup_reason;
/* How the PREVIOUS instance ended, classified once at startup before
* runtime_write_ownership overwrites the record. Holds a
* ps5upload2_prior_verdict_t. Reported by node.status so the client and
* the bug bundle can show it: an externally SIGKILLed predecessor is
* otherwise completely invisible. */
int prior_verdict;
int takeover_requested;
/* Set by runtime_try_takeover: a prior instance was serving AVA1 and was asked to hand over.
* Clear: nothing answered, so no prior was asked to exit and none will go by itself. */
int prior_asked;
uint64_t started_at_unix;
/* The prior instance's ownership record as read at startup, before the takeover made it
* unlink the file. The reap backs a fresh (possibly lost) read with it (review 010). */
int prior_rec_pid;
uint64_t prior_rec_started;
uint64_t command_count;
char ownership_path[256];
} runtime_state_t;
#define PS5UPLOAD2_STARTUP_FRESH 1
#define PS5UPLOAD2_STARTUP_TAKEOVER 2
int runtime_init(runtime_state_t *state);
int runtime_write_ownership(const runtime_state_t *state);
int runtime_clear_ownership(const runtime_state_t *state);
/* Best-effort SIGKILL of a previous instance that crashed and lingered (the
* cooperative takeover only handles a healthy old instance). Call AFTER
* runtime_try_takeover and BEFORE runtime_write_ownership. See runtime.c. */
void runtime_reap_prior_instance(runtime_state_t *state);
/* Classify how the previous instance ended and store it on `state`.
* MUST be called after runtime_init (which fills ownership_path) and
* BEFORE runtime_write_ownership overwrites the prior record. MUST also be
* called before any worker thread starts (mgmt thread, shutdown watchdog,
* etc.): it writes state->prior_verdict WITHOUT holding state_mtx, while
* handle_status_frame reads it under that mutex. That is only safe because
* today's one call site runs on the main thread long before any other
* thread that could read prior_verdict exists — a later call, or a second
* call from a worker thread, would race. This is a threading contract, not
* locking: no lock has been added here on purpose. */
void runtime_classify_prior_instance(runtime_state_t *state);
/* LAST RESORT. SIGKILL every process whose name carries our own
* "ps5upload" prefix, except this one. Returns how many were killed.
*
* Only ever called after BOTH the cooperative TAKEOVER_REQUEST handshake
* AND the pid-based reap have failed. The graceful path stays primary
* because it calls runtime_mark_active_transactions(..., "interrupted")
* first, which tears the journal down cleanly so upload resume survives;
* a SIGKILL skips all of that.
*
* Unlike the pid-based reap this does NOT need an ownership record, which
* is the case it exists for: a predecessor whose record was lost or
* overwritten is otherwise unreachable and the new payload just exits.
*
* Deliberately has NO boot-session guard, unlike runtime_reap_prior_instance.
* That guard exists there because a pid comes from a persisted file that
* survives reboots. Here every pid comes from a live KERN_PROC_PROC sysctl
* snapshot taken at call time — the live snapshot IS the boot-session proof,
* so there is nothing for a started_at/boottime check to add. See the
* comment at the top of the implementation for the full reasoning. */
int runtime_sweep_our_instances(void);
/* Arm a detached watchdog that force-`_exit()`s the process if the graceful
* shutdown wedges, so a stuck shutdown can't leave an orphan. Call once when
* shutdown begins (after runtime_wait_for_shutdown returns).
*
* `state` is used ONLY to clear the ownership record before the forced
* `_exit()` — a shutdown that wedges past this watchdog is a deliberate exit
* we caused, not an external kill, and the ownership record must not
* outlive it (see instance_verdict.h: a leftover record + dead pid is
* indistinguishable from `killed_externally` to the next instance). May be
* NULL to skip that step. */
void runtime_arm_shutdown_watchdog(const runtime_state_t *state, int exit_code);
int runtime_ensure_directories(void);
/* 2.2.52 Tier-1 staging sweep. Removes *.pkg files in
* PS5UPLOAD2_PKG_TEMP_DIR with mtime older than 24 h — orphans
* left by desktop crashes mid-install. Call once at payload init,
* after runtime_ensure_directories. Failure-tolerant: opendir/stat
* errors are silent so payload still starts even if the dir was
* never created (e.g. read-only /data, fresh PS5 startup). */
void runtime_sweep_stale_pkg_temp(void);
int runtime_try_takeover(runtime_state_t *state);
/* Asks this instance to exit (node.shutdown, the old shutdown frame, the takeover flag file). */
void runtime_request_shutdown(runtime_state_t *state, const char *why);
/* Blocks until something asks this instance to exit (node.shutdown, the takeover flag file). The
* AVA1 server runs on its own threads: the main thread has nothing else to serve. */
void runtime_wait_for_shutdown(runtime_state_t *state);
/* Pop a system toast on the PS5 UI (top-right corner). Defined in
* main.c. Used by mount/unmount/register/launch handlers in
* runtime.c so users see status on the PS5 even when the desktop
* client is closed. Empty/NULL message is a no-op. The kernel API
* caps message length at ~3 KiB internally, but in practice we
* format ~100-character strings. */
void pop_notification(const char *message);
/* (Re-)apply the full ucred jailbreak: uid/ruid/svuid=0, all-FF
* sceCaps, sceAttr=0x80000000, debugger authid, root vnode for
* rootdir + jaildir. Idempotent and safe to call multiple times.
* Sets `g_ucred_elevation_rc` to the aggregate result (0 = full
* elevation succeeded, non-zero = at least one kernel write
* failed — typically because kernel R/W isn't available yet).
* Called once at startup from main.c, and again lazily from
* shellui_rpc_init() if the prior elevation attempt failed —
* lets users load kstuff after the payload was already running
* and have sensors/launch start working without a reboot. */
void runtime_apply_ucred_jailbreak(void);
/* Installs the AVA1 management table (mgmt_rpc.h) over this runtime. 0, or -1. Call before
* the AVA1 server starts. */
int runtime_mgmt_install(runtime_state_t *state);
/* `volatile` matches the definition in main.c — without it, the
* compiler is allowed to cache reads across function calls into a
* register, which on a multi-thread frame dispatcher (where every
* connection thread re-runs runtime_apply_ucred_jailbreak() and
* mutates this) means a reader could observe a stale -1 long after
* elevation succeeded, or vice versa. The qualifier is the cheapest
* fix that preserves the existing int-shape extern across all
* call sites. */
extern volatile int g_ucred_elevation_rc;
/* Writable-roots allowlist check. Returns 1 if the path starts with an
* allowed root (/data, /user, /mnt/extN, /mnt/usbN, /mnt/ps5upload/<name>,
* /mnt/shadowmnt) and contains no ".."/"." components that escape it.
* Also resolves symlinks via realpath() and re-validates the canonical
* form. Used by every destructive FS handler and backup.c's restore
* path. NOTE: realpath() fails for not-yet-existing paths (write/mkdir
* targets) — in that case the lexical check result is returned as-is
* (there's no symlink to follow yet). */
int is_path_allowed(const char *p);
/* Mount a .ffpfsc read-only so the kernel decodes it (#453): at FS_MOUNT_BASE/.unpack/<tag>, the
* file inside readable at <mp>/<its name>. 0 with *unit_out set, or -1 with a cause token in err.
* `note` is a .part file the sweep removes if this helper dies before releasing. Always pair with
* fs_pfsc_release. fs_pfsc_sweep clears what a dead helper left mounted. */
int fs_pfsc_mount_ro(const char *image, const char *tag, const char *note, char *mp, size_t mp_cap, int *unit_out,
char *err, size_t err_cap);
void fs_pfsc_release(const char *mp, int unit);
void fs_pfsc_sweep(void);
/* The read half of the unsafe-read rule: `/system*` paths the hard-coded management handlers
* may read when the request asks for it. `is_path_allowed` OR this is the pair every
* unsafe read must satisfy — the same expression runtime.c uses. */
int is_safe_unsafe_read_path(const char *p);
/* The legacy frame number this thread is currently dispatching, or 0 when it is
* not inside a request. Thread-local and written only by its own thread, so
* the fatal-signal handler can read it without a lock.
*
* Why it exists: when the helper dies, the persisted stderr.log used to name
* the faulting call only if it was one of the fault-guarded hardware getters.
* A crash anywhere else left no trace of what was running. Recording the
* frame type turns "the helper disconnected" into "the helper died on
* SIGSEGV while serving frame 68" for EVERY request. */
extern __thread volatile unsigned int g_inflight_frame_type;
#endif