diff --git a/src/patchdl_install.c b/src/patchdl_install.c index 872e86d..dacc80c 100644 --- a/src/patchdl_install.c +++ b/src/patchdl_install.c @@ -475,7 +475,10 @@ patchdl_install_status_json(char *out, size_t out_sz) { char tid[32]; char method[32]; int start_rc; - ai_install_status_t st; + /* ai_install_status_t carries a 2 KB safety pad; live on the heap so + a frequently-polled /api/installstatus doesn't keep committing pages + on every MHD worker's stack. */ + ai_install_status_t *st = NULL; char status[17], src_type[9]; int rc; int progress = 0; @@ -514,20 +517,25 @@ patchdl_install_status_json(char *out, size_t out_sz) { return -1; } + st = calloc(1, sizeof(*st)); + if (!st) { + snprintf(out, out_sz, "{\"error\":\"oom\"}"); + return -1; + } + /* sceAppInstUtilGetInstallStatus(char *content_id_out, status_t *status): first arg is an OUTPUT buffer that receives the current install's content_id. Do NOT pass `cid` there — it would be overwritten. The visible id fits in 0x30 bytes but Sony's NUL-pad length is unknown; use a padded buffer. */ { char ai_cid_out[AI_CONTENTID_OUT_SIZE] = {0}; - memset(&st, 0, sizeof(st)); - rc = ai_get_status(ai_cid_out, &st); + rc = ai_get_status(ai_cid_out, st); (void)ai_cid_out; /* returned content_id for future use */ } - copy_bounded(status, sizeof(status), st.status, sizeof(st.status)); - copy_bounded(src_type, sizeof(src_type), st.src_type, sizeof(st.src_type)); - if (st.total_size > 0) - progress = (int)((st.downloaded_size * 100) / st.total_size); + copy_bounded(status, sizeof(status), st->status, sizeof(st->status)); + copy_bounded(src_type, sizeof(src_type), st->src_type, sizeof(st->src_type)); + if (st->total_size > 0) + progress = (int)((st->downloaded_size * 100) / st->total_size); if (progress < 0) progress = 0; if (progress > 100) progress = 100; terminal = (!strcmp(status, "playable") || @@ -542,10 +550,11 @@ patchdl_install_status_json(char *out, size_t out_sz) { "\"promote_progress\":%u,\"error_code\":%d}", terminal ? "true" : "false", cid, tid, method, start_rc, rc, status, src_type, progress, - (unsigned long long)st.downloaded_size, - (unsigned long long)st.total_size, - (unsigned)st.promote_progress, - (int)st.error_info.error_code); + (unsigned long long)st->downloaded_size, + (unsigned long long)st->total_size, + (unsigned)st->promote_progress, + (int)st->error_info.error_code); + free(st); return rc; } diff --git a/src/patchdl_net.c b/src/patchdl_net.c index 8e63c3c..aa2b77a 100644 --- a/src/patchdl_net.c +++ b/src/patchdl_net.c @@ -31,9 +31,11 @@ #define PATCHDL_MAX_TOTAL_BYTES (200ULL * 1024 * 1024 * 1024) /* 200 GiB */ /* In-RAM buffer caps for full HTTP body fetches. version.xml is a few KB, - manifest JSON is a few MB at most — fail-closed beyond that. */ -#define PATCHDL_BUF_MAX_VERXML (16 * 1024 * 1024) -#define PATCHDL_BUF_MAX_MANIFEST (64 * 1024 * 1024) + manifest JSON is a few MB at most — fail-closed beyond that. The + manifest cap is sized for ~4096 pieces × ~3 KB JSON each with plenty of + headroom; real PS5 manifests are 1-2 MB. */ +#define PATCHDL_BUF_MAX_VERXML (4 * 1024 * 1024) +#define PATCHDL_BUF_MAX_MANIFEST (16 * 1024 * 1024) #ifdef PATCHDL_HAVE_CURL /* Replacement for fopen("wb"/"r+b") that refuses to follow a symlink at the diff --git a/src/patchdl_verxml.c b/src/patchdl_verxml.c index 9d09c21..bb39d86 100644 --- a/src/patchdl_verxml.c +++ b/src/patchdl_verxml.c @@ -183,10 +183,10 @@ patchdl_verxml_query(const char *url, uint32_t fw_bin, patchdl_verinfo_t *out) { if (!url || !out) return -1; memset(out, 0, sizeof(*out)); - /* version.xml is a few KB in practice; cap to 16 MiB so a misbehaving CDN - can't slurp unbounded RAM into the buffer. */ + /* 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 = 16 * 1024 * 1024; + buf.max = 4 * 1024 * 1024; if (patchdl_http_get(url, &buf)) return -1; if (!buf.data || !buf.size) { free(buf.data); return -1; } diff --git a/src/patchdl_websrv.c b/src/patchdl_websrv.c index 77bef75..840a482 100644 --- a/src/patchdl_websrv.c +++ b/src/patchdl_websrv.c @@ -283,7 +283,14 @@ queue_buffer(struct MHD_Connection *conn, unsigned int status, enum MHD_Result ret; resp = MHD_create_response_from_buffer(size, (void *)data, mm); - if (!resp) return MHD_NO; + if (!resp) { + /* MUST_FREE hands ownership to MHD on success; on failure we still + own it and have to free it ourselves or the caller's strdup'd + JSON leaks. PERSISTENT/MUST_COPY buffers are caller-owned and + we leave them alone. */ + if (mm == MHD_RESPMEM_MUST_FREE) free((void *)data); + return MHD_NO; + } MHD_add_response_header(resp, MHD_HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*"); MHD_add_response_header(resp, MHD_HTTP_HEADER_CACHE_CONTROL, "no-store"); @@ -1129,7 +1136,11 @@ write_job_state(const dl_job_t *job) { ps[] states and done_bytes. Called with the pool lock held. */ static void seed_from_sidecar(dl_job_t *job) { - char path[320], buf[16384], murl[768]; + /* 8 KB sidecar buffer: 4096-piece cap × 2 hex chars / 8 bits = 1024 hex + chars for the bitmap, plus the JSON wrapper and the up-to-768-byte + manifest_url — fits with room. Halved from 16 KB to lighten the + per-admit stack frame on the pool worker. */ + char path[320], buf[8192], murl[768]; FILE *f; size_t n; int nbytes = (job->mf.count + 7) / 8; @@ -2341,11 +2352,16 @@ patchdl_websrv_start(unsigned short port) { MHD_USE_INTERNAL_POLLING_THREAD | MHD_USE_THREAD_PER_CONNECTION, port, NULL, NULL, &on_request, NULL, MHD_OPTION_NOTIFY_COMPLETED, request_completed, NULL, - /* DoS guards: bound concurrent sockets + per-IP to keep a misbehaving - LAN client from exhausting pthreads on the PS5. */ - MHD_OPTION_CONNECTION_LIMIT, (unsigned int)64, + /* DoS guards + RAM caps: the PS5 process budget is a few hundred MB + and the default pthread stack on this libc is multiple MB. We're + a single-user UI — 8 concurrent connections is plenty for the + SSE poll + a couple of AJAX, and 256 KB per worker is more than + enough for our handlers (largest stack use is a 16 KB sidecar + buffer in seed_from_sidecar). */ + MHD_OPTION_CONNECTION_LIMIT, (unsigned int)8, MHD_OPTION_PER_IP_CONNECTION_LIMIT, (unsigned int)8, MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)30, + MHD_OPTION_THREAD_STACK_SIZE, (size_t)(512 * 1024), MHD_OPTION_END); if (!web_daemon) { @@ -2398,11 +2414,19 @@ patchdl_websrv_stop(void) { for (int s = 0; s < g_pool.n_workers; s++) pthread_join(g_pool.workers[s], NULL); g_pool.n_workers = 0; + /* Free every job still in the pool list. Workers have joined so nobody + else touches the list. free_job_locked unlinks + frees mf.pieces[i].url, + mf.pieces, ps, bitmap, and closes the fd. */ + pthread_mutex_lock(&g_pool.mtx); + while (g_pool.jobs) free_job_locked(g_pool.jobs); + pthread_mutex_unlock(&g_pool.mtx); patchdl_net_global_cleanup(); pthread_mutex_lock(&g_mutex); patchdl_scan_free(g_titles, g_title_count); g_titles = NULL; g_title_count = 0; + free(g_debug_json); + g_debug_json = NULL; pthread_mutex_unlock(&g_mutex); }