No description
  • C 60.8%
  • JavaScript 20.8%
  • Python 8.5%
  • Shell 3.6%
  • CSS 3.2%
  • Other 3.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
RaringM 53ccc3880d
All checks were successful
release / release (push) Successful in 41s
ci / host tests (unit, integration, sanitizers) (push) Successful in 3m9s
ci / ps5 payload (broad) (push) Successful in 54s
ci / ps5 payload (package-only) (push) Successful in 49s
server: delete-by-name checks transfer claims only
rd_transfers_active() treats the whole data folder as reserved, so the
previous check refused every kept-upload delete.
2026-11-01 13:02:45 -06:00
.forgejo/workflows ci: make host suites pass on Ubuntu runners 2026-10-31 12:09:52 -05:00
assets/ui ui: TV scale, touch targets, one stylesheet per component 2026-11-01 13:01:19 -06:00
docs docs: review findings and console checks for this pass 2026-11-01 13:01:55 -06:00
include/raring server: short claims lock, settings store, consistent API errors 2026-11-01 12:42:26 -06:00
src server: delete-by-name checks transfer claims only 2026-11-01 13:02:45 -06:00
tests ui: fewer steps, safer drops, readable errors 2026-11-01 13:01:19 -06:00
tools ui: TV scale, touch targets, one stylesheet per component 2026-11-01 13:01:19 -06:00
.gitignore add install for smp pkgs, add resumability, an qol 2026-09-12 14:34:58 -05:00
Containerfile v0.4.0: patch-aware installs, package server, streaming, Forgejo CI 2026-10-31 11:57:59 -05:00
LICENSE working 2026-07-25 20:42:51 -05:00
Makefile server: short claims lock, settings store, consistent API errors 2026-11-01 12:42:26 -06:00
NOTICE.md update 2026-09-19 17:53:35 -05:00
README.md ui: TV scale, touch targets, one stylesheet per component 2026-11-01 13:01:19 -06:00

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:

  1. A ps5-payload-sdk-compatible ELF loader with kernel R/W.
  2. 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

  1. Run the jailbreak/exploit appropriate for the console firmware.
  2. Load compatible kstuff or equivalent patches.
  3. Start a standalone ELF loader. The SDK default is port 9021; some exploit-host loaders temporarily use 9020.
  4. Verify the loader is listening:
nc -zv PS5-IP 9021
  1. 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
  1. 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.
  • playable alone 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.pkg changing.
  • 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 hint in 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. /cleartmp returns 409 while 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.elf before 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 only payload.elf need to be stopped once when upgrading; that generic name is not sufficient to identify raring-dpi.
  • Port 12800 is 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 in debug.log.1, rotated at 4 MiB) and the last 64 KiB is served at GET /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.