Files
knutwurst--ps5-patchdl/src/patchdl_install.c
T
Knutwurst fb9d06fb5b Install: pass the /user/data path Sony allowlists; report per-URI rc
Sony path-allowlists the URI given to sceAppInstUtilInstallByPackage —
/user/data/ and /mnt/usb are accepted, a bare /data/... path is rejected with
0x80B2116F (confirmed by the ps5upload project). PatchDL stored the pkg under
/data/patchdl and passed that /data path, so every install was rejected at the
path stage. Pass the /user/data view of the same file instead (the code already
computed it as sdk_path; it was only used for AppInstallPkg before).

Also report each URI's individual rc instead of only the last attempt's, which
revealed the real wall: via file:// the installer reaches header parsing and
rejects with 0x80B21106 — the assembled file is a valid but DEBUG-magic PKG
(\x7FFIH, not retail \x7FCNT), the format Sony's system updater consumes rather
than the retail-pkg format InstallByPackage expects.
2026-06-24 14:25:50 +02:00

492 lines
18 KiB
C

#include "patchdl_install.h"
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <netinet/in.h>
#include <ps5/kernel.h>
#include <pthread.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
/* AppInstUtil structs/signatures, reverse-engineered by the PS5 homebrew
scene (notmaj0r/CheatRunner, ps5-payload-dev/websrv). Sizes per CheatRunner:
CONTENTID=0x30, PLAYGOSCENARIOID=3, LANGUAGE=8, NUM_LANGUAGES=30, NUM_IDS=64.
The trailing `unknown` padding makes the playgo struct safely large. */
#define AI_CONTENTID_SIZE 0x30
#define AI_PLAYGOSCENARIOID_SIZE 3
#define AI_LANGUAGE_SIZE 8
#define AI_NUM_LANGUAGES 30
#define AI_NUM_IDS 64
typedef struct {
char content_id[AI_CONTENTID_SIZE];
int content_type;
int content_platform;
} ai_pkg_info_t;
typedef struct {
const char *uri;
const char *ex_uri;
const char *playgo_scenario_id;
const char *content_id;
const char *content_name;
const char *icon_url;
} ai_meta_info_t;
typedef struct {
char languages[AI_NUM_LANGUAGES][AI_LANGUAGE_SIZE];
char playgo_scenario_ids[AI_NUM_IDS][AI_PLAYGOSCENARIOID_SIZE];
char content_ids[AI_NUM_IDS][AI_CONTENTID_SIZE];
long unknown[810];
} ai_playgo_info_t;
/* Sysmodule IDs (from ps5-payload-dev/sdk crt/rtld_sprx.c). */
#define SYSMOD_IPMI 0x8000001d
#define SYSMOD_USERSERVICE 0x80000011
#define SYSMOD_SYSTEMSERVICE 0x80000010
#define SYSMOD_APPINSTUTIL 0x80000014
typedef int (*sysmod_load_fn)(unsigned int id);
typedef int (*ai_init_fn)(void);
typedef int (*ai_install_pkg_fn)(const char *path, ai_pkg_info_t *info);
typedef int (*ai_install_by_pkg_fn)(ai_meta_info_t *meta, ai_pkg_info_t *info,
ai_playgo_info_t *playgo);
typedef int (*ai_title_from_pkg_fn)(const char *path, char *title_id, int *is_app);
typedef int (*ai_content_from_pkg_fn)(const char *path, char *content_id, int *is_app);
static ai_init_fn ai_initialize;
static ai_install_pkg_fn ai_install_pkg;
static ai_install_by_pkg_fn ai_install_by_package;
static ai_title_from_pkg_fn ai_title_from_pkg;
static ai_content_from_pkg_fn ai_content_from_pkg;
/* Resolve + initialize the AppInstUtil backend WITHOUT linking the sce libs
(that makes the ELF unloadable by the elfldr) and WITHOUT raw
sceKernelLoadStartModule (that wedged the process). Instead use the proper
sysmodule loader — the same path the SDK's rtld_sprx takes — then resolve
symbols via the kernel dynlib helpers. Runs in a detached thread; the HTTP
handler reports the stage and never blocks.
stage: 0 idle, 1 resolve loader, 2 load modules, 3 resolve symbols,
4 initialize, 5 ready, negative = failure at that step. */
static volatile int g_stage;
static int g_err;
static pthread_mutex_t g_mtx = PTHREAD_MUTEX_INITIALIZER;
static char g_probe_json[2048]; /* filled by the backend thread */
static intptr_t
dynsym(const char *module, const char *sym) {
uint32_t h = 0;
if (kernel_dynlib_handle(-1, module, &h) < 0)
return 0;
return kernel_dynlib_dlsym(-1, h, sym);
}
static void
local_ip(char *out, size_t n) {
struct ifaddrs *ifa = NULL, *p;
out[0] = '\0';
if (getifaddrs(&ifa))
return;
for (p = ifa; p; p = p->ifa_next) {
char ip[INET_ADDRSTRLEN];
struct sockaddr_in *s;
if (!p->ifa_addr || p->ifa_addr->sa_family != AF_INET)
continue;
s = (struct sockaddr_in *)p->ifa_addr;
if (!inet_ntop(AF_INET, &s->sin_addr, ip, sizeof(ip)))
continue;
if (strcmp(ip, "127.0.0.1") && strncmp(ip, "0.", 2)) {
strncpy(out, ip, n - 1);
out[n - 1] = '\0';
break;
}
}
freeifaddrs(ifa);
}
/* Resolve (dlsym, never call) a list of candidate patch-install symbols and
record which exist. Runs inside the backend thread, where the AppInstUtil
module is already loaded — the same proven-safe context as the normal symbol
resolution. No sysmod_load and no calls, so it is side-effect free. */
static void
fill_probe(void) {
static const char *ai_syms[] = {
"sceAppInstUtilInitialize",
"sceAppInstUtilAppInstallPkg",
"sceAppInstUtilAppInstallTitleDir", /* takes an explicit title id */
"sceAppInstUtilInstallByPackage",
"sceAppInstUtilInstallByPackageEx",
"sceAppInstUtilGetTitleIdFromPkg",
"sceAppInstUtilGetContentIdFromPkg",
"sceAppInstUtilAppExist",
"sceAppInstUtilAppGetInstallStatus",
"sceAppInstUtilAppInstallStatus",
"sceAppInstUtilAppUnInstall",
"sceAppInstUtilGetMetaInfoFromPkg",
"sceAppInstUtilUpdateTitleByTitleId",
"sceAppInstUtilInstallByChunk",
NULL
};
static const char *bgft_syms[] = {
"sceBgftInitialize",
"sceBgftServiceIntInit",
"sceBgftServiceDownloadRegisterTask",
"sceBgftServiceDownloadRegisterTaskByStorage",
"sceBgftServiceDownloadRegisterTaskByStorageEx",
"sceBgftServiceIntDownloadRegisterTaskByStorageEx",
"sceBgftServiceDownloadStartTask",
"sceBgftServiceDownloadGetProgress",
"sceBgftServiceInstallPackage",
NULL
};
uint32_t h = 0;
int ai_loaded = (kernel_dynlib_handle(-1, "libSceAppInstUtil.sprx", &h) >= 0);
int bgft_loaded = (kernel_dynlib_handle(-1, "libSceBgft.sprx", &h) >= 0);
char tmp[sizeof(g_probe_json)];
size_t n = 0, sz = sizeof(tmp);
char *out = tmp;
int first = 1;
n += snprintf(out + n, sz - n,
"{\"appinstutil_loaded\":%s,\"bgft_loaded\":%s,\"symbols\":{",
ai_loaded ? "true" : "false", bgft_loaded ? "true" : "false");
for (int i = 0; ai_syms[i] && n < sz - 80; i++) {
intptr_t a = dynsym("libSceAppInstUtil.sprx", ai_syms[i]);
n += snprintf(out + n, sz - n, "%s\"%s\":%s",
first ? "" : ",", ai_syms[i], a ? "true" : "false");
first = 0;
}
for (int i = 0; bgft_syms[i] && n < sz - 80; i++) {
intptr_t a = bgft_loaded ? dynsym("libSceBgft.sprx", bgft_syms[i]) : 0;
n += snprintf(out + n, sz - n, "%s\"%s\":%s",
first ? "" : ",", bgft_syms[i], a ? "true" : "false");
first = 0;
}
snprintf(out + n, sz - n, "}}");
/* Publish atomically: the getter runs on an MHD worker thread and reads
g_probe_json under the same lock, so it never sees a half-built buffer. */
pthread_mutex_lock(&g_mtx);
memcpy(g_probe_json, tmp, sizeof(g_probe_json));
pthread_mutex_unlock(&g_mtx);
}
static void *
backend_init_thread(void *arg) {
(void)arg;
sysmod_load_fn sysmod_load;
g_stage = 1;
sysmod_load = (sysmod_load_fn)dynsym("libSceSysmodule.sprx",
"sceSysmoduleLoadModuleInternal");
if (!sysmod_load) { g_stage = -1; return NULL; }
g_stage = 2;
/* Dependencies first, then AppInstUtil. The sysmodule loader pulls in
further deps and is the non-wedging path. */
sysmod_load(SYSMOD_IPMI);
sysmod_load(SYSMOD_USERSERVICE);
sysmod_load(SYSMOD_SYSTEMSERVICE);
g_err = sysmod_load(SYSMOD_APPINSTUTIL);
if (g_err) { g_stage = -2; return NULL; }
g_stage = 3;
ai_initialize = (ai_init_fn)dynsym("libSceAppInstUtil.sprx",
"sceAppInstUtilInitialize");
ai_install_pkg = (ai_install_pkg_fn)dynsym("libSceAppInstUtil.sprx",
"sceAppInstUtilAppInstallPkg");
ai_install_by_package = (ai_install_by_pkg_fn)dynsym("libSceAppInstUtil.sprx",
"sceAppInstUtilInstallByPackage");
ai_title_from_pkg = (ai_title_from_pkg_fn)dynsym("libSceAppInstUtil.sprx",
"sceAppInstUtilGetTitleIdFromPkg");
ai_content_from_pkg = (ai_content_from_pkg_fn)dynsym("libSceAppInstUtil.sprx",
"sceAppInstUtilGetContentIdFromPkg");
/* Read-only feasibility probe — module is loaded, safe context. */
fill_probe();
if (!ai_initialize || !ai_install_pkg || !ai_install_by_package) {
g_stage = -3;
return NULL;
}
g_stage = 4;
g_err = ai_initialize();
if (g_err) { g_stage = -4; return NULL; }
g_stage = 5;
return NULL;
}
static void
backend_start(void) {
pthread_mutex_lock(&g_mtx);
if (g_stage == 0) {
pthread_t tid;
pthread_attr_t attr;
g_stage = 1;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, backend_init_thread, NULL);
pthread_attr_destroy(&attr);
}
pthread_mutex_unlock(&g_mtx);
}
static const char *
stage_str(int s) {
switch (s) {
case 0: return "idle";
case 1: return "resolving sysmodule loader";
case 2: return "loading AppInstUtil module";
case 3: return "resolving AppInstUtil symbols";
case 4: return "initializing service";
case 5: return "ready";
case -1: return "sysmodule loader unavailable";
case -2: return "AppInstUtil module load failed";
case -3: return "AppInstUtil symbol resolution failed";
case -4: return "AppInstUtil init failed";
default: return "unknown";
}
}
int
patchdl_install_backend_check(char *msg, size_t msg_sz) {
int s;
backend_start();
s = g_stage;
snprintf(msg, msg_sz, "%s (rc=0x%08x)", stage_str(s), (unsigned)g_err);
return (s == 5) ? 0 : -1;
}
/* Public getter: trigger the backend (which fills the probe in its own thread)
and return the cached result. Runs from the MHD worker thread, so it only
reads the cached string — it never loads modules or resolves symbols here. */
int
patchdl_install_api_probe(char *out, size_t out_sz) {
int ready;
backend_start();
pthread_mutex_lock(&g_mtx);
ready = (g_probe_json[0] != '\0');
if (ready)
snprintf(out, out_sz, "%s", g_probe_json);
pthread_mutex_unlock(&g_mtx);
if (ready)
return 0;
snprintf(out, out_sz,
"{\"pending\":true,\"stage\":\"%s\"}", stage_str(g_stage));
return -1;
}
/* Read-only: report the .pkg's embedded content id + title id (and whether it
is a full app vs a patch). No install, no side effects. 0 if anything read. */
int
patchdl_install_pkg_meta(const char *local_path, char *content_id, size_t cid_sz,
char *title_id, size_t tid_sz, int *is_app,
char *msg, size_t msg_sz) {
char sdk_path[1024];
char cid[64] = {0}, tid[48] = {0};
int app_c = 0, app_t = 0, ok = 0;
struct stat st;
if (content_id && cid_sz) content_id[0] = '\0';
if (title_id && tid_sz) title_id[0] = '\0';
if (is_app) *is_app = 0;
if (!local_path || !local_path[0] || stat(local_path, &st) != 0) {
snprintf(msg, msg_sz, "package not on disk");
return -1;
}
backend_start();
if (g_stage != 5) {
snprintf(msg, msg_sz, "install backend not ready: %s", stage_str(g_stage));
return -1;
}
if (!strncmp(local_path, "/data/", 6))
snprintf(sdk_path, sizeof sdk_path, "/user%s", local_path);
else
snprintf(sdk_path, sizeof sdk_path, "%s", local_path);
if (ai_content_from_pkg &&
ai_content_from_pkg(sdk_path, cid, &app_c) == 0 && cid[0]) {
if (content_id && cid_sz) {
strncpy(content_id, cid, cid_sz - 1);
content_id[cid_sz - 1] = '\0';
}
if (is_app) *is_app = app_c;
ok = 1;
}
if (ai_title_from_pkg &&
ai_title_from_pkg(sdk_path, tid, &app_t) == 0 && tid[0]) {
if (title_id && tid_sz) {
strncpy(title_id, tid, tid_sz - 1);
title_id[tid_sz - 1] = '\0';
}
ok = 1;
}
snprintf(msg, msg_sz, ok ? "ok" : "could not read pkg metadata");
return ok ? 0 : -1;
}
int
patchdl_install_local_pkg(const char *local_path, const char *expected_title_id,
const char *storage_title_id,
const char *target_content_id,
char *msg, size_t msg_sz) {
char sdk_path[1024];
char pkg_tid[48] = {0};
struct stat st;
int rc;
int pkg_tid_mismatch = 0;
if (!local_path || !local_path[0]) {
snprintf(msg, msg_sz, "no package path");
return -1;
}
if (stat(local_path, &st) != 0) {
snprintf(msg, msg_sz, "package not downloaded");
return -1;
}
backend_start();
if (g_stage != 5) {
snprintf(msg, msg_sz, "install backend not ready: %s", stage_str(g_stage));
return -1;
}
/* Sony's installer sees the user partition as /user/data, not /data, and
PATH-ALLOWLISTS the URI passed to InstallByPackage: /user/data/ and
/mnt/usb are accepted, but a bare /data/... path is REJECTED with
0x80B2116F (empirically confirmed by the ps5upload project). So feed the
/user/data view of the file to both InstallByPackage and AppInstallPkg. */
if (!strncmp(local_path, "/data/", 6))
snprintf(sdk_path, sizeof(sdk_path), "/user%s", local_path);
else
snprintf(sdk_path, sizeof(sdk_path), "%s", local_path);
/* Diagnostic guard: Sony sometimes stores one patch byte stream under a
master title id while the version.xml targets a regional title id. That
is valid only when the caller supplies target metadata, so do not feed
such packages to the raw AppInstallPkg path. */
if (storage_title_id && storage_title_id[0] &&
expected_title_id && expected_title_id[0] &&
strncmp(storage_title_id, expected_title_id, 9) != 0) {
pkg_tid_mismatch = 1;
strncpy(pkg_tid, storage_title_id, sizeof(pkg_tid) - 1);
}
if (ai_title_from_pkg && expected_title_id && expected_title_id[0]) {
int is_app = 0;
if (ai_title_from_pkg(sdk_path, pkg_tid, &is_app) == 0 && pkg_tid[0] &&
strncmp(pkg_tid, expected_title_id, 9) != 0) {
pkg_tid_mismatch = 1;
}
}
if (pkg_tid_mismatch && (!target_content_id || !target_content_id[0])) {
snprintf(msg, msg_sz,
"refused: package metadata is %.12s, target is %.12s",
pkg_tid, expected_title_id);
return -1;
}
/* Preferred path: InstallByPackage accepts target metadata. Use it first,
and use it exclusively when the downloaded bytes report a master/storage
title id that differs from the target regional title id. */
{
char file_uri[1100];
char http_loop_uri[1200] = {0};
char http_lan_uri[1200] = {0};
const char *uris[4];
ai_meta_info_t meta = {0};
ai_pkg_info_t pkg = {0};
ai_playgo_info_t playgo = {0};
int rc2 = -1;
const char *title_dir;
const char *file_base;
snprintf(file_uri, sizeof(file_uri), "file://%s", sdk_path);
title_dir = strstr(local_path, "/data/patchdl/");
file_base = strrchr(local_path, '/');
if (title_dir && file_base && file_base > title_dir + strlen("/data/patchdl/")) {
char title_id[32] = {0};
const char *t = title_dir + strlen("/data/patchdl/");
size_t tlen = (size_t)(file_base - t);
if (tlen > 0 && tlen < sizeof(title_id)) {
char ip[INET_ADDRSTRLEN] = {0};
memcpy(title_id, t, tlen);
snprintf(http_loop_uri, sizeof(http_loop_uri),
"http://127.0.0.1:%d/api/pkg/%s/%s",
PATCHDL_HTTP_PORT, title_id, file_base + 1);
local_ip(ip, sizeof(ip));
if (ip[0])
snprintf(http_lan_uri, sizeof(http_lan_uri),
"http://%s:%d/api/pkg/%s/%s",
ip, PATCHDL_HTTP_PORT, title_id, file_base + 1);
}
}
uris[0] = sdk_path; /* /user/data/... — the allowlisted path */
uris[1] = file_uri; /* file:///user/data/... */
uris[2] = http_loop_uri[0] ? http_loop_uri : NULL;
uris[3] = http_lan_uri[0] ? http_lan_uri : NULL;
meta.ex_uri = "";
meta.playgo_scenario_id = "";
meta.content_id = target_content_id ? target_content_id : "";
meta.content_name = "PatchDL";
meta.icon_url = "";
{
char tries[260] = {0};
const char *labels[4] = { "userdata", "file", "loop", "lan" };
for (int i = 0; i < 4; i++) {
if (!uris[i]) continue;
memset(&pkg, 0, sizeof(pkg));
memset(&playgo, 0, sizeof(playgo));
meta.uri = uris[i];
rc2 = ai_install_by_package(&meta, &pkg, &playgo);
{
size_t l = strlen(tries);
snprintf(tries + l, sizeof(tries) - l, "%s%s=0x%08x",
l ? "," : "", labels[i], (unsigned)rc2);
}
if (rc2 == 0) {
snprintf(msg, msg_sz, "install started (InstallByPackage%s)",
pkg_tid_mismatch ? ", shared master bytes" : "");
return 0;
}
}
rc = rc2;
if (pkg_tid_mismatch) {
snprintf(msg, msg_sz,
"install rejected (pkg %.12s -> %.12s; tries: %s)",
pkg_tid, expected_title_id ? expected_title_id : "", tries);
return rc ? rc : -1;
}
}
}
/* Last resort for normal same-title packages only. This path has no target
metadata parameter, so it is intentionally skipped for shared-master
region bytes. */
{
ai_pkg_info_t pkg = {0};
int rc2 = ai_install_pkg(sdk_path, &pkg);
if (rc2 == 0) {
snprintf(msg, msg_sz, "install started (AppInstallPkg)");
return 0;
}
snprintf(msg, msg_sz,
"install rejected (InstallByPackage=0x%08x, AppInstallPkg=0x%08x)",
(unsigned)rc, (unsigned)rc2);
return rc2 ? rc2 : (rc ? rc : -1);
}
}