From 83fe699aaeb5d427954a9bb5754f478191c2304e Mon Sep 17 00:00:00 2001 From: Roman Tarasov Date: Sat, 10 Oct 2026 18:33:49 +0300 Subject: [PATCH] Restrict sandbox mnt aliases to mounted storage volumes --- README.md | 4 +- src/sm_fakelib.c | 101 +++++++++++++++++++++++++++-------------------- 2 files changed, 61 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index cbaebf1..a2eb830 100644 --- a/README.md +++ b/README.md @@ -190,8 +190,8 @@ Backport overlay behavior: - The game card in the web interface offers `Default`, `Full`, `Emulators only` and `Disabled` fakelib modes. `Default` inherits `fakelib_default_mode` from web settings or `config.ini` (initially `full`). An explicit per-game mode overrides that default, including `full`. Modes apply to the selected game fakelib (including `fakelib2`) and global fakelib on the next launch; active mounts stay unchanged. Equivalent config entries are `fakelib_mode=PPSA12345:emulators` and `fakelib_mode=PPSA12345:full`; `default` removes the override. The last valid entry for a title wins, including legacy `fakelib_exclude` entries. - `Emulators only` mounts available regular files named `libSceAppContent.sprx`, `libSceGameUpdate.sprx` and `libSceNpEntitlementAccess.sprx` for installed PKGs. Folder/image games also allow `libSceAmpr.sprx` and `libScePlayGo.sprx`. Missing files are silently skipped; an empty selection creates no fakelib overlay. Emulator updates obey the same allowlist. - With fakelib enabled, `libkernel.sprx` is removed from the selected writable folder fakelib and participating global fakelib. For image games and installed PKGs, source files remain unchanged and a cache omitting this library is mounted instead. If a required filtered cache cannot be created, the unfiltered source is never mounted. -- Use repeatable `fakelib_exclude=` entries to disable per-game `fakelib`, `fakelib2`, global fakelib and cache preparation for specific games. `/data`, `/mnt` and PKG `app0` backports remain enabled. Reloaded exclusions apply on the next launch; existing mounts remain until game exit. Title IDs are case-insensitive and must have a supported prefix followed by five digits; invalid entries and entries beyond the 128-title limit are logged and ignored. -- `/mnt` and `/data` are always mounted for PS5 (`PPSA*`), PS4 (`CUSA*`) and homebrew (`LAPY*`, `FAKE*`) titles through writable `nullfs` at `/mnt/sandbox/_/mnt` and `/mnt/sandbox/_/data`. Supported IDs have one of these prefixes followed by five digits. These mounts follow the game process lifetime: they are removed on process exit. +- Use repeatable `fakelib_exclude=` entries to disable per-game `fakelib`, `fakelib2`, global fakelib and cache preparation for specific games. `/data`, sandbox storage aliases and PKG `app0` backports remain enabled. Reloaded exclusions apply on the next launch; existing mounts remain until game exit. Title IDs are case-insensitive and must have a supported prefix followed by five digits; invalid entries and entries beyond the 128-title limit are logged and ignored. +- `/data` is mounted through writable `nullfs` for PS5 (`PPSA*`), PS4 (`CUSA*`) and homebrew (`LAPY*`, `FAKE*`) titles. Sandbox `/mnt` remains a local directory: only mounted volumes at `/mnt/usb0` through `/mnt/usb7`, `/mnt/ext0` through `/mnt/ext1` and `/mnt/disc` receive individual writable `nullfs` aliases. Empty mount directories and other host `/mnt` entries are skipped. USB aliases follow device hotplug. Supported IDs have one of these prefixes followed by five digits. All aliases are removed on game process exit. - `backport_fakelib=0` disables the sandbox `fakelib` watcher, including global fakelib and emulator updates. - For `backport_fakelib` to work correctly, the standalone `BackPork` payload must be disabled. Running both at the same time will conflict. diff --git a/src/sm_fakelib.c b/src/sm_fakelib.c index 2484a07..97655b4 100644 --- a/src/sm_fakelib.c +++ b/src/sm_fakelib.c @@ -45,8 +45,8 @@ typedef struct { char title_id[MAX_TITLE_ID]; char sandbox_app0_path[MAX_PATH]; char mount_path[MAX_PATH]; - // fakelib, /mnt, /data, fonts, and up to two created font parents. - fakelib_layer_t layers[6u + SANDBOX_USB_ROOT_COUNT + SANDBOX_EXT_ROOT_COUNT]; + // fakelib, /data, /mnt/disc, fonts, and up to three created parents. + fakelib_layer_t layers[7u + SANDBOX_USB_ROOT_COUNT + SANDBOX_EXT_ROOT_COUNT]; size_t layer_count; sm_l10n_key_t notification; bool notify_pending; @@ -229,6 +229,42 @@ static bool mount_sandbox_directory(const char *title_id, return true; } +// Own only directories we create; cleanup removes them after their children. +static bool ensure_sandbox_parent(const char *app0_path, const char *parent) { + size_t app0_len = strlen(app0_path); + if (app0_len < 5u || strcmp(app0_path + app0_len - 5u, "/app0") != 0) { + errno = ENOENT; + return false; + } + if (g_fakelib_mount.layer_count >= + sizeof(g_fakelib_mount.layers) / sizeof(g_fakelib_mount.layers[0])) { + errno = ENOSPC; + return false; + } + char path[MAX_PATH]; + int written = snprintf(path, sizeof(path), "%.*s%s", + (int)(app0_len - 5u), app0_path, parent); + if (written < 0 || (size_t)written >= sizeof(path)) { + errno = ENAMETOOLONG; + return false; + } + if (mkdir(path, 0777) != 0) { + struct stat st; + if (errno != EEXIST || lstat(path, &st) != 0) + return false; + if (!S_ISDIR(st.st_mode)) { + errno = ENOTDIR; + return false; + } + return true; + } + fakelib_layer_t *layer = + &g_fakelib_mount.layers[g_fakelib_mount.layer_count++]; + layer->created_mount_path = true; + (void)strlcpy(layer->mount_path, path, sizeof(layer->mount_path)); + return true; +} + static bool mount_sandbox_fonts(const char *title_id, const char *app0_path) { const char *source_path = "/preinst/common/font"; struct stat st; @@ -239,42 +275,9 @@ static bool mount_sandbox_fonts(const char *title_id, const char *app0_path) { return false; } - size_t app0_len = strlen(app0_path); - if (app0_len < 5u || strcmp(app0_path + app0_len - 5u, "/app0") != 0) { - errno = ENOENT; + if (!ensure_sandbox_parent(app0_path, "/preinst") || + !ensure_sandbox_parent(app0_path, "/preinst/common")) return false; - } - - // Track only parents we create, so rollback and shutdown remove them after - // the read-only font mount. Empty source paths represent directory ownership. - const char *parents[] = {"/preinst", "/preinst/common"}; - for (size_t i = 0; i < sizeof(parents) / sizeof(parents[0]); ++i) { - if (g_fakelib_mount.layer_count >= - sizeof(g_fakelib_mount.layers) / sizeof(g_fakelib_mount.layers[0])) { - errno = ENOSPC; - return false; - } - char path[MAX_PATH]; - int written = snprintf(path, sizeof(path), "%.*s%s", - (int)(app0_len - 5u), app0_path, parents[i]); - if (written < 0 || (size_t)written >= sizeof(path)) { - errno = ENAMETOOLONG; - return false; - } - if (mkdir(path, 0777) != 0) { - if (errno != EEXIST || lstat(path, &st) != 0) - return false; - if (!S_ISDIR(st.st_mode)) { - errno = ENOTDIR; - return false; - } - continue; - } - fakelib_layer_t *layer = - &g_fakelib_mount.layers[g_fakelib_mount.layer_count++]; - layer->created_mount_path = true; - (void)strlcpy(layer->mount_path, path, sizeof(layer->mount_path)); - } if (mount_sandbox_directory(title_id, app0_path, source_path, MNT_RDONLY)) return true; @@ -295,16 +298,17 @@ static bool mount_sandbox_storage_roots(const char *title_id, return false; } - // nullfs aliases one filesystem; /mnt's tmpfs does not expose the mounted - // USB/external filesystems below it. Bind each connected storage root itself. + // Keep sandbox /mnt local and expose only mounted storage volumes. for (unsigned slot = 0; - slot < SANDBOX_USB_ROOT_COUNT + SANDBOX_EXT_ROOT_COUNT; ++slot) { + slot <= SANDBOX_USB_ROOT_COUNT + SANDBOX_EXT_ROOT_COUNT; ++slot) { char source_path[MAX_PATH]; if (slot < SANDBOX_USB_ROOT_COUNT) (void)snprintf(source_path, sizeof(source_path), "/mnt/usb%u", slot); - else + else if (slot < SANDBOX_USB_ROOT_COUNT + SANDBOX_EXT_ROOT_COUNT) (void)snprintf(source_path, sizeof(source_path), "/mnt/ext%u", slot - SANDBOX_USB_ROOT_COUNT); + else + (void)strlcpy(source_path, "/mnt/disc", sizeof(source_path)); bool mounted = false; for (int i = 0; i < mount_count; ++i) { @@ -316,6 +320,19 @@ static bool mount_sandbox_storage_roots(const char *title_id, if (!mounted) continue; + // A removed volume can leave an empty directory after the snapshot. + struct statfs current; + if (statfs(source_path, ¤t) != 0) { + int stat_errno = errno; + if (stat_errno == ENOENT) + continue; + free(mounts); + errno = stat_errno; + return false; + } + if (strcmp(current.f_mntonname, source_path) != 0) + continue; + if (!mount_sandbox_directory(title_id, app0_path, source_path, 0)) { int mount_errno = errno; // Removal between the snapshot and lookup is an optional-source skip. @@ -1801,7 +1818,7 @@ static bool mount_fakelib_for_game_locked(pid_t pid, const char *title_id, : (has_game ? "game" : "global"); if ((has_source && !track_sandbox_layer(title_id, source_path, mount_path, label, true, 0)) || - !mount_sandbox_directory(title_id, sandbox_app0_path, "/mnt", 0) || + !ensure_sandbox_parent(sandbox_app0_path, "/mnt") || !mount_sandbox_directory(title_id, sandbox_app0_path, "/data", 0) || !mount_sandbox_storage_roots(title_id, sandbox_app0_path) || !mount_sandbox_fonts(title_id, sandbox_app0_path)) {