Restrict sandbox mnt aliases to mounted storage volumes

This commit is contained in:
Roman Tarasov committed 2026-10-10 18:33:49 +03:00
1 parent 721f8e3a6b
commit 83fe699aae
2 files changed
+61 -44

No files matched your search

+2 -2
View File
@@ -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=<TITLE_ID>` 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/<TITLE_ID>_<index>/mnt` and `/mnt/sandbox/<TITLE_ID>_<index>/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=<TITLE_ID>` 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.
+59 -42
View File
@@ -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, &current) != 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)) {