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.
Manifest JSON parser used substring scans with no per-piece scope; a key
defined in a later piece could be misattributed to the current one, and
the escape handling silently dropped the byte after a backslash even for
unknown escapes. json_string_after/json_u64_after now take an optional
limit pointer (NULL = legacy unbounded), and the manifest loops pass
obj_end so per-piece reads can't leak across pieces. JSON escapes are
decoded properly: \" \\ \/ \n \r \t \b \f; unknown \X drops the
backslash and keeps the payload byte.
Sanity caps on assembled manifests: PATCHDL_MAX_PIECES (4096),
PATCHDL_MAX_PIECE_BYTES (8 GiB), PATCHDL_MAX_TOTAL_BYTES (200 GiB).
A malformed manifest with a single multi-TB piece or millions of entries
is now rejected before any disk activity.
patchdl_buf_t gains an optional `max` field; write_cb fails the transfer
when growth would exceed it. patchdl_http_get preserves the caller-set
max across its internal memset(). verxml_query caps at 16 MiB,
fetch_manifest and download_manifest at 64 MiB.
host_allowed switches to strcasecmp (DNS is case-insensitive; an upstream
redirect could otherwise drop out of the list). CURLOPT_PROTOCOLS_STR /
REDIR_PROTOCOLS_STR pin all traffic and redirects to HTTPS.
CURLOPT_NOSIGNAL=1 prevents libcurl from raising SIGPIPE in a worker.
DNS label parser bounds-checks the length byte before incrementing pos,
so a malformed response with a 0xFF label near the end can no longer
read past the receive buffer.
extract_title_id used `p[8]` as the loop guard, which crossed the NUL
terminator on strings shorter than 9 chars (UB). Replaced with a
strlen-based bound.
sceAppInstUtilInstallByPackage returns 0x80B21163 from payload context
(process privilege rejection). sceAppInstUtilAppInstallPkg accepts the
same PKG with rc=0 and does install it. Switch all install paths to use
AppInstallPkg.
do_install (cross-region): require assembled PKG from the manifest
download; InstallByPackage and DP.pkg fallbacks are removed since both
are dead ends in this process context. do_install (same-region): also
switched to AppInstallPkg. do_download: removed the DP.pkg shortcut so
the manifest-assembled full PKG is downloaded as before.
sceAppInstUtilGetInstallStatus ABI: first arg is an output buffer for
the current install's content_id, not an input query. Passing our
tracking buffer there was overwriting it with zeros (disc game has no
explicit content_id). Fix: use a fresh output buffer; keep `cid` from
g_last_content_id untouched. Also add 2048-byte padding to
ai_install_status_t against firmware struct size variance.
New APIs: patchdl_install_by_uri, patchdl_install_app_pkg,
patchdl_install_debug_state. New endpoints: /api/install_uri,
/api/install_aip, /api/debug_install. delta_url propagated through
verxml → scan → websrv for future DP.pkg tracking.
Verified on device (FW 11.60, BD-JB+PPPwn): Dead Island 2 PPSA03099
updated from 01.000.001 to 01.000.011.
Cancel a running download (the worker aborts mid-piece and the partial
file is removed) or delete a finished package, from the queue or the
title card. The progress callback now returns an abort signal that
reaches libcurl and the manifest merge loop.
Manifest merge: bound the piece scan to the "pieces" array so a later
"url" key (e.g. playgoChunkCrcUrl) can't be appended as a bogus piece,
and require each piece's fileOffset to match the bytes written so far so
an out-of-order manifest fails instead of silently producing a corrupt
package.
Report real free space on the download partition via statvfs; it was a
hardcoded 0.
Fixes found in review:
- scan: bound the SFO entry table to the bytes actually read and require
the key to be NUL-terminated before strcmp (OOB read on a crafted
param.sfo from a shadow-mounted dir).
- proc: bound the kinfo_proc walk and the name compare to the record and
the buffer.
- install: publish the API probe under the lock (data race with the MHD
worker thread) and initialize rc2.
- verxml: reject a truncated attribute value instead of returning it as
valid.
- web: keep download/install/downloaded flags across a refresh, stop the
queue poll only after repeated empty results, coerce the progress
number, and treat a cancelled download (HTTP 200, ok:false) as
not-downloaded.
Standalone PS5 payload with an embedded web UI on :12880, no etaHEN.
- Scans installed titles and classifies the source (genuine install,
ShadowMountPlus mount, preinstall, unknown) from the on-disk layout.
- Reads name, installed version and the Sony version.xml URL from the PS5
app database (vendored SQLite).
- Resolves version.xml past nanoDNS via a raw DNS query, TLS pinned to the
SCEI DNAS root.
- Filters patches to the newest one compatible with the current firmware.
- Downloads the patch and installs it through AppInstUtil (sysmodule loaded
at runtime), gated to genuine installs with a package title-id match guard.
- Action-based UI filters and an on-screen startup notification with the URL.