PatchDL 0.0.1: scan, resolve, download and install PS5 game patches

Standalone PS5 payload with an embedded web UI on :12880, no etaHEN.

- Scans installed titles and classifies the source (genuine install,
  ShadowMountPlus mount, preinstall, unknown) from the on-disk layout.
- Reads name, installed version and the Sony version.xml URL from the PS5
  app database (vendored SQLite).
- Resolves version.xml past nanoDNS via a raw DNS query, TLS pinned to the
  SCEI DNAS root.
- Filters patches to the newest one compatible with the current firmware.
- Downloads the patch and installs it through AppInstUtil (sysmodule loaded
  at runtime), gated to genuine installs with a package title-id match guard.
- Action-based UI filters and an on-screen startup notification with the URL.
This commit is contained in:
Knutwurst committed 2026-06-23 14:52:35 +02:00
1 parent 4a14c689e5
commit 01464ae88a
31 files changed
+273765 -365

No files matched your search

+245
View File
@@ -0,0 +1,245 @@
#include "patchdl_install.h"
#include <ps5/kernel.h>
#include <pthread.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.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);
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;
/* 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 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 *
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");
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;
}
int
patchdl_install_local_pkg(const char *local_path, const char *expected_title_id,
char *msg, size_t msg_sz) {
char sdk_path[1024];
struct stat st;
int rc;
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;
}
/* The install service runs in its own sandbox that sees the user
partition as /user/data, not /data — remap so it can read the file. */
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);
/* GUARD: read the PKG's own title id and refuse if it does not match the
installed game. A cross-title/region package (e.g. a US PPSA03098 patch
on an EU PPSA03099 install) would otherwise be registered as a separate
phantom title instead of patching the game. */
if (ai_title_from_pkg && expected_title_id && expected_title_id[0]) {
char pkg_tid[48] = {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) {
snprintf(msg, msg_sz,
"refused: package is for %.12s, installed game is %.12s "
"(cross-title/region)", pkg_tid, expected_title_id);
return -1;
}
}
/* Primary: direct package install. */
{
ai_pkg_info_t pkg = {0};
rc = ai_install_pkg(sdk_path, &pkg);
if (rc == 0) {
snprintf(msg, msg_sz, "install started (AppInstallPkg)");
return 0;
}
}
/* Fallback: InstallByPackage with a file:// URI. */
{
char file_uri[1100];
ai_meta_info_t meta = {0};
ai_pkg_info_t pkg = {0};
ai_playgo_info_t playgo = {0};
int rc2;
snprintf(file_uri, sizeof(file_uri), "file://%s", sdk_path);
meta.uri = file_uri;
meta.ex_uri = "";
meta.playgo_scenario_id = "";
meta.content_id = "";
meta.content_name = "PatchDL";
meta.icon_url = "";
rc2 = ai_install_by_package(&meta, &pkg, &playgo);
if (rc2 == 0) {
snprintf(msg, msg_sz, "install started (InstallByPackage)");
return 0;
}
snprintf(msg, msg_sz,
"install rejected (AppInstallPkg=0x%08x, InstallByPackage=0x%08x)",
(unsigned)rc, (unsigned)rc2);
return rc2;
}
}