OOM/stack protection: cap POST body, validate basename, heap playgo

POST handler: cap accumulated body at PATCHDL_POST_MAX_BYTES (64 KiB).
A LAN client streaming gigabytes into /api/config would otherwise grow
the per-connection buffer until OOM-kill. Past the cap, further chunks
are dropped and the final call returns 413.

title_pkg_path: the basename comes from the patch_url and ultimately
from version.xml via the Sony CDN. A poisoned manifest with a basename
like ".." or one containing delimiters would compose a dest path that
escapes /data/patchdl/<tid>/. Validate the basename through
path_segment_safe and fall back to "<title_id>.pkg" on rejection.
cleanup_installed_download now routes through the same helper instead
of doing its own basename extraction.

ai_install_by_package's playgo struct is 0x2700 bytes — comfortably
fine on its own, but on the MHD worker stack alongside meta/pkg/uris
buffers and Sony's own frame use it leaves little headroom. Move it to
the heap in both patchdl_install_local_pkg and patchdl_install_by_uri.
This commit is contained in:
Knutwurst committed 2026-06-24 20:32:45 +02:00
1 parent 5ba72b850c
commit a6d9f939b5
2 files changed
+70 -28

No files matched your search

+29 -16
View File
@@ -10,6 +10,7 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/stat.h> #include <sys/stat.h>
@@ -614,16 +615,23 @@ patchdl_install_local_pkg(const char *local_path, const char *expected_title_id,
carry a different title id than the installed game; passing content_id carry a different title id than the installed game; passing content_id
is what etaHEN does to route the install correctly. */ is what etaHEN does to route the install correctly. */
{ {
char file_uri[1100]; char file_uri[1100];
char http_loop_uri[1200] = {0}; char http_loop_uri[1200] = {0};
char http_lan_uri[1200] = {0}; char http_lan_uri[1200] = {0};
const char *uris[4]; const char *uris[4];
ai_meta_info_t meta = {0}; ai_meta_info_t meta = {0};
ai_pkg_info_t pkg = {0}; ai_pkg_info_t pkg = {0};
ai_playgo_info_t playgo = {0}; /* playgo is 0x2700 bytes — too big for the MHD worker stack alongside
int rc2 = -1; uris, meta, pkg, resp buffers, and Sony's own frame use. */
const char *title_dir; ai_playgo_info_t *playgo = calloc(1, sizeof(*playgo));
const char *file_base; int rc2 = -1;
const char *title_dir;
const char *file_base;
if (!playgo) {
snprintf(msg, msg_sz, "out of memory");
return -1;
}
snprintf(file_uri, sizeof(file_uri), "file://%s", sdk_path); snprintf(file_uri, sizeof(file_uri), "file://%s", sdk_path);
title_dir = strstr(local_path, "/data/patchdl/"); title_dir = strstr(local_path, "/data/patchdl/");
@@ -670,9 +678,9 @@ patchdl_install_local_pkg(const char *local_path, const char *expected_title_id,
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
if (!uris[i]) continue; if (!uris[i]) continue;
memset(&pkg, 0, sizeof(pkg)); memset(&pkg, 0, sizeof(pkg));
memset(&playgo, 0, sizeof(playgo)); memset(playgo, 0, sizeof(*playgo));
meta.uri = uris[i]; meta.uri = uris[i];
rc2 = ai_install_by_package(&meta, &pkg, &playgo); rc2 = ai_install_by_package(&meta, &pkg, playgo);
{ {
size_t l = strlen(tries); size_t l = strlen(tries);
snprintf(tries + l, sizeof(tries) - l, "%s%s=0x%08x", snprintf(tries + l, sizeof(tries) - l, "%s%s=0x%08x",
@@ -684,11 +692,13 @@ patchdl_install_local_pkg(const char *local_path, const char *expected_title_id,
snprintf(msg, msg_sz, "install started (InstallByPackage, content %.47s)", snprintf(msg, msg_sz, "install started (InstallByPackage, content %.47s)",
pkg.content_id[0] ? pkg.content_id : pkg.content_id[0] ? pkg.content_id :
(target_content_id ? target_content_id : "")); (target_content_id ? target_content_id : ""));
free(playgo);
return 0; return 0;
} }
} }
rc = rc2; rc = rc2;
} }
free(playgo);
} }
/* AppInstallPkg: simpler API, no MetaInfo content_id override. Tried for /* AppInstallPkg: simpler API, no MetaInfo content_id override. Tried for
@@ -735,9 +745,9 @@ int
patchdl_install_by_uri(const char *uri, const char *target_title_id, patchdl_install_by_uri(const char *uri, const char *target_title_id,
const char *target_content_id, const char *target_content_id,
char *msg, size_t msg_sz) { char *msg, size_t msg_sz) {
ai_meta_info_t meta = {0}; ai_meta_info_t meta = {0};
ai_pkg_info_t pkg = {0}; ai_pkg_info_t pkg = {0};
ai_playgo_info_t playgo = {0}; ai_playgo_info_t *playgo;
int rc; int rc;
if (!uri || !uri[0]) { if (!uri || !uri[0]) {
@@ -749,6 +759,8 @@ patchdl_install_by_uri(const char *uri, const char *target_title_id,
snprintf(msg, msg_sz, "install backend not ready: %s", stage_str(g_stage)); snprintf(msg, msg_sz, "install backend not ready: %s", stage_str(g_stage));
return -1; return -1;
} }
playgo = calloc(1, sizeof(*playgo));
if (!playgo) { snprintf(msg, msg_sz, "out of memory"); return -1; }
meta.uri = uri; meta.uri = uri;
meta.ex_uri = ""; meta.ex_uri = "";
@@ -757,8 +769,9 @@ patchdl_install_by_uri(const char *uri, const char *target_title_id,
meta.content_name = "PatchDL"; meta.content_name = "PatchDL";
meta.icon_url = ""; meta.icon_url = "";
rc = ai_install_by_package(&meta, &pkg, &playgo); rc = ai_install_by_package(&meta, &pkg, playgo);
remember_install(target_title_id, "InstallByURI", &pkg, target_content_id, rc); remember_install(target_title_id, "InstallByURI", &pkg, target_content_id, rc);
free(playgo);
if (rc == 0) { if (rc == 0) {
snprintf(msg, msg_sz, "install started (InstallByPackage/uri, content %.47s)", snprintf(msg, msg_sz, "install started (InstallByPackage/uri, content %.47s)",
+41 -12
View File
@@ -811,9 +811,12 @@ build_downloads_json(void) {
here at the next scan — not during the async install, which still reads the here at the next scan — not during the async install, which still reads the
file. */ file. */
static void static void
title_pkg_path(const char *title_id, const char *patch_url,
char *out, size_t out_sz);
static void
cleanup_installed_download(const char *title_id, const char *patch_url) { cleanup_installed_download(const char *title_id, const char *patch_url) {
char dir[256], path[320]; char dir[256], path[320];
const char *base = strrchr(patch_url, '/');
if (!path_segment_safe(title_id)) if (!path_segment_safe(title_id))
return; return;
/* Never delete a directory the download pool still owns (a queued/active/ /* Never delete a directory the download pool still owns (a queued/active/
@@ -825,9 +828,11 @@ cleanup_installed_download(const char *title_id, const char *patch_url) {
return; return;
} }
pthread_mutex_unlock(&g_pool.mtx); pthread_mutex_unlock(&g_pool.mtx);
base = base ? base + 1 : "patch.pkg";
snprintf(dir, sizeof(dir), "/data/patchdl/%s", title_id); snprintf(dir, sizeof(dir), "/data/patchdl/%s", title_id);
snprintf(path, sizeof(path), "%s/%s", dir, base); /* Reuse title_pkg_path so the basename is validated the same way as on
download — a basename with .. or delimiters falls back to "<tid>.pkg"
and we don't end up unlinking outside the title directory. */
title_pkg_path(title_id, patch_url, path, sizeof(path));
unlink(path); unlink(path);
rmdir(dir); rmdir(dir);
} }
@@ -942,7 +947,12 @@ set_title_enabled(struct MHD_Connection *conn, const char *title_id, int en) {
return queue_json_owned(conn, MHD_HTTP_OK, strdup(r)); return queue_json_owned(conn, MHD_HTTP_OK, strdup(r));
} }
/* Local on-disk path a title's patch downloads to / installs from. */ /* Local on-disk path a title's patch downloads to / installs from. The
basename comes from the patch_url (CDN-controlled), so it MUST be
validated — a malicious or corrupted version.xml could otherwise yield
"..", an empty string, or a path with delimiters that escape the title
directory when concatenated. Fallback to a deterministic per-title name
on any rejection so file ops still land somewhere safe. */
static void static void
title_pkg_path(const char *title_id, const char *patch_url, title_pkg_path(const char *title_id, const char *patch_url,
char *out, size_t out_sz) { char *out, size_t out_sz) {
@@ -950,11 +960,15 @@ title_pkg_path(const char *title_id, const char *patch_url,
char name[192]; char name[192];
size_t n; size_t n;
base = base ? base + 1 : "patch.pkg"; base = base ? base + 1 : "";
snprintf(name, sizeof(name), "%s", base); snprintf(name, sizeof(name), "%s", base);
n = strlen(name); n = strlen(name);
if (n > 5 && !strcmp(name + n - 5, ".json")) if (n > 5 && !strcmp(name + n - 5, ".json"))
snprintf(name + n - 5, sizeof(name) - (n - 5), ".pkg"); snprintf(name + n - 5, sizeof(name) - (n - 5), ".pkg");
if (!name[0] || !path_segment_safe(name)) {
snprintf(name, sizeof(name), "%s.pkg",
path_segment_safe(title_id) ? title_id : "patch");
}
snprintf(out, out_sz, "%s/%s/%s", PATCHDL_DL_DIR, title_id, name); snprintf(out, out_sz, "%s/%s/%s", PATCHDL_DL_DIR, title_id, name);
} }
@@ -1777,10 +1791,15 @@ handle_title_action(struct MHD_Connection *conn, const char *url) {
/* ---------- POST body accumulation ------------------------------------- */ /* ---------- POST body accumulation ------------------------------------- */
/* MHD delivers a POST body across several callback invocations. We stash a /* MHD delivers a POST body across several callback invocations. We stash a
growing buffer in con_cls and dispatch once the body is complete. */ growing buffer in con_cls and dispatch once the body is complete. The
largest legitimate body we accept is the config JSON or a small install
request — a few hundred bytes; 64 KiB leaves ample headroom while keeping
a misbehaving LAN client from forcing unbounded allocations. */
#define PATCHDL_POST_MAX_BYTES (64 * 1024)
typedef struct { typedef struct {
char *data; char *data;
size_t len; size_t len;
int oversized;
} post_body_t; } post_body_t;
static void static void
@@ -1857,17 +1876,27 @@ on_request(void *cls, struct MHD_Connection *conn, const char *url,
} }
if (*upload_data_size) { /* a body chunk: append it */ if (*upload_data_size) { /* a body chunk: append it */
char *n = realloc(pb->data, pb->len + *upload_data_size + 1); if (!pb->oversized &&
if (n) { pb->len + *upload_data_size <= PATCHDL_POST_MAX_BYTES) {
memcpy(n + pb->len, upload_data, *upload_data_size); char *n = realloc(pb->data, pb->len + *upload_data_size + 1);
pb->len += *upload_data_size; if (n) {
n[pb->len] = '\0'; memcpy(n + pb->len, upload_data, *upload_data_size);
pb->data = n; pb->len += *upload_data_size;
n[pb->len] = '\0';
pb->data = n;
} else {
pb->oversized = 1;
}
} else {
pb->oversized = 1; /* drop further chunks to bound RAM */
} }
*upload_data_size = 0; *upload_data_size = 0;
return MHD_YES; return MHD_YES;
} }
if (pb->oversized)
return queue_json(conn, MHD_HTTP_CONTENT_TOO_LARGE,
"{\"ok\":false,\"reason\":\"body_too_large\"}");
/* final call: the full body (if any) is in pb->data */ /* final call: the full body (if any) is in pb->data */
const char *body = pb->data ? pb->data : ""; const char *body = pb->data ? pb->data : "";
if (!strcmp(url, "/api/config")) if (!strcmp(url, "/api/config"))