Files
knutwurst--ps5-patchdl/src/patchdl_verxml.c
T
Knutwurst 88368e3df3 Plug response-OOM leak, cap RAM, free pool on shutdown
queue_buffer leaked the MUST_FREE payload (every queue_json_owned/
build_*_json/strdup(resp)) when MHD_create_response_from_buffer hit OOM.
The MUST_FREE contract hands ownership to MHD only on success — on the
NULL return path the caller still owns the buffer, so free it before
bailing. Critical under memory pressure where the first OOM turns into
a cascade.

patchdl_websrv_stop walked the verxml thread + workers but never freed
the remaining dl_job_t entries or g_debug_json. Today main never calls
the function, but the early-failure path inside patchdl_websrv_start
does, and any later graceful-shutdown work would hit the same leak.
Drain g_pool.jobs through free_job_locked under the pool lock; free
g_debug_json under g_mutex.

RAM caps:

- MHD: CONNECTION_LIMIT 64 -> 8 (single-user UI), and
  THREAD_STACK_SIZE = 512 KB. THREAD_PER_CONNECTION on libc's default
  pthread stack (multi-MB) was reserving hundreds of MB of VM per
  burst; the largest stack frame in any handler is the 8 KB sidecar
  buffer, so 512 KB is generous even with curl + openssl in the path.
- patchdl_buf_t caps: manifest 64 -> 16 MiB, version.xml 16 -> 4 MiB.
  Real PS5 manifests are 1-2 MiB; the old caps allowed 64 MiB per
  fetch with multiple fetches possible in flight.
- patchdl_install_status_json: ai_install_status_t (2 KB pad) moves
  from the MHD worker stack to a calloc/free pair so a polling browser
  doesn't keep committing pages on each /api/installstatus tick.
- seed_from_sidecar: 16 KB stack buf -> 8 KB. 4096-piece cap fits with
  the JSON wrapper inside 8 KB.

No functional changes; download/install/scan/tile paths unaffected.
2026-06-25 06:10:48 +02:00

198 lines
6.6 KiB
C

#include "patchdl_verxml.h"
#include "patchdl_net.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Convert packed fw bin to "11.60" string */
static void
fw_bin_to_str(uint32_t bin, char *out, size_t sz) {
/* Packed firmware: hex digits read as decimal (0x10200006 -> "10.20"),
same scheme as patchdl_fw. */
snprintf(out, sz, "%x.%02x",
(bin >> 24) & 0xff,
(bin >> 16) & 0xff);
}
static uint32_t
parse_hex(const char *s) {
uint32_t v = 0;
if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X'))
sscanf(s + 2, "%x", &v);
else
sscanf(s, "%u", &v);
return v;
}
/* Extract attribute value from within a <package ...> tag span.
tag_start points to '<', tag_end points past '>'. */
static int
attr_val(const char *tag_start, const char *tag_end, const char *attr,
char *out, size_t out_sz) {
char needle[64];
const char *p;
size_t len;
if ((size_t)snprintf(needle, sizeof(needle), " %s=\"", attr) >= sizeof(needle))
return -1;
p = tag_start;
while (p < tag_end) {
p = strstr(p, needle);
if (!p || p >= tag_end) return -1;
p += strlen(needle);
for (len = 0; p + len < tag_end && p[len] != '"' && len < out_sz - 1; len++)
;
/* The value must actually end on its closing quote inside the tag —
otherwise we hit tag_end or the buffer limit and would return a
silently truncated URL/title as if it were valid. */
if (p + len >= tag_end || p[len] != '"')
return -1;
memcpy(out, p, len);
out[len] = '\0';
return 0;
}
return -1;
}
/* version strings are zero-padded (e.g. "01.041.000") — strcmp orders correctly */
static int
ver_gt(const char *a, const char *b) {
return strcmp(a, b) > 0;
}
/* Extract the first PS4/PS5 title id token ([A-Z]{4}[0-9]{5}, e.g. PPSA03099)
from a string such as nptitleid, manifest_url, or delta_url. */
static void
extract_title_id(const char *s, char *out, size_t sz) {
size_t len;
out[0] = '\0';
if (sz < 10 || !s) return;
len = strlen(s);
if (len < 9) return;
/* `len - 9` is the last position where a 9-char id can still fit; this
avoids reading p[8] past the NUL terminator. */
for (size_t i = 0; i <= len - 9; i++) {
const char *p = s + i;
int ok = 1;
for (int k = 0; k < 4 && ok; k++)
if (p[k] < 'A' || p[k] > 'Z') ok = 0;
for (int k = 4; k < 9 && ok; k++)
if (p[k] < '0' || p[k] > '9') ok = 0;
if (ok) {
memcpy(out, p, 9);
out[9] = '\0';
return;
}
}
}
static void
parse_root_title_id(const char *xml, char *out, size_t sz) {
const char *p;
const char *tag_end;
char nptitleid[64] = {0};
out[0] = '\0';
if (!xml || sz < 10) return;
p = strstr(xml, "<title_patch");
if (!p) return;
tag_end = strchr(p, '>');
if (!tag_end) return;
tag_end++;
if (!attr_val(p, tag_end, "nptitleid", nptitleid, sizeof(nptitleid)))
extract_title_id(nptitleid, out, sz);
}
static void
parse_packages(const char *xml, uint32_t fw_bin, patchdl_verinfo_t *out) {
const char *p = xml;
char root_title[16] = {0};
parse_root_title_id(xml, root_title, sizeof(root_title));
while ((p = strstr(p, "<package "))) {
const char *tag_end = strchr(p, '>');
if (!tag_end) break;
tag_end++;
char ver[16] = {0};
char sver[16] = {0};
char durl[512] = {0};
char murl[512] = {0};
/* PS5 version.xml uses content_ver; PS4 uses version. */
if (attr_val(p, tag_end, "content_ver", ver, sizeof(ver)))
attr_val(p, tag_end, "version", ver, sizeof(ver));
attr_val(p, tag_end, "system_ver", sver, sizeof(sver));
attr_val(p, tag_end, "delta_url", durl, sizeof(durl));
attr_val(p, tag_end, "manifest_url", murl, sizeof(murl));
if (ver[0] && sver[0]) {
uint32_t pkg_sver = parse_hex(sver);
/* Track latest overall */
if (!out->latest_version[0] || ver_gt(ver, out->latest_version)) {
strncpy(out->latest_version, ver, sizeof(out->latest_version) - 1);
fw_bin_to_str(pkg_sver, out->latest_required_fw,
sizeof(out->latest_required_fw));
}
/* Track latest compatible + its installable patch source. PS5
updates expose a small delta_url (DP.pkg) plus a manifest_url
containing the actual split package pieces. Feeding the DP
bootstrap directly can make the system download the full patch
under the storage/master title id, so prefer the target-title
manifest whenever present. */
if (pkg_sver <= fw_bin) {
if (!out->compatible_version[0] ||
ver_gt(ver, out->compatible_version)) {
strncpy(out->compatible_version, ver,
sizeof(out->compatible_version) - 1);
strncpy(out->compatible_url, murl[0] ? murl : durl,
sizeof(out->compatible_url) - 1);
if (durl[0])
strncpy(out->delta_url, durl, sizeof(out->delta_url) - 1);
extract_title_id(durl, out->compatible_storage_title,
sizeof(out->compatible_storage_title));
if (root_title[0]) {
strncpy(out->compatible_title, root_title,
sizeof(out->compatible_title) - 1);
} else {
extract_title_id(murl, out->compatible_title,
sizeof(out->compatible_title));
if (!out->compatible_title[0])
extract_title_id(durl, out->compatible_title,
sizeof(out->compatible_title));
}
}
}
}
p = tag_end;
}
}
int
patchdl_verxml_query(const char *url, uint32_t fw_bin, patchdl_verinfo_t *out) {
patchdl_buf_t buf;
if (!url || !out) return -1;
memset(out, 0, sizeof(*out));
/* version.xml is a few KB in practice; cap so a misbehaving CDN can't
slurp unbounded RAM into the buffer. */
memset(&buf, 0, sizeof(buf));
buf.max = 4 * 1024 * 1024;
if (patchdl_http_get(url, &buf)) return -1;
if (!buf.data || !buf.size) { free(buf.data); return -1; }
parse_packages(buf.data, fw_bin, out);
free(buf.data);
return (out->latest_version[0]) ? 0 : -1;
}