- C 60.8%
- JavaScript 20.8%
- Python 8.5%
- Shell 3.6%
- CSS 3.2%
- Other 3.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
rd_transfers_active() treats the whole data folder as reserved, so the previous check refused every kept-upload delete. |
||
| .forgejo/workflows | ||
| assets/ui | ||
| docs | ||
| include/raring | ||
| src | ||
| tests | ||
| tools | ||
| .gitignore | ||
| Containerfile | ||
| LICENSE | ||
| Makefile | ||
| NOTICE.md | ||
| README.md | ||
Raring DPI
Raring DPI is a standalone PS5 package-installer payload built with
ps5-payload-sdk. It provides the useful DPI/DPIv2 interfaces without a
runtime dependency on etaHEN or Elf Arsenal.
The payload version is 0.4.0.
Features:
- WebUI and HTTP API on
0.0.0.0:12800. - Console-local legacy DPI service on
127.0.0.1:9040. - HTTP/HTTPS URLs,
file://URIs, absolute console paths, and browser uploads. - Installation status, progress, persistent jobs, and safe upload cleanup.
- Actual mounted-volume discovery and a full filesystem browser.
- Resumable file/folder uploads, confirmed copy/move/delete, and ZIP extraction.
- ShadowMountPlus image/folder staging and bounded PS4/PS5 package previews.
- Batch package uploads with automatic installation after the initial start action.
- CNT, FIH, and LIH package recognition, with native title/content-ID inference.
See file management, recovery, API additions, and benchmarking and the validation results.
Use only with homebrew packages and backups you own.
Requirements
Raring DPI is not a jailbreak, HEN, or signature-patch payload. Load these prerequisites first:
- A
ps5-payload-sdk-compatible ELF loader with kernel R/W. - Firmware-compatible kstuff or equivalent package/signature patches.
Raring DPI elevates only its own process. It does not bundle kstuff, ShellCore patch tables, etaHEN, or Elf Arsenal.
Build and test
The Makefile uses PS5_PAYLOAD_SDK when set, otherwise /opt/ps5-payload-sdk.
make
make test
make integration
make ci # everything the Forgejo CI runs (host suites + payload checks)
make check # ci plus the multi-SDK sweep and browser tests
make transfers-tsan # the HTTP suite against a ThreadSanitizer server
Real-browser suites use Playwright from build/browser-tools (see
docs/validation.md) and optionally a system Chrome:
TMPDIR=/tmp RD_CHROMIUM=/usr/bin/google-chrome python3 tests/browser.py
RD_BROWSER_SCRIPT=tests/batch-install.cjs python3 tests/browser.py # one script instead of browser.cjs
python3 tools/screenshots.py build/screenshots # every view at TV, desktop and phone size
Releases are reproducible with the pinned toolchain (LLVM 20, SDK v0.44,
sha256-checked) in the included Containerfile:
podman build -t raring-dpi-build .
podman run --rm -v "$PWD":/workspace:Z raring-dpi-build all elf-check
make require-ps5-sdk PS5_LLVM_MAJOR=20 checks a local toolchain against that
pin. make dist RELEASE_TAG=v0.4.0 puts the release assets,
raring-dpi.elf and SHA256SUMS, in build/dist/.
CI and releases
.forgejo/workflows/ci.yml runs on every push and pull request: the host
tests, integration suites (both download policies), ASan/UBSan, TSan and
shellcheck, then builds the payload for both download policies with the
pinned toolchain and uploads the ELFs as artifacts.
.forgejo/workflows/release.yml runs on v* tags (or manually for an
existing tag). The tag must match RD_VERSION in include/raring/common.h
(v0.4.0 or v0.4.0-rc1) and point at the checked-out commit. It builds,
verifies and attaches raring-dpi.elf and SHA256SUMS to a Forgejo release;
Forgejo's own source archives carry the license and notices.
It uses the job token; if the instance restricts it, add a RELEASE_TOKEN
repository secret with write:repository.
Every build embeds its source identity (git describe-style, with -dirty
for local changes). It is shown in /api/status as source and in the first
log line.
The deployable payload is build/raring-dpi.elf. An unstripped diagnostic
image is retained as build/raring-dpi.debug.elf.
To build the optional native notification/Downloads artwork variant:
make BUILD_DIR=build/icons INSTALL_ICONS=1
This caches readable embedded PNG artwork and passes a console-local HTTP URI to AppInst. Automatic artwork stays disabled in the default build until actual native rendering is verified on the console; explicit icon URLs remain supported.
To select an SDK explicitly:
make PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk
Downloads are broad by default for DPI compatibility. To permit only
canonical filenames ending in .pkg, build with:
make DOWNLOAD_POLICY=package-only
Working on the WebUI
The page is authored as a source tree under assets/ui/ and assembled into
one self-contained document at build time:
assets/ui/index.html the document, plus the ordered include manifest
assets/ui/css/*.css numeric prefixes because cascade order matters
assets/ui/js/*.js load order is stated in index.html, not globbed
assets/ui/js/browser/*.js the filesystem browser
tools/bundle-ui.sh splices the <!--#include path--> directives in
index.html into build/gen/ui.html, and tools/embed-asset.sh turns that
into build/gen/raring/ui_html.h. Edit the sources, never the generated
files. make ui runs both steps.
The bundler never substitutes, only splices, so & and backslashes in CSS
and JS carry no meaning. Every guard is fatal rather than escaping: a path
outside assets/ui/, a .., a missing or empty file, a nested include, a
carriage return, or a </style/</script in included content all fail the
build. make bundle-test covers them.
Each build prints a line map, which is how you locate a stack frame when the console browser gives you no devtools:
bundle-ui: assets/ui/js/browser/window.js -> 1832..1938
make ui alone is not enough to see a change in the host dev server. The
page is compiled into the binary, so the server has to be relinked:
make build/host/server # rebuilds the header *and* relinks
Serving the page sends Cache-Control: no-store, because its URL never
changes between builds and a cached copy would otherwise outlive the update
that replaced it. File downloads keep the default.
To drive the UI without a console, run the host server with its AppInst simulation enabled. It fakes an install ramp so progress, error and unmonitored states are all reachable:
make build/host/server
RD_HOST_SIMULATE=1 RD_HOST_SIM_SPEED=8 build/host/server /tmp/rd-ui
Then open http://127.0.0.1:18080/ and queue these to see each state:
| Source | Result |
|---|---|
https://example.test/any.pkg |
queued, transferring, promoting, playable |
https://example.test/fail.pkg |
ends in error with a platform code |
https://example.test/nomon.pkg |
unmonitored (no progress channel) |
an upload with content_name=reject |
HTTP 500 with the file retained |
RD_HOST_SIM_SPEED is percent of progress per two-second poll (default 7)
and RD_HOST_SIM_TOTAL sets the fake package size. With RD_HOST_SIMULATE
unset the stubs behave exactly as before, so make integration is
unaffected.
The page is three views behind a tab strip, addressed by the URL hash:
#/install, #/queue, #/files (the storage list), and
#/files/mnt/usb0/games for a directory. The hash is the only record of
where the browser is, so Back, Forward and reload all work; the path it
carries is the percent-encoded wire form, never the display form.
Host device tests read RD_MOUNT_FIXTURE, a text file with rows in the form
mountPath|filesystemType|readOnly|totalBytes|availableBytes. Only enumerated
mounts become device cards; creating an ordinary directory does not create one.
The integration suite manages its own isolated fixtures.
Directory rows are windowed: only the rows on screen exist in the DOM, at a
fixed --row-h. That is why rows never wrap, and why narrow screens drop
columns instead.
Deployment guide
- Run the jailbreak/exploit appropriate for the console firmware.
- Load compatible kstuff or equivalent patches.
- Start a standalone ELF loader. The SDK default is port
9021; some exploit-host loaders temporarily use9020. - Verify the loader is listening:
nc -zv PS5-IP 9021
- Send Raring DPI:
prospero-deploy -h PS5-IP -p 9021 build/raring-dpi.elf
The equivalent Make target is:
make deploy PS5_HOST=PS5-IP PS5_PORT=9021
- Confirm the service started:
curl http://PS5-IP:12800/api/status
Then open http://PS5-IP:12800/ in a browser.
If prospero-deploy reports Connection refused, the PS5 is reachable but no
ELF loader is listening on that port. Restart the loader and send the payload
again. Port 9040 is bound to loopback on the PS5 and cannot be reached
directly from another LAN device.
Usage
The web interface opens Upload file by default. Select one or more packages, review the inline metadata and any per-package options, then click Upload and install once. Files upload in order and each completed package is submitted automatically. A rejected package leaves its error in Queue and the batch continues. Connection or storage failures pause the batch for Resume. Cancel stops the remaining uploads; submitted native installations continue.
From URL submits a native package when you click Install. ShadowMountPlus content still requires a destination, existing files require a conflict choice, and ZIPs require extraction choices. File selection alone does not start work.
Queue a remote package:
curl -H 'Content-Type: application/json' \
--data '{"url":"https://example.test/homebrew.pkg"}' \
http://PS5-IP:12800/api/install
Queue a package already on the console:
curl -H 'Content-Type: application/json' \
--data '{"url":"/data/packages/homebrew.pkg"}' \
http://PS5-IP:12800/api/install
/data/... is normalized to /user/data/....
Upload a package:
curl -F file=@homebrew.pkg http://PS5-IP:12800/upload
Streaming installs (no copy on the console)
Tick Stream directly to the installer after choosing a single .pkg. The
console's installer then pulls the package from the browser tab on demand over
a WebSocket, so it needs no temporary copy and no free space beyond the
installed game. Keep the tab open until the job finishes.
If the connection drops (Wi-Fi blip, closed tab, PC restart), the install
pauses instead of failing. The installer keeps retrying and the console keeps
the job waiting for 30 minutes. A tab that still has the file reconnects by
itself. Otherwise open the WebUI again, choose Resume stream on the job and
pick the same .pkg; the console checks it against the package header it
already has and continues where it stopped. A different file is refused. If
nothing resumes within 30 minutes the job fails. The stream lives in console
RAM, so restarting the payload still ends it.
The protocol is in include/raring/stream.h:
POST /api/stream {"name","size"} -> {"token","ws","chunkBytes"}
GET /api/stream/ws?token=... WebSocket: {"op":"range","id","offset","size"}
answered by binary [8-byte LE id][bytes]
POST /api/stream/install {"token"} queues the install
POST /api/stream/resume {"jobId","size"} -> {"token","ws","chunkBytes"} of a paused job;
the reattached socket answers one range for
segment 0, then gets {"op":"resumed"} or
{"op":"error","message"}
Job status carries a stream object for streamed installs: state
(connected, stalled, detached or closed), detachedAt,
resumeDeadline, and throughput counters (receivedBytes, receiveRate,
waitMs the installer spent waiting on the browser, cacheHits, inflight,
peakInflight, inflightCapped, resumes). The console log prints the same
counters every 10 seconds while a stream is busy.
Upload-then-install remains the default and is still the right choice for
batches. On firmware below 11.00, ps5upload reported that HTTP-pulled installs
from a PC can hang in the PlayGo pre-flight check; streaming runs from the
console's own loopback, like local installs, but has not yet been confirmed on
10.01. A desktop can also skip the copy entirely by installing from a URL it
serves ({"url":"http://PC-IP:PORT/game.pkg"}) if the PS5 can reach it.
Updates and add-ons
Package type comes from the CNT header (content type, content flags, delta
container) cross-checked with the PARAM category, following ShadowMountPlus.
The job then follows the task type the installer reports: updates are polled
through GetPatchInstallStatus, add-ons through the add-on installed status.
- A PS4 update or add-on whose game is not installed is refused before
submission with
0x80A30004; Sony would accept it and install nothing. PS5 updates are passed through, since some carry the whole application. playablealone is not completion: PlayGo marks a title playable while it is still downloading. A job finishes when the download (and any local copy) is complete, or when a retired update task is confirmed by the title's/user/patch/<TID>/patch.pkgchanging.- A task that never appears fails after 30 seconds; an install with no progress and no reads for 15 minutes fails as stalled.
- Known installer codes come with a plain-language
hintin job status.
Local PS5 packages, updates and add-ons are handed to the installer through a
loopback-only package server on 127.0.0.1:12801 (token per file, ranges,
keep-alive, .crc sidecars). It keeps the WebUI's workers free during long
installs, and reloading the payload re-registers in-flight downloads. On
firmware below 11.00 a bare-path install runs under ShellCore's authid, which
those firmwares require.
Uploads are deleted only after AppInst reports playable. To retain a
successful upload:
curl -F file=@homebrew.pkg -F keep_file=true \
http://PS5-IP:12800/upload
A refused upload names the step that refused it in the JSON error field
(no file part in the upload, could not create the upload temp file,
invalid initial boundary, timed out waiting for upload data, …) and logs
the same reason with errno to the payload's stderr:
[raring-dpi] upload: open /user/data/raring-dpi/tmp/upload-….part failed: …
[raring-dpi] upload failed: could not create the upload temp file (errno=13 …)
Uploads are published under their final name with link() plus unlink() so
the package exists under both names before either is removed. Filesystems that
do not implement hard links fall back to rename(), which is logged.
Poll one queued job using the statusUrl returned by the install response:
curl "http://PS5-IP:12800/api/status?id=JOB-ID"
A response with "res":"0" means AppInst accepted the task; it does not mean
installation has finished.
Service status includes storageHealthy, storageCode, and
downloadPolicy. Job sources report sourceRedacted; remote credentials,
query strings, and fragments are never returned by the status API.
API summary
| Method | Route | Purpose |
|---|---|---|
GET |
/ |
WebUI |
GET |
/install?url=... |
Queue a URL or path |
POST |
/ |
Queue form field url |
POST |
/api/install |
Queue JSON/form metadata |
POST |
/upload |
Stream and queue a browser upload |
GET |
/api/status[?id=...] |
Service and job status |
GET |
/api/events |
Server-sent status stream |
GET |
/api/devices |
Mounted volumes, capacities, browseable roots |
GET |
/api/browse?path=...[&meta=0] |
Directory listing |
GET |
/api/pkginfo?path=...&name=... |
PARAM.SFO details for one package |
POST |
/api/mkdir?path=...&name=... |
Create a directory |
POST |
/api/rename?path=...&from=...&to=... |
Rename an entry |
POST |
/api/files/delete?path=... |
Delete a file or empty directory |
POST |
/api/files/delete?name=... |
Delete one payload-owned upload |
GET |
/api/files/stat?path=... |
{"exists"} plus type and size |
GET |
/api/files/download?path=... |
Download a file (same rules as the path route) |
GET |
/api/content/info?path=... |
Package, homebrew image, folder or ZIP details |
GET |
/api/content/icon?path=... |
Icon from a package or homebrew title |
GET |
/api/install-targets |
Writable ShadowMountPlus scan folders |
GET, POST, PUT |
/api/uploads[?id=...&action=...] |
Resumable uploads (see below) |
GET, POST |
/api/file-operations[?id=...] |
Copy, move, delete, extract, ShadowMountPlus placement |
POST |
/api/storage/check |
Will an upload and its install fit |
GET, PUT |
/api/settings |
User settings (see Settings) |
POST |
/api/retry?id=... |
Retry a failed local install using the same job ID |
POST |
/api/jobs?id=...&action=dismiss |
Remove finished, failed or "Sent" history and linked journals; retain the PKG |
POST |
/api/jobs?id=...&action=cancel |
Stop a streamed install (closes its stream) |
GET/HEAD |
/api/jobs/icon?id=... |
Cached native notification artwork when available |
POST |
/cleartmp |
Remove inactive payload-owned uploads |
POST |
/api/stream |
Start a streaming-install session (see Streaming installs) |
GET |
/api/stream/ws?token=... |
Stream WebSocket (browser answers range requests) |
POST |
/api/stream/install |
Queue the install from a connected stream |
POST |
/api/stream/resume |
Reconnect a browser to a paused streamed install |
GET |
/api/log |
Last 64 KiB of debug.log |
GET, HEAD |
allowed absolute path | Restricted file download |
/api/browse returns path, parent, root, entries, shown, total,
truncated and nextOffset. Large folders arrive in pages: pass offset,
limit (1–1000) and the first page's revision; a folder that changes
between pages answers 409 so the client starts over.
Uploads: POST /api/uploads with {destination|name, total, fingerprint}
creates a transfer; PUT /api/uploads?id=&offset=&fingerprint= sends the next
chunk (up to 32 MiB). A busy transfer or an out-of-date offset answers 409:
read the saved offset with GET ?id= and continue from there. Actions:
finalize, resolve (policy), install, cancel, discard, dismiss,
resume. Paths inside these JSON bodies are percent-encoded.
Errors are JSON: {"error": "<plain message>", "code": <negative errno or installer code>, "detail": "<system text>"}. Statuses follow the cause:
404 missing, 403 not allowed, 409 in use, exists or changed, 413 out of
space, 503 installer not ready, 400 a bad request, 500 anything else.
Unknown /api/ routes answer 404; a known route with the wrong method 405.
Filenames on FAT and exFAT volumes are bytes, not text. An entry whose name is
not valid UTF-8 is returned with the bad bytes replaced by U+FFFD, plus
"lossy":true and "raw" holding the percent-encoded original. Display
name; address the file with raw.
meta=0 skips PARAM.SFO inspection so a directory of packages lists
immediately; the WebUI uses it and then asks /api/pkginfo for the rows it is
actually showing.
Metadata fields are url, content_name, content_id,
playgo_scenario_id, ex_uri, icon_url, and keep_file.
Install requests also accept optional native_uri. Local URI overrides must
address the same canonical file as url; an explicit file:// URI is retained
for native submission while filesystem inspection and protection use the
canonical path. Local PS5 packages use a token URL on the loopback package
server (http://127.0.0.1:12801/<token>/<name>.pkg), without another upload or
staging copy. This applies to Files → Install, upload finalization, and retries.
PS4 packages keep their native raw-path default.
The legacy TCP interface also preserves explicit file URI intent. Retry accepts
an optional native_uri query parameter for a controlled retry of the same
retained file. This does not establish which local URI forms a given firmware's
native installer supports, and no automatic URI fallback is attempted.
Queue details expose submittedUri, submittedUriKnown, and failureStage.
Credentials and URL query/fragment secrets are redacted. Journal version 7 retains
native URI, explicit override intent, and platform failure details and still
reads versions 1–6. Jobs from versions 1–5 report their past submission URI as
unknown. Retrying an older local PS5 job
with an implicit bare path selects the loopback HTTP transport. Explicit file
URI overrides are preserved. A restored native rejection keeps
its error message instead of being relabeled as an interruption. In particular,
0x80B2150F is reported as an invalid URI scheme, with the package retained.
Settings
The Settings tab (#/settings) holds the few choices people change. Each
control saves as soon as it changes.
| Setting | Default | Effect |
|---|---|---|
Homebrew folder (homebrew_target) |
Ask every time | Where exFAT/ffpkg images and homebrew folders go; also the starting point for ZIP extraction. Must be a writable ShadowMountPlus scan folder. If it is not mounted, the list is shown. |
Keep PKG files (keep_file) |
Off | Pre-ticks "Keep the file after installing"; API installs that omit keep_file use it. |
Stream single PKGs (stream_single_pkg) |
Off | Pre-ticks streaming when one PKG is chosen. |
Console notifications (notifications) |
all |
all, errors (failures only) or off. Startup errors always notify. |
They live in /user/data/raring-dpi/settings.json and survive payload
restarts. GET /api/settings returns settings, defaults and choices;
PUT /api/settings takes any subset and saves all of it or none, answering
400 with the offending field. A field the payload cannot read falls back to
its default; an unreadable or newer file is kept as
settings.json.unsupported-<time> and defaults apply. Delete the file to reset.
Storage and security
- Job/session records and owned installer uploads live under
/user/data/raring-dpi; Files and homebrew uploads stage on their destination drive. - Legacy multipart partials are removed. Resumable uploads retain durable checkpoints; failed or unmonitorable installations retain their files.
- Active and unmonitored install sources are protected.
/cleartmpreturns409while a legacy upload is arriving or owned transfers still need review. - Long uploads and copies do not block other requests. A short lock covers only
the check-then-claim steps; each transfer claims its own paths, and anything
touching a claimed path gets
409 That item is in use. - Pre-existing local packages are never automatically deleted.
- Download paths use root-anchored, no-follow traversal and reject parent traversal, directories, and symlinks.
- Reloading the payload replaces older processes named
raring-dpi.elfbefore binding ports or opening job storage. It requests shutdown with SIGTERM, waits 15 seconds, then uses SIGKILL if necessary and waits up to five more seconds. Existing upload/job journals remain available for recovery. The storage lock remains a final guard against concurrent writers. - The runtime process name is
raring-dpi.elf, matching the built ELF filename. Builds previously named onlypayload.elfneed to be stopped once when upgrading; that generic name is not sufficient to identify raring-dpi. - Port
12800is unauthenticated and permits CORS for DPI compatibility. Use it only on a trusted LAN. The package server binds loopback only. - Everything the payload logs goes to
/user/data/raring-dpi/debug.log(previous run indebug.log.1, rotated at 4 MiB) and the last 64 KiB is served atGET /api/log. Startup, install completion and failures also raise a console notification (see Settings).
Console ABI notes
The libSceAppInstUtil structure layouts in circulation are community
reconstructions, and some of them have never been executed by anything.
SceAppInstallStatusInstalled is 712 (0x2c8) bytes, not 600. Every
public PS5 installer carries the same 600-byte definition, but nothing had ever
run it: etaHEN keeps its polling loop inside #if 0, and neither elf-arsenal
DPI payload calls sceAppInstUtilGetInstallStatus at all. On firmware 10.01
the console wrote 112 bytes past that definition, overwrote the polling
thread's stack canary, and __stack_chk_fail aborted the payload.
The 112 bytes are two missing fields, not a longer tail: a 100-byte block
after promote_progress and a 12-byte patch_version after error_info
(ShadowMountPlus, verified against the 9.40 library). So the short definition
also read error_info, local_copy_percent and is_copy_only from the wrong
offsets; error codes reported by status polling before this fix were not
trustworthy. rd_install_status_t now has the full layout with static
asserts on the offsets, and rd_appinst_get_status still receives it in an
8 KB stamped heap buffer.
The InstallByPackage metadata object is likewise longer than the
six-pointer declaration: the installer reads the bytes after icon_url, and a
non-zero value at +0x30 selects the patch path (refused 0x80B2116F, measured
by ps5upload on 13.60). It is now a zeroed 0x70-byte object, as in
ShadowMountPlus and ps5upload.
rd_pkg_info_t (56 bytes) and rd_playgo_info_t (9984 bytes) come from the
same unverified sources and get the same buffer treatment, on the grounds that
rd_pkg_info_t "worked" for exactly as long as the status struct did. Both
have since measured clean — the console stays inside both definitions — but
the measurement only covers the calls this payload makes.
Measuring the overrun needs a stamped buffer, not a zeroed one. The first
attempt zeroed the whole scratch and looked for the last non-zero byte. On
firmware 10.01 it reported that sceAppInstUtilGetInstallStatus wrote 57
bytes — for the same call that had been wiping a stack canary through a
600-byte object. Both readings are true together only if what the console
writes past the fields it populates is zeroes, most likely because it clears
its whole structure first; zeroes written over zeroes leave no trace.
Each scratch buffer is therefore zeroed only across the prefix the console is
entitled to write, and stamped with 0xA5 beyond it. The write extent is
where the stamp stops being intact, so a clear of the true size shows up as a
run of erased sentinel. That is how the 712 above was measured. Each call
reports once per new high-water mark:
[raring-dpi] appinst: pkg info stayed inside its 56-byte struct
[raring-dpi] appinst: playgo stayed inside its 9984-byte struct
[raring-dpi] appinst: status stayed inside its 712-byte struct
If a figure ever approaches its scratch size, raise the scratch — do not assume the struct definition is now known.
See NOTICE.md for research attribution.