diff --git a/src/patchdl_net.c b/src/patchdl_net.c index 2bd2e07..e6cd0c7 100644 --- a/src/patchdl_net.c +++ b/src/patchdl_net.c @@ -488,8 +488,11 @@ http_download_to_file_progress(const char *url, FILE *fp, long long *bytes_out, unsigned char dig[EVP_MAX_MD_SIZE]; unsigned int dlen = 0; char hex[2 * EVP_MAX_MD_SIZE + 1]; - EVP_DigestFinal_ex(sink.md, dig, &dlen); + int ok = EVP_DigestFinal_ex(sink.md, dig, &dlen); EVP_MD_CTX_free(sink.md); + /* Fail-closed on a digest API failure — otherwise hex would be empty + and we'd silently report -2 with no diagnostic. */ + if (ok != 1 || dlen == 0) return -2; hex_encode(dig, dlen, hex, sizeof(hex)); if (strcasecmp(hex, expected_sha256_hex) != 0) return -2; /* integrity mismatch */ @@ -893,8 +896,9 @@ patchdl_http_download_piece(const char *url, int fd, unsigned char dig[EVP_MAX_MD_SIZE]; unsigned int dl = 0; char hex[2 * EVP_MAX_MD_SIZE + 1]; - EVP_DigestFinal_ex(sink.md, dig, &dl); + int ok = EVP_DigestFinal_ex(sink.md, dig, &dl); EVP_MD_CTX_free(sink.md); + if (ok != 1 || dl == 0) return -2; hex_encode(dig, dl, hex, sizeof(hex)); if (strcasecmp(hex, expected_sha256_or_null) != 0) return -2; /* integrity mismatch */ @@ -929,8 +933,9 @@ patchdl_sha256_fd_region(int fd, long long offset, long long size, char *out_hex { unsigned char dig[EVP_MAX_MD_SIZE]; unsigned int dl = 0, i; - EVP_DigestFinal_ex(md, dig, &dl); - for (i = 0; i < dl; i++) sprintf(out_hex + 2 * i, "%02x", dig[i]); + int ok = EVP_DigestFinal_ex(md, dig, &dl); + if (ok != 1 || dl == 0) { free(buf); EVP_MD_CTX_free(md); return -1; } + for (i = 0; i < dl; i++) snprintf(out_hex + 2 * i, 3, "%02x", dig[i]); out_hex[2 * dl] = '\0'; } free(buf); diff --git a/src/patchdl_scan.c b/src/patchdl_scan.c index 496b436..e85398c 100644 --- a/src/patchdl_scan.c +++ b/src/patchdl_scan.c @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -389,6 +390,15 @@ merge_appdb(patchdl_title_t *arr, size_t cnt) { patchdl_appdb_free(info); } +/* See patchdl_scan_lock — both vnode-swap entry points return -1 / NULL once + this is set so a late rescan call can't race the running MHD threads. */ +static _Atomic int g_scan_locked = 0; + +void +patchdl_scan_lock(void) { + atomic_store(&g_scan_locked, 1); +} + int patchdl_scan(patchdl_title_t **titles_out, size_t *count_out) { patchdl_title_t *arr; @@ -400,6 +410,8 @@ patchdl_scan(patchdl_title_t **titles_out, size_t *count_out) { struct statfs *mounts = NULL; int nmounts; + if (atomic_load(&g_scan_locked)) return -1; + arr = calloc(MAX_TITLES, sizeof(*arr)); if (!arr) return -1; @@ -467,6 +479,8 @@ patchdl_scan_debug_json(void) { char tmp[2048]; pid_t pid = getpid(); intptr_t saved_root = 0, root_vnode; + + if (atomic_load(&g_scan_locked)) return NULL; int using_vswap = 0; struct statfs *mounts = NULL; int nmounts; diff --git a/src/patchdl_scan.h b/src/patchdl_scan.h index 6a20f18..a3ef40e 100644 --- a/src/patchdl_scan.h +++ b/src/patchdl_scan.h @@ -40,3 +40,9 @@ const char *patchdl_source_str(patchdl_source_t src); /* Diagnostic: malloc'd JSON dump of the mount table + scan-base directory listings. Caller frees. */ char *patchdl_scan_debug_json(void); + +/* Mark scan/debug as no longer safe to call (must be set after MHD worker + threads come up — patchdl_scan performs a process-wide vnode swap that + would race any concurrent thread). After this is set, both entry points + return immediately. Call once during startup, after MHD_start_daemon. */ +void patchdl_scan_lock(void); diff --git a/src/patchdl_websrv.c b/src/patchdl_websrv.c index f2528f7..c37beae 100644 --- a/src/patchdl_websrv.c +++ b/src/patchdl_websrv.c @@ -162,7 +162,10 @@ json_get_int(const char *s, const char *key, int dflt) { return (int)strtol(p, NULL, 10); } -/* Find `"key":"value"` and copy value into out. */ +/* Find `"key":"value"` and copy value into out. Decodes the common JSON + string escapes (\" \\ \/ \n \r \t \b \f). \uXXXX is collapsed to '?' — + we don't accept multi-byte content in any field here. An unknown escape + keeps the payload byte. */ static void json_get_str(const char *s, const char *key, char *out, size_t sz) { out[0] = '\0'; @@ -177,7 +180,31 @@ json_get_str(const char *s, const char *key, char *out, size_t sz) { if (*p != '"') return; p++; size_t i = 0; - while (*p && *p != '"' && i + 1 < sz) out[i++] = *p++; + while (*p && *p != '"' && i + 1 < sz) { + if (*p == '\\' && p[1]) { + p++; + switch (*p) { + case '"': out[i++] = '"'; break; + case '\\': out[i++] = '\\'; break; + case '/': out[i++] = '/'; break; + case 'n': out[i++] = '\n'; break; + case 'r': out[i++] = '\r'; break; + case 't': out[i++] = '\t'; break; + case 'b': out[i++] = '\b'; break; + case 'f': out[i++] = '\f'; break; + case 'u': + /* \uXXXX: not needed for any field we accept; drop to '?' so + neither the surrogate pair nor the hex digits leak. */ + if (p[1] && p[2] && p[3] && p[4]) p += 4; + out[i++] = '?'; + break; + default: out[i++] = *p; break; + } + p++; + } else { + out[i++] = *p++; + } + } out[i] = '\0'; } @@ -1820,6 +1847,11 @@ handle_config_post(struct MHD_Connection *conn, const char *body) { int mc; json_get_str(body, "default_policy", pol, sizeof(pol)); + /* Only the two real values are accepted; anything else is silently + dropped so a malformed POST can't corrupt config.json or the JSON + we later round-trip out of /api/config. */ + if (pol[0] && strcmp(pol, "allow") != 0 && strcmp(pol, "deny") != 0) + pol[0] = '\0'; pthread_mutex_lock(&g_mutex); if (pol[0]) { @@ -2037,6 +2069,8 @@ on_request(void *cls, struct MHD_Connection *conn, const char *url, patchdl_manifest_t mf; jbuf_t j = {0}; + if (!path_segment_safe(tid)) + return queue_text(conn, MHD_HTTP_FORBIDDEN, "forbidden"); if (!get_title_action_info(tid, &src, purl, sizeof purl, durl_, sizeof durl_, pti, sizeof pti, @@ -2074,6 +2108,8 @@ on_request(void *cls, struct MHD_Connection *conn, const char *url, patchdl_manifest_t mf; jbuf_t j = {0}; + if (!path_segment_safe(tid)) + return queue_text(conn, MHD_HTTP_FORBIDDEN, "forbidden"); if (!get_title_action_info(tid, &src, purl, sizeof purl, durl_, sizeof durl_, pti, sizeof pti, @@ -2123,6 +2159,8 @@ on_request(void *cls, struct MHD_Connection *conn, const char *url, patchdl_source_t src; int en, is_app = 0, rc; + if (!path_segment_safe(tid)) + return queue_text(conn, MHD_HTTP_FORBIDDEN, "forbidden"); if (!get_title_action_info(tid, &src, purl, sizeof purl, durl_, sizeof durl_, pti, sizeof pti, @@ -2276,6 +2314,11 @@ patchdl_websrv_start(unsigned short port) { return -1; } + /* Lock further patchdl_scan / patchdl_scan_debug_json calls — both do a + process-wide vnode swap that is only safe before MHD worker threads + come up. After this point the only scan happens internally above. */ + patchdl_scan_lock(); + /* Start background verxml fetch — joinable so patchdl_websrv_stop can wait for it before freeing g_titles (it reads g_titles across blocking queries). */ g_verxml_stop = 0;