From b4c6528b5cad9959d54effdbd957e3ab8610aef2 Mon Sep 17 00:00:00 2001 From: phantomptr Date: Sun, 5 Jul 2026 16:54:47 -0700 Subject: [PATCH 1/2] fix: payload ptrace/mount/symlink hardening, install-all trim, SDK version docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Findings from a full-repo scan. Client + payload + docs; all gates green (payload -Werror build, client typecheck/lint/754 tests, i18n 18-lang). payload/src/ptrace_remote.c — three failure-path hardening fixes on the ShellUI RPC primitives (candidate root cause for the FW-12 install crash where the helper goes unreachable after a rejected install): - pt_mmap: a failed remote mmap returns -errno in rax; callers only checked ==-1||==0, so e.g. -ENOMEM (0xFFFF...F4) was used as a valid pointer and dereferenced inside SceShellUI -> crash. Collapse the whole [-4095,-1] range to -1 in one place so every caller is covered. - pt_syscall: restore bak_reg on the pt_step/pt_getregs failure paths (mirroring pt_call), so a later PT_DETACH can't resume ShellUI at the syscall site with junk argument registers. - pt_attach: PT_DETACH on waitpid failure so a mid-attach EINTR can't leave ShellUI frozen + our tracer leaked (next attach would EBUSY). payload/src/runtime.c — FS_MOUNT: validate the derived mount_name too, and reject a lone "." — otherwise mount_name "." (or an image named "..exfat") builds /mnt/ps5upload/. which the kernel normalises to the namespace root, shadowing every existing mount. Also bump reconcile's src[256] -> src[512] to match the writer/find-existing buffers (long source paths were truncated -> false "source missing" at boot). payload/src/register.c — copy_dir_recursive: lstat() instead of stat() when staging sce_sys, so a symlink is skipped (as the comment already claims) rather than followed — a hostile dump's sce_sys/leak -> /system_data/... no longer copies a system file into user space, and a dangling link no longer aborts the whole copy. client/src/state/pkgLibrary.ts — trim an unfinished installAll WIP that declared installAll/installingAll on the store interface without implementing them (TypeScript TS2739 -> CI red) and an orphaned pkgEntryInstallOrder helper. Keep the standalone-good failure-notification bell so a failed install rings the bell instead of only updating the row. README.md, FAQ.md, scripts/install-{macos,ubuntu,windows} — bump the pinned PS5 Payload SDK from v0.38 -> v0.40 to match CI/release (publish.yml, engine-ci.yml). v0.38 predates getloadavg(); make install && make payload against it failed to link (undefined symbol: getloadavg). The historical CHANGELOG 2.12.1 entry documenting the original v0.38->v0.39 bump is left untouched. Co-Authored-By: Claude Opus 4.8 --- FAQ.md | 2 +- README.md | 4 ++-- client/src/state/pkgLibrary.ts | 8 +++++++ payload/src/ptrace_remote.c | 39 ++++++++++++++++++++++++++++++---- payload/src/register.c | 10 ++++++++- payload/src/runtime.c | 17 ++++++++++++--- scripts/install-macos.sh | 4 ++-- scripts/install-ubuntu.sh | 4 ++-- scripts/install-windows.ps1 | 4 ++-- 9 files changed, 75 insertions(+), 17 deletions(-) diff --git a/FAQ.md b/FAQ.md index 0620faee..7e6e3fa1 100644 --- a/FAQ.md +++ b/FAQ.md @@ -97,7 +97,7 @@ Game pkgs (UP / EP / JP / HP / CUSA / PPSA / PCSA / etc.) work fine. setup, permissions, and uploading from your phone. **Q: Which PS5 firmware works?** -ps5upload is built against PS5 Payload SDK v0.38, which resolves +ps5upload is built against PS5 Payload SDK v0.40, which resolves kernel offsets at startup for every firmware it knows about. The same binary runs on the full range **1.00 – 12.70**. diff --git a/README.md b/README.md index d31516f0..f85a2fd5 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ make run-client # launch the Tauri dev app (`libwebkit2gtk-4.1-dev`, `libgtk-3-dev`, `librsvg2-dev`, `libayatana-appindicator3-dev`, `libxdo-dev`, `libssl-dev`, `build-essential`), Node.js 22 LTS via NodeSource (only if missing), - Rust via rustup, and PS5 Payload SDK v0.38 → `~/ps5-payload-sdk`. + Rust via rustup, and PS5 Payload SDK v0.40 → `~/ps5-payload-sdk`. - **`make install-macos`** — macOS: Xcode CLT, Homebrew, `node`, `llvm@18` (the only Homebrew llvm shipped with `ld.lld` — required by `prospero-clang`), Rust via rustup, and PS5 Payload SDK. @@ -301,7 +301,7 @@ cross-platform, and live-PS5 validation workflow. **PS5 payload** — every firmware the PS5 Payload SDK supports, currently **1.00 through 12.70** on every console model (original CFI-1xxx, Slim CFI-2xxx, Pro CFI-7xxx, Digital). Built against SDK -v0.38, which ships per-firmware kernel offsets and resolves them at +v0.40, which ships per-firmware kernel offsets and resolves them at payload startup via `kernel_get_fw_version()` — the same binary runs on every supported firmware without per-release rebuilds. diff --git a/client/src/state/pkgLibrary.ts b/client/src/state/pkgLibrary.ts index a7f99d99..da7f66e9 100644 --- a/client/src/state/pkgLibrary.ts +++ b/client/src/state/pkgLibrary.ts @@ -1520,6 +1520,14 @@ const makePkgLibraryStore = () => status: "idle", lastResult: { ok: false, message: mainErr || "Install was rejected." }, }); + // Surface failures in the bell too (success already notifies above). + // Without this a failed item — an update or DLC especially — was silent + // if the user navigated away from the Library tab mid-install. + pushNotification( + stalled ? "warning" : "error", + `${label} install ${stalled ? "didn’t finish" : "failed"}`, + { body: mainErr || "The PS5 didn’t confirm the install. Try again." }, + ); } } catch (e) { patch({ status: "idle", lastResult: { ok: false, message: pkgError(e) } }); diff --git a/payload/src/ptrace_remote.c b/payload/src/ptrace_remote.c index 71d33a3d..6b68f269 100644 --- a/payload/src/ptrace_remote.c +++ b/payload/src/ptrace_remote.c @@ -126,6 +126,14 @@ int pt_attach(pid_t pid) { /* Block until the child reports the SIGSTOP. Without this the * subsequent PT_GETREGS races the kernel and returns ESRCH. */ if (waitpid(pid, 0, 0) == -1) { + /* PT_ATTACH already succeeded: we are still the tracer and the + * target is stopped. Returning without detaching leaks a frozen + * SceShellUI and leaves the next pt_attach EBUSY under our own + * tracer. waitpid realistically only fails here on EINTR (one + * of our signal handlers firing mid-wait), so a clean detach + * restores the target to running and frees the slot. Best-effort + * — if detach also fails there is nothing left to try. */ + (void)pt_detach(pid, 0); return -1; } return 0; @@ -312,14 +320,27 @@ long pt_syscall(pid_t pid, int sysno, ...) { jmp_reg.r_r9 = va_arg(ap, uint64_t); va_end(ap); - if (pt_setregs(pid, &jmp_reg) != 0) return -1; + if (pt_setregs(pid, &jmp_reg) != 0) { + /* Mirror pt_call: best-effort restore of the original regs in + * case the failed PT_SETREGS partially applied, and so a later + * PT_DETACH doesn't resume the target at the syscall site with + * junk argument registers. */ + (void)pt_setregs(pid, &bak_reg); + return -1; + } /* Single-step until we land back on the syscall site after * sysret. Bound to 1k steps because syscalls are fast. */ int step_budget = 1000; while (jmp_reg.r_rsp <= bak_reg.r_rsp && step_budget > 0) { - if (pt_step(pid) != 0) return -1; - if (pt_getregs(pid, &jmp_reg) != 0) return -1; + if (pt_step(pid) != 0) { + (void)pt_setregs(pid, &bak_reg); + return -1; + } + if (pt_getregs(pid, &jmp_reg) != 0) { + (void)pt_setregs(pid, &bak_reg); + return -1; + } step_budget--; } if (step_budget == 0) { @@ -333,7 +354,17 @@ long pt_syscall(pid_t pid, int sysno, ...) { intptr_t pt_mmap(pid_t pid, intptr_t addr, size_t len, int prot, int flags, int fd, off_t off) { - return pt_syscall(pid, SYS_mmap, addr, len, prot, flags, fd, off); + long r = pt_syscall(pid, SYS_mmap, addr, len, prot, flags, fd, off); + /* On syscall failure the kernel writes -errno into rax (e.g. + * -ENOMEM under memory pressure, -EINVAL, -EACCES). Every caller + * checks `scratch == -1 || scratch == 0`, so a -12 (0xFFFFFFFFFFFFFFF4) + * sails past as a "valid pointer" and the next pt_copyin/pt_call + * dereferences it inside SceShellUI → ShellUI crash, and Sony's + * watchdog can cascade into our payload. Collapse the whole errno + * range here so every caller is fixed in one place. Valid mmap + * results are page-aligned and >= 0x1000, never in [-4095,-1]. */ + if ((unsigned long)r >= (unsigned long)-4095) return -1; + return r; } int pt_munmap(pid_t pid, intptr_t addr, size_t len) { diff --git a/payload/src/register.c b/payload/src/register.c index fccfdd66..c9abb411 100644 --- a/payload/src/register.c +++ b/payload/src/register.c @@ -515,7 +515,15 @@ static int copy_dir_recursive(const char *src, const char *dst) { n = snprintf(dp, sizeof(dp), "%s/%s", dst, e->d_name); if (n < 0 || (size_t)n >= sizeof(dp)) { rc = -1; break; } struct stat st; - if (stat(sp, &st) != 0) { rc = -1; break; } + /* lstat, not stat: stat() follows a symlink and resolves it to + * the link *target*'s mode, so a symlink to a regular file + * (e.g. a hostile dump's sce_sys/leak -> /system_data/priv/mms/ + * app.db) would match S_ISREG and get its target copied into + * user-accessible /user/app//sce_sys/, and a dangling + * symlink would abort the whole sce_sys copy. lstat() lets + * symlinks fall through to "skipped" as the comment below + * already documents as the intent. */ + if (lstat(sp, &st) != 0) { rc = -1; break; } if (S_ISDIR(st.st_mode)) { if (copy_dir_recursive(sp, dp) != 0) { rc = -1; break; } } else if (S_ISREG(st.st_mode)) { diff --git a/payload/src/runtime.c b/payload/src/runtime.c index 53712327..d9714acf 100644 --- a/payload/src/runtime.c +++ b/payload/src/runtime.c @@ -1388,7 +1388,7 @@ void runtime_reconcile_mounts(void) { * the mount was created, keep the mount — users may have * intentionally moved the file and we don't own cleanup of * that. Log only; don't clean up. */ - char src[256]; + char src[512]; int have_src = mount_tracker_read(mnt_on, src, sizeof(src)); if (have_src) { struct stat src_st; @@ -8068,8 +8068,19 @@ static int handle_fs_mount(runtime_state_t *state, int client_fd, * from the image filename. */ if (mount_name[0] == '\0') { fs_mount_derive_name(image_path, mount_name, sizeof(mount_name)); - } else if (strchr(mount_name, '/') != NULL || - strstr(mount_name, "..") != NULL) { + } + /* Validate whether the name was caller-supplied or just derived + * from the image filename: reject path separators, "..", and a + * lone ".". The "." case matters because snprintf below then + * builds /mnt/ps5upload/. which the kernel normalises to + * FS_MOUNT_BASE itself — shadowing every existing mount in the + * namespace. That is the same footgun the strcmp(mount_point, + * FS_MOUNT_BASE) guard blocks for caller-supplied mount_points + * above, but this derived path bypasses it. fs_mount_derive_name + * can also yield "." for image names like "..exfat" or ".". */ + if (strchr(mount_name, '/') != NULL || + strstr(mount_name, "..") != NULL || + strcmp(mount_name, ".") == 0) { return send_frame(client_fd, FTX2_FRAME_ERROR, 0, trace_id, "fs_mount_bad_name", 17); } diff --git a/scripts/install-macos.sh b/scripts/install-macos.sh index e9ad8755..3212f954 100755 --- a/scripts/install-macos.sh +++ b/scripts/install-macos.sh @@ -9,7 +9,7 @@ # (llvm@18 is the only Homebrew llvm shipped with `ld.lld`, which prospero-clang needs; # the root Makefile pins LLVM_CONFIG to llvm@18 on macOS — keep them aligned) # - Rust toolchain (rustup, stable, default profile) -# - PS5 Payload SDK v0.38 → $PS5_PAYLOAD_SDK (default $HOME/ps5-payload-sdk) +# - PS5 Payload SDK v0.40 → $PS5_PAYLOAD_SDK (default $HOME/ps5-payload-sdk) # # After it finishes the script prints the env exports you need to add to ~/.zshrc # (or ~/.bash_profile) so `make build` and `make run-client` work in any new shell. @@ -23,7 +23,7 @@ set -euo pipefail # it to /opt/ps5-payload-sdk (root-only). Override the install location with # PS5_SDK_INSTALL_DIR if you want somewhere else. SDK_DIR="${PS5_SDK_INSTALL_DIR:-$HOME/ps5-payload-sdk}" -SDK_TAG="v0.38" +SDK_TAG="v0.40" SDK_URL="https://github.com/ps5-payload-dev/sdk/releases/download/${SDK_TAG}/ps5-payload-sdk.zip" BREW_DEPS=( diff --git a/scripts/install-ubuntu.sh b/scripts/install-ubuntu.sh index 3328d83c..0681c642 100755 --- a/scripts/install-ubuntu.sh +++ b/scripts/install-ubuntu.sh @@ -8,7 +8,7 @@ # libxdo, libssl, build-essential, file, curl, wget, unzip, pkg-config, python3) # - Rust toolchain (rustup, stable, default profile) # - Node.js 22 LTS via NodeSource (only if `node` is missing — keeps existing installs) -# - PS5 Payload SDK v0.38 → $PS5_PAYLOAD_SDK (default $HOME/ps5-payload-sdk) +# - PS5 Payload SDK v0.40 → $PS5_PAYLOAD_SDK (default $HOME/ps5-payload-sdk) # # After it finishes, the script prints the env exports you need to add to ~/.bashrc # (or ~/.zshrc) so `make build` and `make run-client` work in any new shell. @@ -22,7 +22,7 @@ set -euo pipefail # it to /opt/ps5-payload-sdk (root-only). Override the install location with # PS5_SDK_INSTALL_DIR if you want somewhere else. SDK_DIR="${PS5_SDK_INSTALL_DIR:-$HOME/ps5-payload-sdk}" -SDK_TAG="v0.38" +SDK_TAG="v0.40" SDK_URL="https://github.com/ps5-payload-dev/sdk/releases/download/${SDK_TAG}/ps5-payload-sdk.zip" NODE_MAJOR="22" diff --git a/scripts/install-windows.ps1 b/scripts/install-windows.ps1 index d5d49975..0a134974 100644 --- a/scripts/install-windows.ps1 +++ b/scripts/install-windows.ps1 @@ -8,7 +8,7 @@ # - Visual Studio 2022 Build Tools w/ C++ workload (MSVC + Windows SDK) # - 7zip (used to extract the PS5 SDK zip in CI-clean way) # - Microsoft Edge WebView2 Runtime (preinstalled on Win11; verified) -# - PS5 Payload SDK v0.38 → $env:USERPROFILE\ps5-payload-sdk +# - PS5 Payload SDK v0.40 → $env:USERPROFILE\ps5-payload-sdk # # Run from an elevated PowerShell: # PS> Set-ExecutionPolicy -Scope Process Bypass -Force @@ -27,7 +27,7 @@ param( # *build-time* SDK path and may point at somewhere the current user can't # write to. Pass -SdkDir or set $env:PS5_SDK_INSTALL_DIR to change. [string]$SdkDir = $(if ($env:PS5_SDK_INSTALL_DIR) { $env:PS5_SDK_INSTALL_DIR } else { Join-Path $env:USERPROFILE 'ps5-payload-sdk' }), - [string]$SdkTag = 'v0.38' + [string]$SdkTag = 'v0.40' ) $ErrorActionPreference = 'Stop' From f93770378be208718866dd98c8cc0794b49d2228 Mon Sep 17 00:00:00 2001 From: phantomptr Date: Sun, 5 Jul 2026 16:59:01 -0700 Subject: [PATCH 2/2] style(webui): rustfmt webui.rs to satisfy the engine fmt gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #171 was admin-merged without CI, so its webui.rs landed with formatting that cargo fmt --check rejects — blocking the engine job for every subsequent PR. cargo fmt --all touches the whole workspace regardless of feature gates, so this surfaced on the first post-#171 CI run. No behavior change. Co-Authored-By: Claude Opus 4.8 --- engine/crates/ps5upload-engine/src/webui.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/engine/crates/ps5upload-engine/src/webui.rs b/engine/crates/ps5upload-engine/src/webui.rs index d2afbab2..d2fa97ee 100644 --- a/engine/crates/ps5upload-engine/src/webui.rs +++ b/engine/crates/ps5upload-engine/src/webui.rs @@ -37,13 +37,7 @@ fn asset_response(path: &str) -> Option { .first_raw() .unwrap_or("application/octet-stream"); let body: Vec = file.data.into_owned(); - Some( - ( - [(header::CONTENT_TYPE, mime)], - body, - ) - .into_response(), - ) + Some(([(header::CONTENT_TYPE, mime)], body).into_response()) } /// Return a response for `path` inside the embedded bundle. If the path is @@ -84,4 +78,3 @@ pub async fn spa_fallback(req: Request) -> Response { // directly to e.g. `/library`. spa_response("index.html") } -