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,
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_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
@@ -205,6 +207,8 @@ backend_init_thread(void *arg) {
"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();
@@ -284,6 +288,56 @@ patchdl_install_api_probe(char *out, size_t out_sz) {
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,
+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
firmware. Writes a JSON object into `out`. No install, no side effects. */
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;
}
/* 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
patchdl_manifest_free(patchdl_manifest_t *m) {
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,
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
cancel large patch downloads); return 0 to continue. */
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"))
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")) {
char diag[1024] = "{\"error\":\"no title with version_file_uri\"}";
pthread_mutex_lock(&g_mutex);