Files
ShadowMountPlus/src/sm_filesystem.c
T

1634 lines
50 KiB
C

#include "sm_platform.h"
#include "sm_shellcore_service.h"
#include "sm_runtime.h"
#include "sm_filesystem.h"
#include "sm_config_mount.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_mount_diag.h"
#include "sm_image_cache.h"
#include "sm_image.h"
#include "sm_path_utils.h"
#include "sm_paths.h"
static bool unmount_controlled_mount_stack(const char *path);
static bool unmount_controlled_mount_stack_impl(const char *path,
bool diagnose_busy);
int sm_mount_table_snapshot(struct statfs **mounts_out) {
*mounts_out = NULL;
int count = getfsstat(NULL, 0, MNT_NOWAIT);
if (count < 0)
return -1;
// A full buffer may be truncated by concurrent mounts. Grow it a bounded
// number of times before reporting an unavailable table.
size_t capacity = (size_t)count + 16u;
for (int attempt = 0; attempt < 3; ++attempt) {
if (capacity > (size_t)LONG_MAX / sizeof(struct statfs)) {
errno = EOVERFLOW;
return -1;
}
size_t bytes = capacity * sizeof(struct statfs);
struct statfs *mounts = malloc(bytes);
if (!mounts)
return -1;
count = getfsstat(mounts, (long)bytes, MNT_NOWAIT);
if (count >= 0 && (size_t)count < capacity) {
*mounts_out = mounts;
return count;
}
int saved_errno = errno;
free(mounts);
if (count < 0) {
errno = saved_errno;
return -1;
}
capacity *= 2u;
}
errno = EAGAIN;
return -1;
}
// --- FILESYSTEM ---
bool is_installed(const char *title_id) {
char path[MAX_PATH];
snprintf(path, sizeof(path), "%s/%s", APP_BASE, title_id);
struct stat st;
return (stat(path, &st) == 0);
}
bool has_appmeta_data(const char *title_id) {
char path[MAX_PATH];
snprintf(path, sizeof(path), "%s/%s/param.json", APPMETA_BASE, title_id);
struct stat st;
return (stat(path, &st) == 0 && S_ISREG(st.st_mode));
}
bool is_data_mounted(const char *title_id) {
char path[MAX_PATH];
snprintf(path, sizeof(path), "/system_ex/app/%s/sce_sys/param.json",
title_id);
return path_exists(path);
}
static bool read_mount_link_file(const char *lnk_path, char *out,
size_t out_size) {
if (!lnk_path || out_size == 0)
return false;
out[0] = '\0';
FILE *f = fopen(lnk_path, "r");
if (!f)
return false;
if (!fgets(out, (int)out_size, f)) {
fclose(f);
out[0] = '\0';
return false;
}
size_t len = strcspn(out, "\r\n");
bool truncated = false;
if (out[len] == '\0' && len + 1u >= out_size) {
int next = fgetc(f);
truncated = (next != EOF && next != '\n' && next != '\r');
}
fclose(f);
if (truncated) {
out[0] = '\0';
return false;
}
out[len] = '\0';
return out[0] != '\0';
}
static void build_title_link_path(const char *title_id, const char *filename,
char out[MAX_PATH]) {
snprintf(out, MAX_PATH, "%s/%s/%s", APP_BASE, title_id, filename);
}
typedef struct {
char mount_link[MAX_PATH];
char staged_mount_link[MAX_PATH];
char mount_image_link[MAX_PATH];
} title_link_paths_t;
static void build_title_link_paths(const char *title_id,
title_link_paths_t *paths) {
build_title_link_path(title_id, "mount.lnk", paths->mount_link);
build_title_link_path(title_id, "mount.lnk.cleanup",
paths->staged_mount_link);
build_title_link_path(title_id, "mount_img.lnk", paths->mount_image_link);
}
typedef bool (*title_app_dir_iter_fn)(const char *title_id,
const title_link_paths_t *paths,
void *ctx);
static bool for_each_title_app_dir(const char *open_error_context,
bool stop_on_signal,
title_app_dir_iter_fn fn, void *ctx) {
DIR *d = opendir(APP_BASE);
if (!d) {
if (errno != ENOENT)
log_debug(" [LINK] open %s failed%s: %s", APP_BASE,
open_error_context ? open_error_context : "", strerror(errno));
return errno == ENOENT;
}
bool completed = true;
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
if (stop_on_signal && should_stop_requested()) {
completed = false;
break;
}
if (!resolve_title_app_dir(entry, NULL, 0))
continue;
title_link_paths_t paths;
build_title_link_paths(entry->d_name, &paths);
if (!fn(entry->d_name, &paths, ctx)) {
completed = false;
break;
}
}
if (closedir(d) != 0)
completed = false;
return completed;
}
bool read_mount_link(const char *title_id, char *out, size_t out_size) {
if (out_size == 0)
return false;
out[0] = '\0';
title_link_paths_t paths;
build_title_link_paths(title_id, &paths);
return read_mount_link_file(paths.mount_link, out, out_size);
}
static bool write_link_lines_atomic(const char *path,
const char *const *lines,
size_t count) {
char temp_path[MAX_PATH];
int written = snprintf(temp_path, sizeof(temp_path), "%s.tmp", path);
if (written < 0 || (size_t)written >= sizeof(temp_path))
return false;
FILE *file = fopen(temp_path, "w");
if (!file)
return false;
bool ok = true;
for (size_t i = 0; i < count; ++i) {
if (!lines[i] || lines[i][0] == '\0' ||
fprintf(file, "%s%s", lines[i], i + 1u < count ? "\n" : "") < 0) {
ok = false;
break;
}
}
if (ok && fflush(file) != 0)
ok = false;
if (ok && fsync(fileno(file)) != 0)
ok = false;
if (fclose(file) != 0)
ok = false;
if (ok && rename(temp_path, path) == 0)
return true;
int saved_errno = errno != 0 ? errno : EIO;
(void)unlink(temp_path);
errno = saved_errno;
return false;
}
bool write_mount_link(const char *title_id, const char *source_path) {
if (!title_id || title_id[0] == '\0' || !source_path ||
source_path[0] == '\0') {
return false;
}
title_link_paths_t paths;
build_title_link_paths(title_id, &paths);
const char *lines[] = {source_path};
return write_link_lines_atomic(paths.mount_link, lines, 1);
}
bool read_mount_image_link(const char *title_id, char *out, size_t out_size) {
if (!title_id || !out || out_size == 0)
return false;
out[0] = '\0';
title_link_paths_t paths;
build_title_link_paths(title_id, &paths);
return read_mount_link_file(paths.mount_image_link, out, out_size);
}
bool write_mount_image_link(const char *title_id, const char *image_path) {
if (!title_id || title_id[0] == '\0' || !image_path ||
image_path[0] == '\0') {
return false;
}
char chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH] = {{0}};
(void)strlcpy(chain[0], image_path, sizeof(chain[0]));
return write_mount_image_chain(title_id, chain, 1);
}
bool read_mount_image_chain(
const char *title_id,
char paths[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH], size_t *count_out) {
if (!title_id || !paths || !count_out)
return false;
*count_out = 0;
title_link_paths_t link_paths;
build_title_link_paths(title_id, &link_paths);
FILE *file = fopen(link_paths.mount_image_link, "r");
if (!file)
return false;
bool ok = true;
while (*count_out < MAX_IMAGE_CHAIN_DEPTH &&
fgets(paths[*count_out], MAX_PATH, file)) {
size_t len = strcspn(paths[*count_out], "\r\n");
if (paths[*count_out][len] == '\0' && len + 1u >= MAX_PATH) {
int next = fgetc(file);
if (next != EOF && next != '\n' && next != '\r') {
ok = false;
break;
}
}
paths[*count_out][len] = '\0';
if (len == 0) {
ok = false;
break;
}
(*count_out)++;
}
if (ok && *count_out == MAX_IMAGE_CHAIN_DEPTH && fgetc(file) != EOF)
ok = false;
if (ferror(file))
ok = false;
if (fclose(file) != 0)
ok = false;
if (!ok)
*count_out = 0;
return ok && *count_out > 0;
}
bool write_mount_image_chain(
const char *title_id,
const char paths[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH], size_t count) {
if (!title_id || title_id[0] == '\0' || !paths || count == 0 ||
count > MAX_IMAGE_CHAIN_DEPTH) {
return false;
}
title_link_paths_t link_paths;
build_title_link_paths(title_id, &link_paths);
const char *lines[MAX_IMAGE_CHAIN_DEPTH];
for (size_t i = 0; i < count; ++i)
lines[i] = paths[i];
return write_link_lines_atomic(link_paths.mount_image_link, lines, count);
}
bool resolve_title_app_dir(const struct dirent *entry, char *app_dir,
size_t app_dir_size) {
if (!entry)
return false;
if (entry->d_name[0] == '.')
return false;
if (strlen(entry->d_name) != 9)
return false;
char path_buf[MAX_PATH];
char *path = path_buf;
size_t path_size = sizeof(path_buf);
if (app_dir && app_dir_size > 0) {
path = app_dir;
path_size = app_dir_size;
}
int written = snprintf(path, path_size, "%s/%s", APP_BASE, entry->d_name);
if (written < 0 || (size_t)written >= path_size) {
if (app_dir && app_dir_size > 0)
app_dir[0] = '\0';
return false;
}
if (entry->d_type != DT_DIR) {
if (entry->d_type != DT_UNKNOWN)
return false;
struct stat st;
if (stat(path, &st) != 0 || !S_ISDIR(st.st_mode))
return false;
}
return true;
}
static bool source_path_needs_cleanup(const char *source_path) {
if (is_under_image_mount_base(source_path)) {
char image_source_path[MAX_PATH];
bool has_image_source = resolve_image_source_from_mount_cache(
source_path, image_source_path, sizeof(image_source_path));
if (has_image_source) {
if (!path_exists(image_source_path))
return true;
// The image mount point is intentionally absent outside install/launch.
if (!path_exists(source_path))
return false;
}
}
if (!path_exists(source_path))
return true;
if (path_matches_root_or_child(source_path, "/system_ex/app"))
return true;
char eboot_path[MAX_PATH];
int written =
snprintf(eboot_path, sizeof(eboot_path), "%s/eboot.bin", source_path);
if (written < 0 || (size_t)written >= sizeof(eboot_path))
return true;
return !path_exists(eboot_path);
}
static bool resolve_mount_image_source_path(const title_link_paths_t *paths,
const char *source_path,
char image_source_path[MAX_PATH]) {
image_source_path[0] = '\0';
if (!is_under_image_mount_base(source_path))
return false;
if (read_mount_link_file(paths->mount_image_link, image_source_path, MAX_PATH)) {
(void)cache_image_source_mapping(image_source_path, source_path);
return true;
}
return resolve_image_source_from_mount_cache(source_path, image_source_path,
MAX_PATH);
}
static bool cleanup_staged_mount_links_entry(const char *title_id,
const title_link_paths_t *paths,
void *ctx) {
(void)title_id;
(void)ctx;
struct stat staged_st;
if (stat(paths->staged_mount_link, &staged_st) == 0) {
struct stat link_st;
if (stat(paths->mount_link, &link_st) == 0) {
if (unlink(paths->staged_mount_link) == 0) {
log_debug(" [LINK] removed stale staged mount link: %s",
paths->staged_mount_link);
} else if (errno != ENOENT) {
log_debug(" [LINK] staged cleanup failed for %s: %s",
paths->staged_mount_link, strerror(errno));
}
} else if (errno != ENOENT) {
log_debug(" [LINK] staged cleanup failed for %s: %s",
paths->staged_mount_link, strerror(errno));
} else if (rename(paths->staged_mount_link, paths->mount_link) == 0) {
log_debug(" [LINK] restored interrupted mount link: %s",
paths->mount_link);
} else {
log_debug(" [LINK] restore failed for %s: %s", paths->mount_link,
strerror(errno));
return true;
}
} else if (errno != ENOENT) {
log_debug(" [LINK] staged cleanup failed for %s: %s",
paths->staged_mount_link, strerror(errno));
return true;
}
char source_path[MAX_PATH];
if (!read_mount_link_file(paths->mount_link, source_path, sizeof(source_path))) {
if (unlink(paths->mount_image_link) != 0 && errno != ENOENT) {
log_debug(" [LINK] remove failed for %s: %s", paths->mount_image_link,
strerror(errno));
}
return true;
}
if (!is_under_image_mount_base(source_path)) {
if (unlink(paths->mount_image_link) != 0 && errno != ENOENT) {
log_debug(" [LINK] remove failed for %s: %s", paths->mount_image_link,
strerror(errno));
}
return true;
}
char image_source_path[MAX_PATH];
if (!read_mount_link_file(paths->mount_image_link, image_source_path,
sizeof(image_source_path))) {
return true;
}
if (!cache_image_source_mapping(image_source_path, source_path)) {
log_debug(" [LINK] image source cache warmup failed: %s -> %s",
source_path, image_source_path);
}
return true;
}
void cleanup_staged_mount_links(void) {
for_each_title_app_dir(" for staged cleanup", false,
cleanup_staged_mount_links_entry, NULL);
}
// 1: mounted, 0: absent, -1: mount table unavailable.
static int get_top_mount(const char *path, struct statfs *mount_st_out) {
if (statfs(path, mount_st_out) == 0 &&
strcmp(mount_st_out->f_mntonname, path) == 0) {
return 1;
}
struct statfs *mntbuf = NULL;
int mntcount = sm_mount_table_snapshot(&mntbuf);
if (mntcount < 0)
return -1;
const struct statfs *best_mount = NULL;
for (int i = 0; i < mntcount; i++) {
if (strcmp(mntbuf[i].f_mntonname, path) != 0)
continue;
if (!best_mount ||
(strcmp(mntbuf[i].f_fstypename, "unionfs") == 0 &&
strcmp(best_mount->f_fstypename, "unionfs") != 0)) {
best_mount = &mntbuf[i];
}
}
bool found = best_mount != NULL;
if (found)
*mount_st_out = *best_mount;
free(mntbuf);
return found;
}
typedef enum {
TITLE_STACK_TOP_NONE = 0,
TITLE_STACK_TOP_OTHER,
TITLE_STACK_TOP_NULLFS,
TITLE_STACK_TOP_BACKPORT,
} title_stack_top_kind_t;
typedef struct {
bool mounted;
bool has_our_nullfs;
bool has_nullfs_from_root;
bool has_our_backport;
bool top_is_our_nullfs;
int our_nullfs_count;
int our_backport_count;
title_stack_top_kind_t top_kind;
char system_ex_path[MAX_PATH];
char top_from[MAX_PATH];
} title_mount_state_t;
static bool path_is_managed_backport_for_title(const char *title_id,
const char *path) {
if (strncmp(path, "<above>:", 8) == 0)
path += 8;
char root[MAX_PATH];
int scan_path_count = get_backport_scan_path_count();
for (int i = 0; i < scan_path_count; i++) {
char scan_path[MAX_PATH];
if (!get_backport_scan_path(i, scan_path) ||
!build_backports_root_path(scan_path, root))
continue;
size_t root_len = strlen(root);
if (strncmp(path, root, root_len) != 0)
continue;
if (path[root_len] != '/')
continue;
if (backport_name_matches_title(title_id, path + root_len + 1u))
return true;
}
return false;
}
static bool inspect_title_stack(const char *title_id, const char *source_path,
const char *source_root,
title_mount_state_t *state_out) {
memset(state_out, 0, sizeof(*state_out));
snprintf(state_out->system_ex_path, sizeof(state_out->system_ex_path),
"/system_ex/app/%s", title_id);
struct statfs statfs_mount;
bool statfs_ok = false;
int inspect_errno = 0;
int statfs_res = statfs(state_out->system_ex_path, &statfs_mount);
if (statfs_res == 0 &&
strcmp(statfs_mount.f_mntonname, state_out->system_ex_path) == 0) {
statfs_ok = true;
} else if (statfs_res != 0) {
inspect_errno = errno;
}
struct statfs *mntbuf = NULL;
int mntcount = sm_mount_table_snapshot(&mntbuf);
if (mntcount < 0)
return false;
const struct statfs *best_mount = NULL;
bool found_mount_entry = false;
if (mntcount > 0 && mntbuf) {
for (int i = 0; i < mntcount; i++) {
if (strcmp(mntbuf[i].f_mntonname, state_out->system_ex_path) != 0)
continue;
found_mount_entry = true;
if (!best_mount ||
(strcmp(mntbuf[i].f_fstypename, "unionfs") == 0 &&
strcmp(best_mount->f_fstypename, "unionfs") != 0)) {
best_mount = &mntbuf[i];
}
if (strcmp(mntbuf[i].f_fstypename, "nullfs") == 0) {
if (source_path && source_path[0] != '\0' &&
strcmp(mntbuf[i].f_mntfromname, source_path) == 0) {
state_out->has_our_nullfs = true;
state_out->our_nullfs_count++;
}
if (source_root && source_root[0] != '\0' &&
path_matches_root_or_child(mntbuf[i].f_mntfromname, source_root)) {
state_out->has_nullfs_from_root = true;
}
continue;
}
if (strcmp(mntbuf[i].f_fstypename, "unionfs") == 0 &&
path_is_managed_backport_for_title(title_id,
mntbuf[i].f_mntfromname)) {
state_out->has_our_backport = true;
state_out->our_backport_count++;
}
}
}
const struct statfs *top_mount = NULL;
if (statfs_ok) {
top_mount = &statfs_mount;
} else if (best_mount) {
top_mount = best_mount;
if (inspect_errno == EEXIST) {
log_debug(" [LINK] inspect recovered from statfs(EEXIST) for %s",
state_out->system_ex_path);
}
} else if (inspect_errno != 0 && inspect_errno != ENOENT &&
inspect_errno != EINVAL) {
log_debug(" [LINK] inspect statfs failed for %s: %s",
state_out->system_ex_path, strerror(inspect_errno));
if (mntcount <= 0 || !mntbuf) {
log_debug(" [LINK] mount table unavailable for %s",
state_out->system_ex_path);
} else if (!found_mount_entry) {
log_debug(" [LINK] no mount entries for %s", state_out->system_ex_path);
}
free(mntbuf);
errno = inspect_errno;
return false;
}
if (!top_mount) {
free(mntbuf);
return true;
}
const char *top_from = top_mount->f_mntfromname;
if (strcmp(top_mount->f_fstypename, "unionfs") == 0 &&
strncmp(top_from, "<above>:", 8) == 0) {
top_from += 8;
}
state_out->mounted = true;
(void)strlcpy(state_out->top_from, top_from, sizeof(state_out->top_from));
state_out->top_kind = TITLE_STACK_TOP_OTHER;
if (strcmp(top_mount->f_fstypename, "nullfs") == 0) {
state_out->top_kind = TITLE_STACK_TOP_NULLFS;
if (source_path && source_path[0] != '\0' &&
strcmp(top_from, source_path) == 0) {
state_out->top_is_our_nullfs = true;
state_out->has_our_nullfs = true;
}
} else if (strcmp(top_mount->f_fstypename, "unionfs") == 0 &&
path_is_managed_backport_for_title(title_id, top_from)) {
state_out->top_kind = TITLE_STACK_TOP_BACKPORT;
state_out->has_our_backport = true;
}
free(mntbuf);
return true;
}
static bool unmount_top_controlled_layer_impl(const char *path,
bool diagnose_busy) {
struct statfs mount_st;
int mounted = get_top_mount(path, &mount_st);
if (mounted <= 0)
return mounted == 0;
bool controlled_top =
(strcmp(mount_st.f_fstypename, "unionfs") == 0) ||
(strcmp(mount_st.f_fstypename, "nullfs") == 0);
if (!controlled_top) {
log_debug(" [LINK] mount point busy for %s: type=%s from=%s flags=0x%lX",
path, mount_st.f_fstypename, mount_st.f_mntfromname,
(unsigned long)mount_st.f_flags);
return false;
}
if (unmount(path, 0) == 0 || errno == ENOENT || errno == EINVAL)
return true;
int unmount_errno = errno;
if (unmount_errno == EBUSY && diagnose_busy)
sm_mount_diag_log_busy(path);
if (unmount_errno != EBUSY || diagnose_busy) {
log_debug(" [LINK] unmount deferred for %s: %s", path,
strerror(unmount_errno));
}
errno = unmount_errno;
return false;
}
static bool unmount_top_controlled_layer(const char *path) {
return unmount_top_controlled_layer_impl(path, true);
}
static bool title_stack_top_is_managed(const title_mount_state_t *state) {
return state->top_is_our_nullfs || state->top_kind == TITLE_STACK_TOP_BACKPORT;
}
bool rollback_title_nullfs_mount(const char *title_id, const char *src_path) {
title_mount_state_t state;
if (!inspect_title_stack(title_id, src_path, NULL, &state))
return false;
if (!state.top_is_our_nullfs)
return false;
if (!unmount_top_controlled_layer(state.system_ex_path))
return false;
log_debug(" [LINK] rolled back unpublished nullfs mount: %s -> %s",
src_path, state.system_ex_path);
return true;
}
static bool unmount_title_runtime_layers_impl(const char *title_id,
bool diagnose_busy) {
if (!title_id || title_id[0] == '\0')
return false;
if (sm_shellcore_title_is_protected(title_id)) {
errno = EBUSY;
return false;
}
char path[MAX_PATH];
int written = snprintf(path, sizeof(path), "/system_ex/app/%s", title_id);
if (written < 0 || (size_t)written >= sizeof(path))
return false;
if (!unmount_controlled_mount_stack_impl(path, diagnose_busy))
return false;
log_debug(" [LINK] runtime layers released: %s", title_id);
return true;
}
bool unmount_title_runtime_layers(const char *title_id) {
return unmount_title_runtime_layers_impl(title_id, true);
}
bool unmount_title_runtime_layers_quiet(const char *title_id) {
return unmount_title_runtime_layers_impl(title_id, false);
}
static bool unmount_managed_title_entry(
const char *title_id, const title_link_paths_t *paths, void *ctx) {
if (sm_shellcore_title_is_protected(title_id))
return true;
bool *all_released = (bool *)ctx;
char source_path[MAX_PATH];
if (!read_mount_link_file(paths->mount_link, source_path,
sizeof(source_path))) {
return true;
}
title_mount_state_t state;
if (!inspect_title_stack(title_id, source_path, NULL, &state)) {
*all_released = false;
return true;
}
if (!state.has_our_nullfs) {
return true;
}
if (!unmount_title_runtime_layers(title_id))
*all_released = false;
return true;
}
bool unmount_all_title_runtime_layers(void) {
bool all_released = true;
if (!for_each_title_app_dir(" during runtime mount cleanup", false,
unmount_managed_title_entry, &all_released)) {
all_released = false;
}
return all_released;
}
bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
const char *expected_backport_path,
bool *overlay_active_out) {
if (overlay_active_out)
*overlay_active_out = false;
if (!title_id || title_id[0] == '\0' || !src_path || src_path[0] == '\0' ||
!expected_backport_path || expected_backport_path[0] == '\0') {
return false;
}
struct stat backport_st;
bool backport_present = stat(expected_backport_path, &backport_st) == 0 &&
S_ISDIR(backport_st.st_mode);
title_mount_state_t state;
if (!inspect_title_stack(title_id, src_path, NULL, &state)) {
return false;
}
if (!state.has_our_nullfs)
return false;
if (state.top_is_our_nullfs)
return backport_present;
if (!state.has_our_backport || state.top_kind != TITLE_STACK_TOP_BACKPORT)
return false;
if (strcmp(state.top_from, expected_backport_path) == 0 && backport_present) {
if (overlay_active_out)
*overlay_active_out = true;
return true;
}
if (!unmount_top_controlled_layer(state.system_ex_path))
return false;
log_debug(" [IMG] backport overlay removed: %s -> %s", state.top_from,
state.system_ex_path);
return backport_present;
}
static void log_backport_overlay_path(const char *label, const char *path,
const struct stat *known_stat,
const struct statfs *known_statfs) {
struct stat path_stat;
const struct stat *st = known_stat;
if (!st) {
if (stat(path, &path_stat) != 0) {
int stat_errno = errno;
log_debug(" [IMG][UNIONFS] %s stat failed: path=%s error=%d (%s)",
label, path, stat_errno, strerror(stat_errno));
} else {
st = &path_stat;
}
}
if (st) {
log_debug(" [IMG][UNIONFS] %s stat: path=%s mode=0%o uid=%lu gid=%lu "
"dev=0x%llX ino=0x%llX",
label, path, (unsigned)st->st_mode,
(unsigned long)st->st_uid, (unsigned long)st->st_gid,
(unsigned long long)st->st_dev,
(unsigned long long)st->st_ino);
}
struct statfs path_statfs;
const struct statfs *sfs = known_statfs;
if (!sfs) {
if (statfs(path, &path_statfs) != 0) {
int statfs_errno = errno;
log_debug(" [IMG][UNIONFS] %s statfs failed: path=%s error=%d (%s)",
label, path, statfs_errno, strerror(statfs_errno));
} else {
sfs = &path_statfs;
}
}
if (sfs) {
log_debug(" [IMG][UNIONFS] %s fs: type=%s from=%s on=%s "
"flags=0x%lX bsize=%lu iosize=%ld",
label, sfs->f_fstypename, sfs->f_mntfromname,
sfs->f_mntonname, (unsigned long)sfs->f_flags,
(unsigned long)sfs->f_bsize, (long)sfs->f_iosize);
}
}
static void log_backport_overlay_failure(const char *mount_point,
const char *backport_path,
const struct stat *backport_stat,
const struct statfs *mounted_sfs,
int overlay_flags,
int overlay_errno) {
log_debug(" [IMG][UNIONFS] nmount failed: error=%d (%s) "
"mount_flags=0x%X read_only=%d euid=%lu egid=%lu "
"options=copymode:transparent,noatime,fnodup",
overlay_errno, strerror(overlay_errno), overlay_flags,
(overlay_flags & MNT_RDONLY) != 0 ? 1 : 0,
(unsigned long)geteuid(), (unsigned long)getegid());
log_backport_overlay_path("upper", backport_path, backport_stat, NULL);
log_backport_overlay_path("covered", mount_point, NULL, mounted_sfs);
struct statfs *mounts = NULL;
int mount_count = sm_mount_table_snapshot(&mounts);
if (mount_count < 0) {
int table_errno = errno;
log_debug(" [IMG][UNIONFS] mount table unavailable: error=%d (%s)",
table_errno, strerror(table_errno));
return;
}
int exact_count = 0;
for (int i = 0; i < mount_count; ++i) {
if (strcmp(mounts[i].f_mntonname, mount_point) != 0)
continue;
exact_count++;
log_debug(" [IMG][UNIONFS] covered layer[%d]: type=%s from=%s "
"flags=0x%lX",
exact_count - 1, mounts[i].f_fstypename,
mounts[i].f_mntfromname, (unsigned long)mounts[i].f_flags);
}
if (exact_count == 0) {
log_debug(" [IMG][UNIONFS] no exact mount-table entry for %s",
mount_point);
}
free(mounts);
}
backport_overlay_result_t mount_backport_overlay(const char *mount_point,
const char *backport_path,
const char *title_id) {
struct stat backport_st;
if (stat(backport_path, &backport_st) != 0 || !S_ISDIR(backport_st.st_mode))
return BACKPORT_OVERLAY_SKIPPED;
struct statfs mounted_sfs;
if (statfs(mount_point, &mounted_sfs) != 0 ||
strcmp(mounted_sfs.f_mntonname, mount_point) != 0) {
return BACKPORT_OVERLAY_SKIPPED;
}
if (strcmp(mounted_sfs.f_fstypename, "unionfs") == 0) {
const char *source = mounted_sfs.f_mntfromname;
if (strncmp(source, "<above>:", 8) == 0)
source += 8;
return strcmp(source, backport_path) == 0 ? BACKPORT_OVERLAY_ACTIVE
: BACKPORT_OVERLAY_SKIPPED;
}
bool mount_read_only = ((mounted_sfs.f_flags & MNT_RDONLY) != 0);
struct iovec overlay_iov[] = {
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("unionfs"),
IOVEC_ENTRY("from"), IOVEC_ENTRY(backport_path),
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_point),
IOVEC_ENTRY("copymode"), IOVEC_ENTRY("transparent"),
IOVEC_ENTRY("noatime"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("fnodup"), IOVEC_ENTRY(NULL)
};
int overlay_flags = mount_read_only ? MNT_RDONLY : 0;
if (nmount(overlay_iov, IOVEC_SIZE(overlay_iov), overlay_flags) == 0) {
log_debug(" [IMG] backport overlay mounted (%s): %s -> %s",
mount_read_only ? "ro" : "rw", backport_path, mount_point);
return BACKPORT_OVERLAY_ACTIVE;
}
int overlay_err = errno;
log_debug(" [IMG] backport overlay failed: %s -> %s (%s)", backport_path,
mount_point, strerror(overlay_err));
log_backport_overlay_failure(mount_point, backport_path, &backport_st,
&mounted_sfs, overlay_flags, overlay_err);
notify_system_l10n(SM_L10N_BACKPORT_OVERLAY_FAILED, title_id, backport_path,
(uint32_t)overlay_err);
errno = overlay_err;
return BACKPORT_OVERLAY_FAILED;
}
static bool unmount_controlled_mount_stack_impl(const char *path,
bool diagnose_busy) {
for (int i = 0; i < MAX_LAYERED_UNMOUNT_ATTEMPTS * 4; i++) {
struct statfs mount_st;
int mounted = get_top_mount(path, &mount_st);
if (mounted <= 0)
return mounted == 0;
if (!unmount_top_controlled_layer_impl(path, diagnose_busy))
return false;
}
struct statfs mount_st;
return get_top_mount(path, &mount_st) == 0;
}
static bool unmount_controlled_mount_stack(const char *path) {
return unmount_controlled_mount_stack_impl(path, true);
}
static void unmount_mount_point_for_recovery(const char *path) {
if (!unmount_controlled_mount_stack(path)) {
log_debug(" [LINK] failed to reset mount stack for recovery: %s", path);
}
}
static bool recover_mount_directory_after_stat_failure(
const char *path, struct stat *st_out) {
log_debug(" [LINK] resetting mount stack after stat failure: %s", path);
unmount_mount_point_for_recovery(path);
if (stat(path, st_out) == 0)
return true;
int stat_errno = errno;
if (stat_errno != ENOENT) {
log_debug(" [LINK] mount directory stat failed after reset for %s: %s",
path, strerror(stat_errno));
return false;
}
if (mkdir(path, 0755) != 0 && errno != EEXIST) {
log_debug(" [LINK] failed to recreate mount directory %s: %s", path,
strerror(errno));
return false;
}
if (stat(path, st_out) == 0)
return true;
log_debug(" [LINK] mount directory stat failed after recreate for %s: %s",
path, strerror(errno));
return false;
}
static bool cleanup_duplicate_title_mounts_entry(const char *title_id,
const title_link_paths_t *paths,
void *ctx) {
(void)ctx;
char source_path[MAX_PATH];
if (!read_mount_link_file(paths->mount_link, source_path, sizeof(source_path)))
return true;
title_mount_state_t state;
if (!inspect_title_stack(title_id, source_path, NULL, &state))
return true;
if ((state.our_nullfs_count <= 1 && state.our_backport_count <= 1) ||
!state.has_our_nullfs) {
return true;
}
if (!title_stack_top_is_managed(&state)) {
log_debug(" [LINK] duplicate managed mount layers kept for %s: top layer "
"is not managed by ShadowMount",
state.system_ex_path);
return true;
}
log_debug(
" [LINK] duplicate managed mount layers for %s: nullfs=%d backport=%d. "
"Resetting stack.",
state.system_ex_path, state.our_nullfs_count, state.our_backport_count);
if (!unmount_controlled_mount_stack(state.system_ex_path)) {
log_debug(" [LINK] failed to reset duplicate mount stack for %s",
state.system_ex_path);
}
return true;
}
void cleanup_duplicate_title_mounts(void) {
for_each_title_app_dir(" for duplicate cleanup", false,
cleanup_duplicate_title_mounts_entry, NULL);
}
// --- Copy Helpers for Install Action ---
static FILE *open_copy_destination(const char *path, bool set_mode,
mode_t mode) {
if (!set_mode)
return fopen(path, "wb");
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);
if (fd < 0)
return NULL;
FILE *file = fdopen(fd, "wb");
if (!file) {
int saved_errno = errno;
(void)close(fd);
(void)unlink(path);
errno = saved_errno;
}
return file;
}
static int copy_param_json_rewrite(const char *src, const char *dst,
bool set_mode, mode_t mode) {
FILE *fs = fopen(src, "rb");
if (!fs)
return -1;
if (fseek(fs, 0, SEEK_END) != 0) {
fclose(fs);
return -1;
}
long file_size = ftell(fs);
if (file_size < 0) {
fclose(fs);
return -1;
}
if ((unsigned long)file_size > MAX_PARAM_JSON_SIZE) {
fclose(fs);
return -1;
}
rewind(fs);
size_t len = (size_t)file_size;
char *buf = (char *)malloc(len + 1);
if (!buf) {
fclose(fs);
return -1;
}
if (len > 0 && fread(buf, 1, len, fs) != len) {
free(buf);
fclose(fs);
return -1;
}
fclose(fs);
buf[len] = '\0';
char *hit = strstr(buf, "upgradable");
if (hit) {
size_t offset = (size_t)(hit - buf);
size_t tail = len - offset - 10u;
memcpy(hit, "standard", 8u);
memmove(hit + 8u, hit + 10u, tail + 1u);
len -= 2u;
}
FILE *fd = open_copy_destination(dst, set_mode, mode);
if (!fd) {
free(buf);
return -1;
}
int ret = len > 0 && fwrite(buf, 1, len, fd) != len ? -1 : 0;
if (fclose(fd) != 0)
ret = -1;
if (ret == 0 && hit)
log_debug(" [COPY] param.json patched: %s", dst);
if (ret != 0)
(void)unlink(dst);
free(buf);
return ret;
}
static int copy_file_buffered(const char *src, const char *dst, bool set_mode,
mode_t mode, char *buffer,
size_t buffer_size) {
if (strstr(src, "/sce_sys/param.json")) {
return copy_param_json_rewrite(src, dst, set_mode, mode);
}
FILE *fs = fopen(src, "rb");
if (!fs)
return -1;
FILE *fd = open_copy_destination(dst, set_mode, mode);
if (!fd) {
fclose(fs);
return -1;
}
int ret = 0;
while (true) {
size_t n = fread(buffer, 1, buffer_size, fs);
if (n > 0 && fwrite(buffer, 1, n, fd) != n) {
ret = -1;
break;
}
if (n < buffer_size) {
if (ferror(fs))
ret = -1;
break;
}
}
if (fflush(fd) != 0)
ret = -1;
if (fclose(fd) != 0)
ret = -1;
if (fclose(fs) != 0)
ret = -1;
if (ret != 0)
(void)unlink(dst);
return ret;
}
static int copy_file_impl(const char *src, const char *dst, bool set_mode,
mode_t mode) {
const size_t buffer_size = 64u * 1024u;
char *buffer = malloc(buffer_size);
if (!buffer)
return -1;
int ret = copy_file_buffered(src, dst, set_mode, mode, buffer, buffer_size);
free(buffer);
return ret;
}
static int remove_copy_path(const char *path) {
struct stat st;
if (lstat(path, &st) != 0)
return errno == ENOENT ? 0 : -1;
if (!S_ISDIR(st.st_mode))
return unlink(path);
DIR *d = opendir(path);
if (!d)
return -1;
int ret = 0;
struct dirent *entry;
while (ret == 0 && (entry = readdir(d)) != NULL) {
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
char child[MAX_PATH];
int written = snprintf(child, sizeof(child), "%s/%s", path,
entry->d_name);
if (written < 0 || (size_t)written >= sizeof(child) ||
remove_copy_path(child) != 0) {
ret = -1;
}
}
if (closedir(d) != 0)
ret = -1;
if (ret == 0 && rmdir(path) != 0)
ret = -1;
return ret;
}
static int copy_dir_impl(const char *src, const char *dst, bool set_mode,
mode_t mode, bool overlay, char *buffer,
size_t buffer_size) {
if (mkdir(dst, set_mode ? mode : 0777) != 0) {
if (errno != EEXIST)
return -1;
struct stat dst_st;
if (lstat(dst, &dst_st) != 0)
return -1;
if (!S_ISDIR(dst_st.st_mode)) {
errno = ENOTDIR;
return -1;
}
}
DIR *d = opendir(src);
if (!d)
return -1;
int ret = 0;
char ss[MAX_PATH], dd[MAX_PATH];
struct stat st;
struct stat lst;
while (true) {
errno = 0;
struct dirent *e = readdir(d);
if (!e) {
if (errno != 0)
ret = -1;
break;
}
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, ".."))
continue;
int src_written = snprintf(ss, sizeof(ss), "%s/%s", src, e->d_name);
int dst_written = snprintf(dd, sizeof(dd), "%s/%s", dst, e->d_name);
if (src_written < 0 || (size_t)src_written >= sizeof(ss) ||
dst_written < 0 || (size_t)dst_written >= sizeof(dd)) {
errno = ENAMETOOLONG;
ret = -1;
break;
}
if (lstat(ss, &lst) != 0) {
ret = -1;
break;
}
if (S_ISLNK(lst.st_mode)) {
if (stat(ss, &st) != 0) {
ret = -1;
break;
}
if (S_ISDIR(st.st_mode)) {
log_debug(" [COPY] refusing symlink directory: %s", ss);
errno = EINVAL;
ret = -1;
break;
}
} else {
st = lst;
}
if (!S_ISDIR(st.st_mode) && !S_ISREG(st.st_mode)) {
errno = EINVAL;
ret = -1;
break;
}
if (overlay) {
struct stat dst_st;
if (lstat(dd, &dst_st) == 0) {
if (!S_ISDIR(st.st_mode) || !S_ISDIR(dst_st.st_mode)) {
if (remove_copy_path(dd) != 0) {
ret = -1;
break;
}
}
} else if (errno != ENOENT) {
ret = -1;
break;
}
}
if (S_ISDIR(st.st_mode)) {
if (copy_dir_impl(ss, dd, set_mode, mode, overlay, buffer,
buffer_size) != 0) {
ret = -1;
break;
}
} else {
if (copy_file_buffered(ss, dd, set_mode, mode, buffer, buffer_size) !=
0) {
ret = -1;
break;
}
}
}
int saved_errno = ret != 0 ? errno : 0;
if (closedir(d) != 0 && ret == 0) {
ret = -1;
saved_errno = errno;
}
if (ret != 0)
errno = saved_errno;
return ret;
}
static int copy_dir_with_options(const char *src, const char *dst,
bool set_mode, mode_t mode, bool overlay) {
const size_t buffer_size = 64u * 1024u;
char *buffer = malloc(buffer_size);
if (!buffer)
return -1;
int ret = copy_dir_impl(src, dst, set_mode, mode, overlay, buffer,
buffer_size);
free(buffer);
return ret;
}
int copy_file(const char *src, const char *dst) {
return copy_file_impl(src, dst, false, 0);
}
int copy_file_with_mode(const char *src, const char *dst, mode_t mode) {
return copy_file_impl(src, dst, true, mode);
}
int copy_dir(const char *src, const char *dst) {
return copy_dir_with_options(src, dst, false, 0, false);
}
int copy_dir_with_mode(const char *src, const char *dst, mode_t mode) {
return copy_dir_with_options(src, dst, true, mode, true);
}
int remount_system_ex(void) {
struct iovec iov[] = {
IOVEC_ENTRY("from"), IOVEC_ENTRY("/dev/ssd0.system_ex"),
IOVEC_ENTRY("fspath"), IOVEC_ENTRY("/system_ex"),
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("exfatfs"),
IOVEC_ENTRY("large"), IOVEC_ENTRY("yes"),
IOVEC_ENTRY("timezone"), IOVEC_ENTRY("static"),
IOVEC_ENTRY("async"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("ignoreacl"), IOVEC_ENTRY(NULL)};
return nmount(iov, IOVEC_SIZE(iov), MNT_UPDATE);
}
bool mount_title_nullfs(const char *title_id, const char *src_path) {
char dst[MAX_PATH];
char src_eboot[MAX_PATH];
char dst_eboot[MAX_PATH];
snprintf(dst, sizeof(dst), "/system_ex/app/%s", title_id);
int written = snprintf(src_eboot, sizeof(src_eboot), "%s/eboot.bin", src_path);
if (written < 0 || (size_t)written >= sizeof(src_eboot)) {
log_debug(" [LINK] source eboot path too long for %s: %s", title_id,
src_path);
return false;
}
snprintf(dst_eboot, sizeof(dst_eboot), "%s/eboot.bin", dst);
struct stat src_st;
if (stat(src_path, &src_st) != 0) {
log_debug(" [LINK] source path check failed for %s -> %s: %s", title_id,
src_path, strerror(errno));
return false;
}
if (!S_ISDIR(src_st.st_mode)) {
log_debug(" [LINK] source path is not a directory for %s: %s mode=0%o",
title_id, src_path, (unsigned)(src_st.st_mode & 077777));
return false;
}
if (!path_exists(src_eboot)) {
if (source_path_needs_cleanup(src_path)) {
log_debug(" [LINK] source eboot.bin missing for %s: %s", title_id,
src_eboot);
return false;
}
}
int mkdir_res = mkdir(dst, 0755);
if (mkdir_res != 0) {
if (errno != EEXIST) {
log_debug(" [LINK] Failed to create mount directory for title %s: %s",
title_id, strerror(errno));
return false;
}
struct stat dst_st;
if (stat(dst, &dst_st) != 0) {
int stat_errno = errno;
log_debug(" [LINK] mount directory exists but stat failed for %s: %s",
dst, strerror(stat_errno));
if (!recover_mount_directory_after_stat_failure(dst, &dst_st))
return false;
}
if (!S_ISDIR(dst_st.st_mode)) {
log_debug(" [LINK] mount target is not a directory: %s mode=0%o", dst,
(unsigned)(dst_st.st_mode & 077777));
return false;
}
}
title_mount_state_t state;
if (!inspect_title_stack(title_id, src_path, NULL, &state)) {
log_debug(" [LINK] inspect failed for %s (%s): %s", title_id, dst,
strerror(errno));
return false;
}
if (state.mounted) {
if (state.has_our_nullfs && title_stack_top_is_managed(&state) &&
path_exists(dst_eboot)) {
log_debug(" [LINK] mount stack already active: %s -> %s", src_path, dst);
return true;
}
if (!unmount_controlled_mount_stack(dst)) {
log_debug(" [LINK] failed to reset mount stack for %s (%s)", title_id,
dst);
return false;
}
}
struct iovec iov[] = {
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("nullfs"),
IOVEC_ENTRY("from"), IOVEC_ENTRY(src_path),
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(dst),
};
if (runtime_sleep_mode_active())
return false;
if (nmount(iov, IOVEC_SIZE(iov), 0) != 0) {
log_debug(" [LINK] Failed to auto-mount nullfs title=%s src=%s dst=%s: %s",
title_id, src_path, dst, strerror(errno));
return false;
}
if (runtime_sleep_mode_active()) {
if (!unmount_top_controlled_layer(dst))
log_debug(" [LINK] nullfs rollback deferred during sleep: %s", dst);
return false;
}
if (!path_exists(dst_eboot)) {
log_debug(" [LINK] mounted nullfs but eboot.bin is missing at target: %s",
dst_eboot);
if (!unmount_top_controlled_layer(dst))
log_debug(" [LINK] empty nullfs rollback deferred: %s", dst);
return false;
}
log_debug(" [LINK] nullfs mounted: %s -> %s", src_path, dst);
return true;
}
typedef struct {
const char *removed_source_root;
bool unmount_system_ex_bind;
bool force_remove_matching_source;
bool match_usb_sources;
bool preserve_mount_links;
} cleanup_mount_links_ctx_t;
static bool cleanup_mount_links_entry(const char *title_id,
const title_link_paths_t *paths,
void *ctx_ptr) {
cleanup_mount_links_ctx_t *ctx = (cleanup_mount_links_ctx_t *)ctx_ptr;
if (sm_shellcore_title_is_protected(title_id))
return true;
struct stat lst;
if (stat(paths->mount_link, &lst) != 0 || !S_ISREG(lst.st_mode))
return true;
char source_path[MAX_PATH];
char image_source_path[MAX_PATH];
source_path[0] = '\0';
image_source_path[0] = '\0';
bool has_image_source = false;
bool should_remove = false;
bool matches_removed_source = false;
if (!read_mount_link_file(paths->mount_link, source_path, sizeof(source_path))) {
should_remove = true;
} else {
has_image_source =
resolve_mount_image_source_path(paths, source_path, image_source_path);
}
if (!should_remove) {
if (ctx->match_usb_sources) {
matches_removed_source =
is_usb_storage_path(source_path) ||
(has_image_source && is_usb_storage_path(image_source_path));
if (!matches_removed_source)
return true;
should_remove = true;
} else if (ctx->removed_source_root && ctx->removed_source_root[0] != '\0') {
matches_removed_source =
path_matches_root_or_child(source_path, ctx->removed_source_root) ||
(has_image_source &&
path_matches_root_or_child(image_source_path,
ctx->removed_source_root));
if (!matches_removed_source)
return true;
should_remove = ctx->force_remove_matching_source ||
(has_image_source && !path_exists(image_source_path)) ||
source_path_needs_cleanup(source_path);
} else {
should_remove = (has_image_source && !path_exists(image_source_path)) ||
source_path_needs_cleanup(source_path);
}
}
if (!should_remove)
return true;
bool keep_mount_link = false;
bool mount_link_staged = false;
if (ctx->unmount_system_ex_bind) {
title_mount_state_t state;
memset(&state, 0, sizeof(state));
bool inspected = inspect_title_stack(title_id, source_path,
ctx->removed_source_root, &state);
if (!inspected) {
log_debug(" [LINK] keeping mount link for %s: mount state unavailable",
title_id);
return true;
}
bool top_is_ours = inspected && title_stack_top_is_managed(&state);
bool stack_matches_link = false;
if (inspected && source_path[0] != '\0') {
stack_matches_link = state.has_our_nullfs;
} else if (inspected && matches_removed_source && ctx->removed_source_root &&
ctx->removed_source_root[0] != '\0') {
stack_matches_link = state.has_nullfs_from_root;
}
if (stack_matches_link && !top_is_ours) {
keep_mount_link = true;
log_debug(" [LINK] keeping mount link for %s: stack still active but "
"top layer is not ours", state.system_ex_path);
} else if (top_is_ours && stack_matches_link) {
if (!ctx->preserve_mount_links) {
if (rename(paths->mount_link, paths->staged_mount_link) != 0) {
log_debug(" [LINK] stage failed for %s: %s", paths->mount_link,
strerror(errno));
return true;
}
mount_link_staged = true;
}
if (!unmount_controlled_mount_stack(state.system_ex_path)) {
keep_mount_link = true;
log_debug(" [LINK] failed to unmount mount stack for %s",
state.system_ex_path);
} else if (ctx->match_usb_sources) {
log_debug(" [LINK] unmounted USB-backed mount stack: %s source=%s%s%s",
state.system_ex_path, source_path,
has_image_source ? " image=" : "",
has_image_source ? image_source_path : "");
}
}
}
if (ctx->preserve_mount_links)
keep_mount_link = true;
if (!keep_mount_link) {
int unlink_res = mount_link_staged ? unlink(paths->staged_mount_link)
: unlink(paths->mount_link);
if (unlink_res == 0 || errno == ENOENT) {
log_debug(" [LINK] removed stale mount link: %s", paths->mount_link);
if (unlink(paths->mount_image_link) != 0 && errno != ENOENT) {
log_debug(" [LINK] remove failed for %s: %s", paths->mount_image_link,
strerror(errno));
}
} else {
log_debug(" [LINK] remove failed for %s: %s",
mount_link_staged ? paths->staged_mount_link : paths->mount_link,
strerror(errno));
}
return true;
}
if (mount_link_staged && rename(paths->staged_mount_link, paths->mount_link) == 0) {
log_debug(" [LINK] restored staged mount link: %s", paths->mount_link);
} else if (mount_link_staged) {
log_debug(" [LINK] restore failed for %s: %s", paths->mount_link,
strerror(errno));
}
return true;
}
void cleanup_mount_links(const char *removed_source_root,
bool unmount_system_ex_bind) {
cleanup_mount_links_ctx_t ctx = {
.removed_source_root = removed_source_root,
.unmount_system_ex_bind = unmount_system_ex_bind,
.force_remove_matching_source = false,
.match_usb_sources = false,
.preserve_mount_links = false,
};
for_each_title_app_dir("", !unmount_system_ex_bind, cleanup_mount_links_entry,
&ctx);
}
void cleanup_mount_links_for_source_unmount(const char *source_root) {
if (!source_root || source_root[0] == '\0')
return;
cleanup_mount_links_ctx_t ctx = {
.removed_source_root = source_root,
.unmount_system_ex_bind = true,
.force_remove_matching_source = true,
.match_usb_sources = false,
.preserve_mount_links = false,
};
for_each_title_app_dir(" during source unmount cleanup", false,
cleanup_mount_links_entry, &ctx);
}
void cleanup_usb_mount_links_for_suspend(void) {
cleanup_mount_links_ctx_t ctx = {
.removed_source_root = NULL,
.unmount_system_ex_bind = true,
.force_remove_matching_source = false,
.match_usb_sources = true,
.preserve_mount_links = true,
};
for_each_title_app_dir(" during USB suspend cleanup", false,
cleanup_mount_links_entry, &ctx);
}
static bool shutdown_title_mounts_entry(const char *title_id,
const title_link_paths_t *paths,
void *ctx) {
bool *all_released = (bool *)ctx;
char source_path[MAX_PATH];
if (!read_mount_link_file(paths->mount_link, source_path, sizeof(source_path))) {
return true;
}
title_mount_state_t state;
if (!inspect_title_stack(title_id, source_path, NULL, &state)) {
*all_released = false;
return true;
}
if (!state.has_our_nullfs)
return true;
if (!title_stack_top_is_managed(&state)) {
log_debug(" [LINK] shutdown stack blocked by unmanaged top for %s",
state.system_ex_path);
*all_released = false;
return true;
}
if (!unmount_controlled_mount_stack(state.system_ex_path)) {
log_debug(" [LINK] failed to unmount shutdown stack for %s",
state.system_ex_path);
*all_released = false;
}
return true;
}
bool shutdown_title_mounts(void) {
bool all_released = true;
if (!for_each_title_app_dir(" during shutdown", false,
shutdown_title_mounts_entry, &all_released)) {
all_released = false;
}
return all_released;
}