mirror of
https://github.com/knutwurst/patchdl.git
synced 2026-10-06 04:04:15 +02:00
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.
This commit is contained in:
1 parent
c80be921c5
commit
88368e3df3
4 files changed
+57
-22
No files matched your search
+20
-11
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
+29
-5
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user