Files
ps5upload/payload/installer/main.c
T
phantomptrandClaude Opus 5.5 c14782944d PS5Upload v5.35.0
Upload, install, convert and manage PS5 game packages over the network.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 04:31:13 -07:00

321 lines
12 KiB
C

/* PS5Upload installer daemon: TCP :9115, JSON-lines protocol. Loaded onto the
* console by the :9021 ELF loader as a companion image (never evicts the
* helper). Self-escalates, bounded AppInstUtil init, then serves hello /
* install / job / stop. Sony calls are serialized behind one mutex so hello
* and job answer while an install is in flight. No third-party branding. */
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <stdint.h>
#include <time.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include "protocol.h"
#include "pathsafe.h"
#include "jobs.h"
#include "loopback.h"
#include "sony.h"
#include "escalate.h"
#include "authid.h" /* ps5_detect_firmware_major */
#define INST_PORT 9115
#define INST_VERSION "1.1.0"
#define BOOT_STEP_MS 500
#define BOOT_STEPS 50 /* 50 * 500ms = 25s */
/* Sony toast helper. Every message starts "PS5Upload installer". */
typedef struct notify_request {
char useless1[45];
char message[3075];
} notify_request_t;
extern int sceKernelSendNotificationRequest(int, notify_request_t *, size_t, int);
static void notify(const char *fmt, ...) {
notify_request_t req;
va_list ap;
bzero(&req, sizeof req);
va_start(ap, fmt);
vsnprintf(req.message, sizeof req.message, fmt, ap);
va_end(ap);
sceKernelSendNotificationRequest(0, &req, sizeof req, 0);
}
typedef struct {
pthread_mutex_t sony_lock; /* serializes InstallByPackage */
pthread_mutex_t state_lock; /* active job + ring + seq */
inst_sony_state_t sony;
inst_job_phase_t active_phase;
char active_id[INST_JOBID_MAX];
inst_loopback_t *active_lb;
inst_job_ring_t ring;
uint32_t seq;
volatile int stop;
} daemon_t;
static daemon_t g_d;
static double uptime_seconds(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
}
static void make_job_id(char *out, size_t cap) {
pthread_mutex_lock(&g_d.state_lock);
uint32_t s = ++g_d.seq;
pthread_mutex_unlock(&g_d.state_lock);
snprintf(out, cap, "%ld-%u", (long)time(NULL), s);
}
/* ── dispatch: each returns a JSON object length in `out`, or -1. ─────── */
static int do_hello(char *out, size_t cap) {
pthread_mutex_lock(&g_d.state_lock);
int esc = g_d.sony.escalated;
uint32_t irc = g_d.sony.init_rc;
int done = g_d.sony.init_done;
int fw = g_d.sony.fw_major;
pthread_mutex_unlock(&g_d.state_lock);
const char *state = done ? "ready" : "init_failed";
char fwbuf[16];
snprintf(fwbuf, sizeof(fwbuf), "%d", fw);
return inst_reply_hello(out, cap, INST_VERSION, fwbuf, state, irc, esc);
}
static int do_job(const inst_request_t *req, char *out, size_t cap) {
inst_job_phase_t ph; uint32_t code; uint64_t bs, tot;
pthread_mutex_lock(&g_d.state_lock);
/* if it's the active loopback job, refresh live bytes first */
if (g_d.active_lb && strcmp(g_d.active_id, req->job) == 0) {
bs = inst_loopback_bytes_served(g_d.active_lb);
tot = inst_loopback_total(g_d.active_lb);
inst_ring_put(&g_d.ring, req->job, g_d.active_phase, 0, bs, tot);
}
int found = inst_ring_get(&g_d.ring, req->job, &ph, &code, &bs, &tot);
pthread_mutex_unlock(&g_d.state_lock);
if (!found) return inst_reply_err_str(out, cap, "unknown_job");
return inst_reply_job(out, cap, inst_job_phase_str(ph), bs, tot, code);
}
static int do_install(const inst_request_t *req, char *out, size_t cap) {
if (req->src == INST_SRC_PATH && !inst_path_is_safe(req->path))
return inst_reply_err_str(out, cap, "bad_path");
/* ensure init (retry once on demand) */
pthread_mutex_lock(&g_d.state_lock);
int done = g_d.sony.init_done;
pthread_mutex_unlock(&g_d.state_lock);
if (!done) {
int rc = inst_sony_init(&g_d.sony);
pthread_mutex_lock(&g_d.state_lock);
g_d.sony.init_done = (rc == 0);
g_d.sony.init_rc = (uint32_t)rc;
done = (rc == 0);
pthread_mutex_unlock(&g_d.state_lock);
if (!done)
return inst_reply_err_not_ready(out, cap, (uint32_t)rc);
}
/* Admission: only a serving loopback job blocks a new install. Take the
* sony_lock FIRST, then decide admission under state_lock. The check must
* be inside the sony_lock, not before it: active_phase is not set to
* SERVING until an accepted loopback install finishes below, so a
* pre-lock check would let a second install slip in while the first's
* Sony call is still in flight — starting a second loopback (leaking the
* first) and issuing a duplicate InstallByPackage. Serialising the check
* behind sony_lock closes that window. */
pthread_mutex_lock(&g_d.sony_lock);
pthread_mutex_lock(&g_d.state_lock);
inst_job_phase_t active = g_d.active_phase;
char busy_id[INST_JOBID_MAX];
snprintf(busy_id, sizeof(busy_id), "%s", g_d.active_id);
int active_complete = 0;
if (g_d.active_lb && active == INST_JOB_SERVING) {
uint64_t tot = inst_loopback_total(g_d.active_lb);
active_complete = (tot == 0) || (inst_loopback_bytes_served(g_d.active_lb) >= tot);
}
if (!inst_admit_install(active, active_complete)) {
pthread_mutex_unlock(&g_d.state_lock);
pthread_mutex_unlock(&g_d.sony_lock);
return inst_reply_err_busy(out, cap, busy_id);
}
/* Sony has read every byte of the previous loopback job: retire it now
* (instead of after its idle window) so this install can start its own. */
inst_loopback_t *retire = NULL;
if (active == INST_JOB_SERVING && active_complete) {
uint64_t tot = inst_loopback_total(g_d.active_lb);
inst_ring_put(&g_d.ring, g_d.active_id, INST_JOB_DONE, 0, tot, tot);
retire = g_d.active_lb;
g_d.active_lb = NULL;
g_d.active_phase = INST_JOB_DONE;
}
pthread_mutex_unlock(&g_d.state_lock);
if (retire) inst_loopback_stop(retire); /* join outside state_lock */
char job_id[INST_JOBID_MAX];
make_job_id(job_id, sizeof(job_id));
inst_loopback_t *lb = NULL;
inst_install_result_t res = inst_sony_install(req, &g_d.sony, &lb, job_id);
pthread_mutex_lock(&g_d.state_lock);
if (res.accepted) {
if (lb) { /* live loopback serving job */
g_d.active_lb = lb;
g_d.active_phase = INST_JOB_SERVING;
snprintf(g_d.active_id, sizeof(g_d.active_id), "%s", job_id);
inst_ring_put(&g_d.ring, job_id, INST_JOB_SERVING, 0, 0,
inst_loopback_total(lb));
} else { /* url or path: accepted, no serving */
inst_ring_put(&g_d.ring, job_id, INST_JOB_ACCEPTED, 0, 0, 0);
}
} else {
inst_ring_put(&g_d.ring, job_id, INST_JOB_FAILED, res.code, 0, 0);
}
pthread_mutex_unlock(&g_d.state_lock);
pthread_mutex_unlock(&g_d.sony_lock);
if (res.accepted)
return inst_reply_install_ok(out, cap, job_id, res.via);
return inst_reply_err_sony(out, cap, res.code, res.hint);
}
/* ── connection worker ─────────────────────────────────────────────────── */
static void *conn_worker(void *arg) {
int fd = (int)(intptr_t)arg;
struct timeval tv = { 5, 0 };
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
int yes = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof(yes));
char buf[INST_REQ_MAX + 1];
size_t len = 0;
while (len < INST_REQ_MAX) {
ssize_t n = recv(fd, buf + len, INST_REQ_MAX - len, 0);
if (n <= 0) break;
len += (size_t)n;
buf[len] = '\0';
if (memchr(buf, '\n', len)) break; /* one line per request */
}
/* trim at newline */
char *nl = memchr(buf, '\n', len);
if (nl) { *nl = '\0'; len = (size_t)(nl - buf); }
inst_request_t req;
inst_parse_request(buf, len, &req);
char out[512];
int olen = -1;
int is_stop = 0;
if (req.error) {
olen = inst_reply_err_str(out, sizeof(out), req.error);
} else switch (req.op) {
case INST_OP_HELLO: olen = do_hello(out, sizeof(out)); break;
case INST_OP_INSTALL: olen = do_install(&req, out, sizeof(out)); break;
case INST_OP_JOB: olen = do_job(&req, out, sizeof(out)); break;
case INST_OP_STOP: olen = inst_reply_ok(out, sizeof(out)); is_stop = 1; break;
default: olen = inst_reply_err_str(out, sizeof(out), "bad_request"); break;
}
if (olen > 0) {
(void)send(fd, out, (size_t)olen, MSG_NOSIGNAL);
(void)send(fd, "\n", 1, MSG_NOSIGNAL);
}
close(fd);
if (is_stop) g_d.stop = 1;
return NULL;
}
/* ── monitor: retire a serving loopback job once fully covered + idle ──── */
static void *monitor(void *arg) {
(void)arg;
while (!g_d.stop) {
inst_loopback_t *retire = NULL;
char id[INST_JOBID_MAX];
pthread_mutex_lock(&g_d.state_lock);
if (g_d.active_lb && g_d.active_phase == INST_JOB_SERVING) {
uint64_t bs = inst_loopback_bytes_served(g_d.active_lb);
uint64_t tot = inst_loopback_total(g_d.active_lb);
int complete = (tot == 0) || (bs >= tot);
double idle = inst_loopback_idle_seconds(g_d.active_lb);
inst_ring_put(&g_d.ring, g_d.active_id, INST_JOB_SERVING, 0, bs, tot);
if (inst_job_should_finish(complete, idle)) {
retire = g_d.active_lb;
snprintf(id, sizeof(id), "%s", g_d.active_id);
inst_ring_put(&g_d.ring, g_d.active_id, INST_JOB_DONE, 0, tot, tot);
g_d.active_lb = NULL;
g_d.active_phase = INST_JOB_DONE;
}
}
pthread_mutex_unlock(&g_d.state_lock);
if (retire) inst_loopback_stop(retire); /* join outside the lock */
struct timespec ts = { 5, 0 };
nanosleep(&ts, NULL);
}
return NULL;
}
int main(void) {
memset(&g_d, 0, sizeof(g_d));
pthread_mutex_init(&g_d.sony_lock, NULL);
pthread_mutex_init(&g_d.state_lock, NULL);
inst_ring_reset(&g_d.ring);
g_d.active_phase = INST_JOB_NONE;
/* Bind :9115 first so a probe sees us while we boot-wait. */
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) { notify("PS5Upload installer: socket failed"); return -1; }
int reuse = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(INST_PORT);
if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
/* another copy already owns the port — exit quietly */
close(server_fd);
return 0;
}
if (listen(server_fd, 8) < 0) { close(server_fd); return 0; }
/* Boot wait only on a fresh boot (uptime <= 120s). */
if (inst_should_boot_wait(uptime_seconds())) {
struct timespec ts = { 0, BOOT_STEP_MS * 1000000L };
for (int i = 0; i < BOOT_STEPS; i++) nanosleep(&ts, NULL);
}
g_d.sony.fw_major = ps5_detect_firmware_major();
g_d.sony.escalated = (inst_escalate_self() == 0);
if (!g_d.sony.escalated)
notify("PS5Upload installer: escalation failed; install may not work");
(void)inst_sony_init(&g_d.sony);
pthread_t mon;
pthread_create(&mon, NULL, monitor, NULL);
pthread_detach(mon);
notify("PS5Upload installer listening on port %d", INST_PORT);
while (!g_d.stop) {
socklen_t al = sizeof(addr);
int c = accept(server_fd, (struct sockaddr *)&addr, &al);
if (c < 0) continue;
pthread_t t;
if (pthread_create(&t, NULL, conn_worker, (void *)(intptr_t)c) == 0)
pthread_detach(t);
else
close(c);
}
notify("PS5Upload installer stopping");
close(server_fd);
return 0;
}