Add read-only pkg diagnostics: manifest dump, integrity verify, embedded ids

Three read-only endpoints (no install, no writes) to inspect a downloaded
package on-device:

- GET /api/manifest/<title_id> — re-fetch the patch manifest (PatchDL bypasses
  the DNS block) and dump each piece's offset/size/SHA-256.
- GET /api/pkgverify/<title_id> — SHA-256 every piece of the assembled .pkg
  against the manifest hashes, on-device (SSD, no multi-GB transfer), and report
  per-piece pass/fail. Proves whether the file is byte-correct.
- GET /api/pkgmeta/<title_id> — read the pkg's embedded content id + title id
  (GetContentIdFromPkg) vs the target ids, to expose cross-region linkage.

Supporting code: patchdl_sha256_fd_region() (pread + OpenSSL EVP) in the net
layer, and bind sceAppInstUtilGetContentIdFromPkg in the install backend.

Used to diagnose the Dead Island 2 install: the 61.6 GB package verifies
byte-perfect (17/17 pieces) and its content id matches the target, so the
0x80B2116F install rejection is a Sony install-method limitation, not the data.
This commit is contained in:
Knutwurst committed 2026-06-24 14:10:39 +02:00
1 parent 986ff00c36
commit 3d2ee430e1
5 files changed
+211 -4

No files matched your search

+58 -4
View File
@@ -57,11 +57,13 @@ 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, typedef int (*ai_install_by_pkg_fn)(ai_meta_info_t *meta, ai_pkg_info_t *info,
ai_playgo_info_t *playgo); ai_playgo_info_t *playgo);
typedef int (*ai_title_from_pkg_fn)(const char *path, char *title_id, int *is_app); 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_init_fn ai_initialize;
static ai_install_pkg_fn ai_install_pkg; static ai_install_pkg_fn ai_install_pkg;
static ai_install_by_pkg_fn ai_install_by_package; static ai_install_by_pkg_fn ai_install_by_package;
static ai_title_from_pkg_fn ai_title_from_pkg; 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 /* Resolve + initialize the AppInstUtil backend WITHOUT linking the sce libs
(that makes the ELF unloadable by the elfldr) and WITHOUT raw (that makes the ELF unloadable by the elfldr) and WITHOUT raw
@@ -205,6 +207,8 @@ backend_init_thread(void *arg) {
"sceAppInstUtilInstallByPackage"); "sceAppInstUtilInstallByPackage");
ai_title_from_pkg = (ai_title_from_pkg_fn)dynsym("libSceAppInstUtil.sprx", ai_title_from_pkg = (ai_title_from_pkg_fn)dynsym("libSceAppInstUtil.sprx",
"sceAppInstUtilGetTitleIdFromPkg"); "sceAppInstUtilGetTitleIdFromPkg");
ai_content_from_pkg = (ai_content_from_pkg_fn)dynsym("libSceAppInstUtil.sprx",
"sceAppInstUtilGetContentIdFromPkg");
/* Read-only feasibility probe — module is loaded, safe context. */ /* Read-only feasibility probe — module is loaded, safe context. */
fill_probe(); fill_probe();
@@ -284,6 +288,56 @@ patchdl_install_api_probe(char *out, size_t out_sz) {
return -1; 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 int
patchdl_install_local_pkg(const char *local_path, const char *expected_title_id, patchdl_install_local_pkg(const char *local_path, const char *expected_title_id,
const char *storage_title_id, const char *storage_title_id,
+6
View File
@@ -31,3 +31,9 @@ int patchdl_install_backend_check(char *msg, size_t msg_sz);
AppInstUtil/Bgft patch-install symbols and report which exist on this AppInstUtil/Bgft patch-install symbols and report which exist on this
firmware. Writes a JSON object into `out`. No install, no side effects. */ firmware. Writes a JSON object into `out`. No install, no side effects. */
int patchdl_install_api_probe(char *out, size_t out_sz); int patchdl_install_api_probe(char *out, size_t out_sz);
/* Read-only: report the .pkg's embedded content id + title id (and whether it
is a full app vs a patch, via *is_app). No install. 0 if anything was 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);
+35
View File
@@ -807,6 +807,41 @@ patchdl_http_download_piece(const char *url, int fd,
return 0; return 0;
} }
/* Read-only: SHA-256 a [offset, offset+size) region of fd into out_hex (>=65
bytes). Uses pread so it doesn't disturb the fd offset. 0 on success. */
int
patchdl_sha256_fd_region(int fd, long long offset, long long size, char *out_hex) {
EVP_MD_CTX *md;
unsigned char *buf;
long long pos = offset, remaining = size;
const size_t CHUNK = 1u << 20;
out_hex[0] = '\0';
if (fd < 0 || size < 0) return -1;
md = EVP_MD_CTX_new();
if (!md) return -1;
buf = malloc(CHUNK);
if (!buf) { EVP_MD_CTX_free(md); return -1; }
EVP_DigestInit_ex(md, EVP_sha256(), NULL);
while (remaining > 0) {
size_t want = remaining > (long long)CHUNK ? CHUNK : (size_t)remaining;
ssize_t got = pread(fd, buf, want, (off_t)pos);
if (got <= 0) { free(buf); EVP_MD_CTX_free(md); return -1; }
EVP_DigestUpdate(md, buf, (size_t)got);
pos += got; remaining -= got;
}
{
unsigned char dig[EVP_MAX_MD_SIZE];
unsigned int dl = 0, i;
EVP_DigestFinal_ex(md, dig, &dl);
for (i = 0; i < dl; i++) sprintf(out_hex + 2 * i, "%02x", dig[i]);
out_hex[2 * dl] = '\0';
}
free(buf);
EVP_MD_CTX_free(md);
return 0;
}
void void
patchdl_manifest_free(patchdl_manifest_t *m) { patchdl_manifest_free(patchdl_manifest_t *m) {
if (!m || !m->pieces) return; if (!m || !m->pieces) return;
+5
View File
@@ -57,6 +57,11 @@ int patchdl_http_download_piece(const char *url, int fd,
const char *expected_sha256_or_null, const char *expected_sha256_or_null,
patchdl_piece_ctx_t *ctx); patchdl_piece_ctx_t *ctx);
/* Read-only: SHA-256 a [offset, offset+size) region of fd into out_hex
(caller provides >= 65 bytes). Returns 0 on success. */
int patchdl_sha256_fd_region(int fd, long long offset, long long size,
char *out_hex);
/* Progress callback. Return non-zero to ABORT the in-flight download (used to /* Progress callback. Return non-zero to ABORT the in-flight download (used to
cancel large patch downloads); return 0 to continue. */ cancel large patch downloads); return 0 to continue. */
typedef int (*patchdl_download_progress_cb)(void *ctx, typedef int (*patchdl_download_progress_cb)(void *ctx,
+107
View File
@@ -1807,6 +1807,113 @@ on_request(void *cls, struct MHD_Connection *conn, const char *url,
if (!strcmp(url, "/api/pkgdiag")) if (!strcmp(url, "/api/pkgdiag"))
return queue_json(conn, MHD_HTTP_OK, g_pkg_diag_json); return queue_json(conn, MHD_HTTP_OK, g_pkg_diag_json);
/* Read-only diagnostic: re-fetch the patch manifest for a title (PatchDL can
bypass the DNS block) and dump each piece's offset/size/SHA-256 so the
assembled .pkg can be verified against Sony's own hashes. No install. */
if (!strncmp(url, "/api/manifest/", 14)) {
const char *tid = url + 14;
char purl[768] = {0}, pti[32], psti[32], cid[64], nm[128], ver[16];
patchdl_source_t src;
int en;
patchdl_manifest_t mf;
jbuf_t j = {0};
if (!get_title_action_info(tid, &src, purl, sizeof purl, pti, sizeof pti,
psti, sizeof psti, cid, sizeof cid,
nm, sizeof nm, ver, sizeof ver, &en) || !purl[0])
return queue_json(conn, MHD_HTTP_NOT_FOUND,
"{\"error\":\"no patch_url for title\"}");
memset(&mf, 0, sizeof mf);
if (patchdl_fetch_manifest(purl, &mf) != 0)
return queue_json(conn, MHD_HTTP_BAD_GATEWAY,
"{\"error\":\"manifest fetch failed\"}");
jbuf_appendf(&j, "{\"count\":%d,\"total\":%lld,\"pieces\":[",
mf.count, mf.total);
for (int i = 0; i < mf.count; i++) {
if (i) jbuf_append(&j, ",");
jbuf_appendf(&j, "{\"o\":%lld,\"s\":%lld,\"h\":\"%s\"}",
mf.pieces[i].offset, mf.pieces[i].size, mf.pieces[i].hash);
}
jbuf_append(&j, "]}");
patchdl_manifest_free(&mf);
return queue_json_owned(conn, MHD_HTTP_OK,
j.buf ? j.buf : strdup("{}"));
}
/* Read-only: SHA-256 every piece of the on-disk .pkg and compare to Sony's
manifest hashes — proves whether the assembled file is byte-correct. No
install. Hashing runs on-device (SSD), so no multi-GB transfer. */
if (!strncmp(url, "/api/pkgverify/", 15)) {
const char *tid = url + 15;
char purl[768] = {0}, pti[32], psti[32], cid[64], nm[128], ver[16];
char dest[320];
patchdl_source_t src;
int en, fd, okc = 0, badc = 0, first_bad = -1;
patchdl_manifest_t mf;
jbuf_t j = {0};
if (!get_title_action_info(tid, &src, purl, sizeof purl, pti, sizeof pti,
psti, sizeof psti, cid, sizeof cid,
nm, sizeof nm, ver, sizeof ver, &en) || !purl[0])
return queue_json(conn, MHD_HTTP_NOT_FOUND,
"{\"error\":\"no patch_url for title\"}");
title_pkg_path(tid, purl, dest, sizeof dest);
fd = open(dest, O_RDONLY);
if (fd < 0)
return queue_json(conn, MHD_HTTP_NOT_FOUND,
"{\"error\":\"pkg not on disk\"}");
memset(&mf, 0, sizeof mf);
if (patchdl_fetch_manifest(purl, &mf) != 0) {
close(fd);
return queue_json(conn, MHD_HTTP_BAD_GATEWAY,
"{\"error\":\"manifest fetch failed\"}");
}
jbuf_appendf(&j, "{\"count\":%d,\"pieces\":[", mf.count);
for (int i = 0; i < mf.count; i++) {
char got[80] = {0};
int ok = 0;
if (mf.pieces[i].hash[0] &&
patchdl_sha256_fd_region(fd, mf.pieces[i].offset,
mf.pieces[i].size, got) == 0)
ok = (strcasecmp(got, mf.pieces[i].hash) == 0);
if (ok) okc++; else { badc++; if (first_bad < 0) first_bad = i; }
if (i) jbuf_append(&j, ",");
jbuf_appendf(&j, "{\"i\":%d,\"o\":%lld,\"s\":%lld,\"ok\":%s}",
i, mf.pieces[i].offset, mf.pieces[i].size,
ok ? "true" : "false");
}
jbuf_appendf(&j, "],\"ok\":%d,\"bad\":%d,\"first_bad\":%d,\"all_ok\":%s}",
okc, badc, first_bad, (badc == 0 ? "true" : "false"));
patchdl_manifest_free(&mf);
close(fd);
return queue_json_owned(conn, MHD_HTTP_OK, j.buf ? j.buf : strdup("{}"));
}
/* Read-only: report the .pkg's embedded content id / title id vs the target
(installed app) ids, to expose the cross-region linkage. No install. */
if (!strncmp(url, "/api/pkgmeta/", 13)) {
const char *tid = url + 13;
char purl[768] = {0}, pti[32], psti[32], cid[64], nm[128], ver[16];
char dest[320], ecid[64] = {0}, etid[48] = {0}, msg[128], resp[900];
patchdl_source_t src;
int en, is_app = 0, rc;
if (!get_title_action_info(tid, &src, purl, sizeof purl, pti, sizeof pti,
psti, sizeof psti, cid, sizeof cid,
nm, sizeof nm, ver, sizeof ver, &en) || !purl[0])
return queue_json(conn, MHD_HTTP_NOT_FOUND, "{\"error\":\"unknown title\"}");
title_pkg_path(tid, purl, dest, sizeof dest);
rc = patchdl_install_pkg_meta(dest, ecid, sizeof ecid, etid, sizeof etid,
&is_app, msg, sizeof msg);
snprintf(resp, sizeof resp,
"{\"ok\":%s,\"pkg_content_id\":\"%s\",\"pkg_title_id\":\"%s\","
"\"is_app\":%s,\"target_content_id\":\"%s\",\"target_title_id\":\"%s\","
"\"storage_title_id\":\"%s\",\"msg\":\"%s\"}",
rc == 0 ? "true" : "false", ecid, etid, is_app ? "true" : "false",
cid, tid, psti, msg);
return queue_json_owned(conn, MHD_HTTP_OK, strdup(resp));
}
if (!strcmp(url, "/api/netcheck")) { if (!strcmp(url, "/api/netcheck")) {
char diag[1024] = "{\"error\":\"no title with version_file_uri\"}"; char diag[1024] = "{\"error\":\"no title with version_file_uri\"}";
pthread_mutex_lock(&g_mutex); pthread_mutex_lock(&g_mutex);