From e20a5f53ac7087108f17bae9f64b7e400b311812 Mon Sep 17 00:00:00 2001 From: phantomptr Date: Tue, 16 Jun 2026 23:52:16 -0700 Subject: [PATCH] =?UTF-8?q?fix(payload):=20never=20rename()=20across=20dev?= =?UTF-8?q?ices=20=E2=80=94=20fixes=20USB=E2=86=92internal=20move=20kernel?= =?UTF-8?q?=20panic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moving (cut & paste) a file from a USB drive to the internal SSD reliably KERNEL-PANICKED the console — a hard crash + reboot, reported and reproduced every time. Copy worked fine. Root cause: handle_fs_move (and the shell 'mv' builtin) called rename() directly. On this kernel a cross-DEVICE rename (USB exFAT → internal nullfs/UFS) does not return EXDEV — it PANICS. The client already falls back to copy-then-delete on 'fs_move_cross_mount', but the payload panicked before it could ever return that error. Fix: before rename(), stat the source and the destination's parent dir and compare st_dev; if they differ, refuse the rename and return fs_move_cross_mount (the client then completes the move as copy+delete, which reads/writes bytes and is cross-volume safe). Applied to both the FS_MOVE handler and the shell 'mv' command. stat() on USB is safe (FS_COPY already does it). HARDWARE-VERIFIED on a real PS5 (Phat, FW 5.10) with a USB drive attached: the USB→/data move that used to panic now returns fs_move_cross_mount and the console stays up; an intra-volume move still renames fine; readiness probe (3.3.20) returns ready. Throwaway test files only; user data untouched. Release 3.3.21. --- CHANGELOG.md | 12 ++++ VERSION | 2 +- client/package-lock.json | 4 +- client/package.json | 2 +- client/src-tauri/Cargo.lock | 10 +-- client/src-tauri/Cargo.toml | 2 +- client/src-tauri/tauri.conf.json | 2 +- client/src/screens/FileSystem/index.tsx | 12 ++-- engine/Cargo.lock | 14 ++--- engine/Cargo.toml | 2 +- engine/crates/ps5upload-engine/src/lib.rs | 13 ++-- payload/include/config.h | 2 +- payload/src/runtime.c | 74 ++++++++++++++++++++--- 13 files changed, 110 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8a8de5a..1d742fbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ What's new in ps5upload, written for humans. --- +## 3.3.21 + +- **Fixed: moving files from USB to the internal SSD crashed the console.** Cut + & paste from a USB drive to internal storage was reliably kernel-panicking the + PS5 (a hard crash + reboot). The tool was asking the console to *rename* the + file across drives, which this kernel can't do — it panics instead of + reporting the error. ps5upload now detects a cross-drive move up front and + completes it the safe way (copy, then remove the original) — the same thing it + already did for copies. **Hardware-verified: the move that used to crash the + console now completes without a hitch.** (Same fix applied to the shell tab's + `mv` command.) + ## 3.3.20 - **Installs wait for the PS5 to be ready — fewer "couldn't be applied" errors.** diff --git a/VERSION b/VERSION index 3df908d4..b2847d51 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.20 +3.3.21 diff --git a/client/package-lock.json b/client/package-lock.json index 1506b2a6..5d402a01 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "ps5upload-client", - "version": "3.3.20", + "version": "3.3.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ps5upload-client", - "version": "3.3.20", + "version": "3.3.21", "dependencies": { "@tauri-apps/api": "^2.11.0", "@tauri-apps/plugin-dialog": "^2.7.1", diff --git a/client/package.json b/client/package.json index 76114f66..7c0b28ed 100644 --- a/client/package.json +++ b/client/package.json @@ -1,7 +1,7 @@ { "name": "ps5upload-client", "private": true, - "version": "3.3.20", + "version": "3.3.21", "description": "The all-in-one PS5 companion app.", "homepage": "https://github.com/phantomptr/ps5upload", "author": "PhantomPtr ", diff --git a/client/src-tauri/Cargo.lock b/client/src-tauri/Cargo.lock index 19831e08..e3b17f85 100644 --- a/client/src-tauri/Cargo.lock +++ b/client/src-tauri/Cargo.lock @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "ftx2-proto" -version = "3.3.20" +version = "3.3.21" dependencies = [ "serde", "thiserror 2.0.18", @@ -3319,7 +3319,7 @@ dependencies = [ [[package]] name = "ps5upload-core" -version = "3.3.20" +version = "3.3.21" dependencies = [ "anyhow", "base64 0.22.1", @@ -3336,7 +3336,7 @@ dependencies = [ [[package]] name = "ps5upload-desktop" -version = "3.3.20" +version = "3.3.21" dependencies = [ "anyhow", "base64 0.22.1", @@ -3369,7 +3369,7 @@ dependencies = [ [[package]] name = "ps5upload-engine" -version = "3.3.20" +version = "3.3.21" dependencies = [ "anyhow", "axum", @@ -3387,7 +3387,7 @@ dependencies = [ [[package]] name = "ps5upload-pkg" -version = "3.3.20" +version = "3.3.21" dependencies = [ "serde", "thiserror 2.0.18", diff --git a/client/src-tauri/Cargo.toml b/client/src-tauri/Cargo.toml index 2ab57353..d7215756 100644 --- a/client/src-tauri/Cargo.toml +++ b/client/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ps5upload-desktop" -version = "3.3.20" +version = "3.3.21" description = "The all-in-one PS5 companion app." edition = "2021" rust-version = "1.77" diff --git a/client/src-tauri/tauri.conf.json b/client/src-tauri/tauri.conf.json index 242164b8..ced616c2 100644 --- a/client/src-tauri/tauri.conf.json +++ b/client/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "PS5Upload", - "version": "3.3.20", + "version": "3.3.21", "identifier": "com.phantomptr.ps5upload", "build": { "beforeDevCommand": "npm run dev:vite", diff --git a/client/src/screens/FileSystem/index.tsx b/client/src/screens/FileSystem/index.tsx index 2e1fb4b5..e4983c42 100644 --- a/client/src/screens/FileSystem/index.tsx +++ b/client/src/screens/FileSystem/index.tsx @@ -93,11 +93,13 @@ import { humanizePs5Error } from "../../lib/humanizeError"; * from the toolbar whenever the clipboard is non-empty, pasting into * the currently-browsed directory. * - * Cross-volume note: fsMove uses rename() which fails with EXDEV across - * mount points. fsCopy works across mounts because the payload reads + - * writes bytes explicitly. The UI falls back to "Copy, then delete" - * when a Cut+Paste fails with EXDEV — matches how file managers on - * Linux/macOS handle cross-filesystem moves. + * Cross-volume note: fsMove is rename()-based, which only works within one + * volume. Across mounts (e.g. USB → internal) the payload does NOT attempt the + * rename — a cross-device rename panics the PS5 kernel — and returns + * `fs_move_cross_mount`. fsCopy works across mounts because the payload reads + + * writes bytes explicitly, so the UI falls back to "Copy, then delete" on that + * error — matching how file managers on Linux/macOS handle cross-filesystem + * moves. */ interface DirEntry { diff --git a/engine/Cargo.lock b/engine/Cargo.lock index 2befc5d3..a123c2c0 100644 --- a/engine/Cargo.lock +++ b/engine/Cargo.lock @@ -494,7 +494,7 @@ dependencies = [ [[package]] name = "ftx2-proto" -version = "3.3.20" +version = "3.3.21" dependencies = [ "serde", "thiserror", @@ -954,7 +954,7 @@ dependencies = [ [[package]] name = "ps5upload-bench" -version = "3.3.20" +version = "3.3.21" dependencies = [ "criterion", "ftx2-proto", @@ -964,7 +964,7 @@ dependencies = [ [[package]] name = "ps5upload-core" -version = "3.3.20" +version = "3.3.21" dependencies = [ "anyhow", "base64", @@ -981,7 +981,7 @@ dependencies = [ [[package]] name = "ps5upload-engine" -version = "3.3.20" +version = "3.3.21" dependencies = [ "anyhow", "axum", @@ -999,7 +999,7 @@ dependencies = [ [[package]] name = "ps5upload-lab" -version = "3.3.20" +version = "3.3.21" dependencies = [ "anyhow", "ftx2-proto", @@ -1008,7 +1008,7 @@ dependencies = [ [[package]] name = "ps5upload-pkg" -version = "3.3.20" +version = "3.3.21" dependencies = [ "serde", "serde_json", @@ -1017,7 +1017,7 @@ dependencies = [ [[package]] name = "ps5upload-tests" -version = "3.3.20" +version = "3.3.21" dependencies = [ "anyhow", "ftx2-proto", diff --git a/engine/Cargo.toml b/engine/Cargo.toml index c0075de0..eb2a00c7 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -13,7 +13,7 @@ resolver = "2" [workspace.package] edition = "2021" license = "GPL-3.0-or-later" -version = "3.3.20" +version = "3.3.21" [workspace.dependencies] anyhow = "1.0" diff --git a/engine/crates/ps5upload-engine/src/lib.rs b/engine/crates/ps5upload-engine/src/lib.rs index c75eb010..23f5a94c 100644 --- a/engine/crates/ps5upload-engine/src/lib.rs +++ b/engine/crates/ps5upload-engine/src/lib.rs @@ -1301,12 +1301,13 @@ async fn ps5_fs_move( crate::log_info!("fs_move: addr={addr} from={from} to={to}"); let from_for_log = from.clone(); let to_for_log = to.clone(); - // 1-hour deadline: most fs_move calls return in milliseconds - // (rename(2) is metadata-only), but cross-volume moves that the - // payload retries via copy-then-delete inherit the same long - // bound as fs_copy. The default 30 s socket timeout would fire - // mid-copy of any multi-GiB file and surface as the cryptic "read - // frame header" 502. + // 1-hour deadline: an intra-volume fs_move returns in milliseconds + // (rename(2) is metadata-only). A CROSS-volume move can't rename (the + // payload refuses it — a cross-device rename panics this kernel) and + // returns `fs_move_cross_mount`; the CLIENT then completes it as + // copy-then-delete, which can run for minutes on a multi-GiB file. Keep the + // generous bound so the default 30 s socket timeout can't fire mid-op and + // surface as the cryptic "read frame header" 502. let io_timeout = std::time::Duration::from_secs(60 * 60); match tokio::task::spawn_blocking(move || { fs_move_with_timeout(&addr, &from, &to, Some(io_timeout)) diff --git a/payload/include/config.h b/payload/include/config.h index 3cc2bdd8..56726cfc 100644 --- a/payload/include/config.h +++ b/payload/include/config.h @@ -5,7 +5,7 @@ * UI tell apart an old payload still running from a build that includes * a particular fix, without having to boot the console. Keep in sync * with the desktop app's package.json during releases. */ -#define PS5UPLOAD2_VERSION "3.3.20" +#define PS5UPLOAD2_VERSION "3.3.21" /* Author credit — embedded in the startup toast so anyone looking at * the console screen knows who wrote the software that just loaded. * Kept separate from VERSION so release scripts can bump the version diff --git a/payload/src/runtime.c b/payload/src/runtime.c index b9b4c933..53712327 100644 --- a/payload/src/runtime.c +++ b/payload/src/runtime.c @@ -7008,9 +7008,14 @@ static int handle_fs_delete(runtime_state_t *state, int client_fd, } /* ── FS_MOVE handler ──────────────────────────────────────────────────── - * rename(2) only works intra-volume on POSIX. Cross-volume moves return - * EXDEV, which we surface as a specific error so the client can tell - * the user "move across mounts not supported". */ + * rename(2) only works intra-volume on POSIX. Cross-volume moves are supposed + * to fail with EXDEV — but on this kernel a cross-DEVICE rename (e.g. a USB + * exFAT file → the internal SSD) does NOT return EXDEV: it KERNEL PANICS and + * crashes the console (a reproducible hard crash, reported on USB→internal cut + * & paste). So we must NEVER let rename() run across devices. We compare the + * source's device id against the destination directory's up front and surface + * `fs_move_cross_mount` instead — the client then completes the move safely as + * copy-then-delete (FS_COPY reads/writes bytes, which is cross-volume safe). */ static int handle_fs_move(runtime_state_t *state, int client_fd, uint64_t trace_id, const char *request_body, uint64_t body_len) { char from[512], to[512]; @@ -7025,6 +7030,31 @@ static int handle_fs_move(runtime_state_t *state, int client_fd, return send_frame(client_fd, FTX2_FRAME_ERROR, 0, trace_id, "fs_move_path_not_allowed", 24); } + /* Cross-device guard — see the header comment. Compare st_dev of the source + * and of the destination's parent directory; if they differ, refuse the + * rename (it would panic) and report cross-mount. stat() on USB is safe + * (FS_COPY stats the same paths). If either stat fails we fall through to + * rename(), but only when devices can't be compared — a missing source/dest + * there fails with a normal errno, not the cross-device panic. */ + { + struct stat sf, sdp; + char to_dir[512]; + const char *slash = strrchr(to, '/'); + if (slash && slash != to) { + size_t dlen = (size_t)(slash - to); + if (dlen >= sizeof(to_dir)) dlen = sizeof(to_dir) - 1; + memcpy(to_dir, to, dlen); + to_dir[dlen] = '\0'; + } else { + to_dir[0] = '/'; + to_dir[1] = '\0'; + } + if (stat(from, &sf) == 0 && stat(to_dir, &sdp) == 0 && + sf.st_dev != sdp.st_dev) { + return send_frame(client_fd, FTX2_FRAME_ERROR, 0, trace_id, + "fs_move_cross_mount", 19); + } + } if (rename(from, to) != 0) { if (errno == EXDEV) { return send_frame(client_fd, FTX2_FRAME_ERROR, 0, trace_id, @@ -12237,13 +12267,37 @@ static int handle_shell_builtin(const char *cmd_in, char **out_text, any_err = 1; continue; } - if (rename(argv[i], target) == 0) continue; - if (errno != EXDEV) { - len = shell_appendf(&out, &cap, len, - "mv: %s -> %s: %s\n", - argv[i], target, strerror(errno)); - any_err = 1; - continue; + /* A cross-DEVICE rename() panics this kernel instead of returning + * EXDEV (see handle_fs_move). Only attempt the rename when source + * and dest are on the SAME device; otherwise skip straight to the + * copy-then-unlink path below. Never call rename() across mounts. */ + int mv_same_dev = 0; + { + struct stat mv_sf, mv_dd; + char mv_dpar[1024]; + const char *mv_ds = strrchr(target, '/'); + if (mv_ds && mv_ds != target) { + size_t dl = (size_t)(mv_ds - target); + if (dl >= sizeof(mv_dpar)) dl = sizeof(mv_dpar) - 1; + memcpy(mv_dpar, target, dl); + mv_dpar[dl] = '\0'; + } else { + mv_dpar[0] = '/'; + mv_dpar[1] = '\0'; + } + mv_same_dev = (stat(argv[i], &mv_sf) == 0 && + stat(mv_dpar, &mv_dd) == 0 && + mv_sf.st_dev == mv_dd.st_dev); + } + if (mv_same_dev) { + if (rename(argv[i], target) == 0) continue; + if (errno != EXDEV) { + len = shell_appendf(&out, &cap, len, + "mv: %s -> %s: %s\n", + argv[i], target, strerror(errno)); + any_err = 1; + continue; + } } /* Cross-FS — copy then unlink. Single file only; cross-FS * directory mv is too complex for shell tab (use cp -r +