mirror of
https://github.com/knutwurst/patchdl.git
synced 2026-10-06 07:00:26 +02:00
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:
1 parent
5ba72b850c
commit
a6d9f939b5
2 files changed
+70
-28
No files matched your search
+29
-16
@@ -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
@@ -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"))
|
||||||
|
|||||||
Reference in new issue
Block a user