Files
ShadowMountPlus/src/sm_filesystem.c
T

996 lines
31 KiB
C

#include "sm_platform.h"
#include "sm_runtime.h"
#include "sm_filesystem.h"
#include "sm_config_mount.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_image_cache.h"
#include "sm_image.h"
#include "sm_path_utils.h"
#include "sm_paths.h"
// --- FILESYSTEM ---
bool is_installed(const char *title_id) {
char path[MAX_PATH];
snprintf(path, sizeof(path), "/user/app/%s", title_id);
struct stat st;
return (stat(path, &st) == 0);
}
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 (access(path, F_OK) == 0);
}
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;
}
fclose(f);
size_t len = strcspn(out, "\r\n");
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, "/user/app/%s/%s", 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 void 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("/user/app");
if (!d) {
if (errno != ENOENT)
log_debug(" [LINK] open /user/app failed%s: %s",
open_error_context ? open_error_context : "", strerror(errno));
return;
}
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
if (stop_on_signal && should_stop_requested())
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))
break;
}
closedir(d);
}
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);
}
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, "/user/app/%s", 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,
bool *tried_image_recovery) {
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 && access(image_source_path, F_OK) != 0)
return true;
}
if (access(source_path, F_OK) != 0)
return true;
if (path_matches_root_or_child(source_path, "/system_ex/app"))
return true;
char eboot_path[MAX_PATH];
snprintf(eboot_path, sizeof(eboot_path), "%s/eboot.bin", source_path);
if (access(eboot_path, F_OK) == 0)
return false;
if (!*tried_image_recovery && is_under_image_mount_base(source_path)) {
cleanup_stale_image_mounts();
*tried_image_recovery = true;
}
return access(eboot_path, F_OK) != 0;
}
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);
}
static bool 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 true;
}
struct statfs *mntbuf = NULL;
int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT);
if (mntcount <= 0 || !mntbuf)
return false;
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];
}
}
if (!best_mount)
return false;
*mount_st_out = *best_mount;
return true;
}
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_scan_path_count();
for (int i = 0; i < scan_path_count; i++) {
if (!build_backports_root_path(get_scan_path(i), root))
continue;
size_t root_len = strlen(root);
if (strncmp(path, root, root_len) != 0)
continue;
if (path[root_len] != '/')
continue;
if (strcmp(path + root_len + 1u, title_id) == 0)
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 = getmntinfo(&mntbuf, MNT_NOWAIT);
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);
}
for (int i = 0; i < mntcount; i++) {
if (strcmp(mntbuf[i].f_mntonname, state_out->system_ex_path) != 0)
continue;
log_debug(" [LINK] mount entry for %s: type=%s from=%s flags=0x%lX",
state_out->system_ex_path, mntbuf[i].f_fstypename,
mntbuf[i].f_mntfromname, (unsigned long)mntbuf[i].f_flags);
}
return false;
}
if (!top_mount)
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;
}
return true;
}
static bool unmount_top_controlled_layer(const char *path) {
struct statfs mount_st;
if (!get_top_mount(path, &mount_st))
return true;
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;
if (unmount(path, MNT_FORCE) == 0 || errno == ENOENT || errno == EINVAL)
return true;
log_debug(" [LINK] unmount failed for %s: %s", path, strerror(errno));
return false;
}
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;
}
void 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;
struct statfs mounted_sfs;
if (statfs(mount_point, &mounted_sfs) != 0 ||
strcmp(mounted_sfs.f_mntonname, mount_point) != 0) {
return;
}
if (strcmp(mounted_sfs.f_fstypename, "unionfs") == 0)
return;
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("notime"), 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;
}
int overlay_err = errno;
log_debug(" [IMG] backport overlay failed: %s -> %s (%s)", backport_path,
mount_point, strerror(overlay_err));
notify_system("Backport overlay failed: %s\n%s\n0x%08X", title_id,
backport_path, (uint32_t)overlay_err);
}
static bool unmount_controlled_mount_stack(const char *path) {
for (int i = 0; i < MAX_LAYERED_UNMOUNT_ATTEMPTS * 4; i++) {
struct statfs mount_st;
if (!get_top_mount(path, &mount_st))
return true;
if (!unmount_top_controlled_layer(path))
return false;
}
struct statfs mount_st;
return !get_top_mount(path, &mount_st);
}
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;
}
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 int copy_param_json_rewrite(const char *src, const char *dst) {
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;
}
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 = fopen(dst, "wb");
if (!fd) {
free(buf);
return -1;
}
int ret = 0;
if (len > 0 && fwrite(buf, 1, len, fd) != len)
ret = -1;
if (fclose(fd) != 0)
ret = -1;
if (ret == 0 && hit)
log_debug(" [COPY] param.json patched: %s", dst);
free(buf);
return ret;
}
int copy_file(const char *src, const char *dst) {
if (strstr(src, "/sce_sys/param.json")) {
return copy_param_json_rewrite(src, dst);
}
char buf[8192];
FILE *fs = fopen(src, "rb");
if (!fs)
return -1;
FILE *fd = fopen(dst, "wb");
if (!fd) {
fclose(fs);
return -1;
}
int ret = 0;
while (true) {
size_t n = fread(buf, 1, sizeof(buf), fs);
if (n > 0 && fwrite(buf, 1, n, fd) != n) {
ret = -1;
break;
}
if (n < sizeof(buf)) {
if (ferror(fs))
ret = -1;
break;
}
}
if (fflush(fd) != 0)
ret = -1;
if (fclose(fd) != 0)
ret = -1;
if (fclose(fs) != 0)
ret = -1;
return ret;
}
int copy_dir(const char *src, const char *dst) {
if (mkdir(dst, 0777) != 0 && errno != EEXIST)
return -1;
DIR *d = opendir(src);
if (!d)
return -1;
int ret = 0;
struct dirent *e;
char ss[MAX_PATH], dd[MAX_PATH];
struct stat st;
struct stat lst;
while ((e = readdir(d))) {
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, ".."))
continue;
snprintf(ss, sizeof(ss), "%s/%s", src, e->d_name);
snprintf(dd, sizeof(dd), "%s/%s", dst, e->d_name);
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);
ret = -1;
break;
}
} else {
st = lst;
}
if (S_ISDIR(st.st_mode)) {
if (copy_dir(ss, dd) != 0) {
ret = -1;
break;
}
} else {
if (copy_file(ss, dd) != 0) {
ret = -1;
break;
}
}
}
if (closedir(d) != 0)
ret = -1;
return ret;
}
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);
snprintf(src_eboot, sizeof(src_eboot), "%s/eboot.bin", src_path);
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 (access(src_eboot, F_OK) != 0) {
bool tried_image_recovery = false;
if (source_path_needs_cleanup(src_path, &tried_image_recovery)) {
log_debug(" [LINK] source eboot.bin missing for %s: %s", title_id,
src_eboot);
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 &&
(state.top_is_our_nullfs || state.has_our_backport) &&
access(dst_eboot, F_OK) == 0) {
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;
}
}
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) {
log_debug(" [LINK] mount directory exists but stat failed for %s: %s",
dst, strerror(errno));
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;
}
}
struct iovec iov[] = {
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("nullfs"),
IOVEC_ENTRY("from"), IOVEC_ENTRY(src_path),
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(dst),
};
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 (access(dst_eboot, F_OK) != 0) {
log_debug(" [LINK] mounted nullfs but eboot.bin is missing at target: %s",
dst_eboot);
if (unmount(dst, 0) != 0 && errno != ENOENT && errno != EINVAL) {
if (unmount(dst, MNT_FORCE) != 0 && errno != ENOENT && errno != EINVAL) {
log_debug(" [LINK] failed to rollback empty nullfs mount %s: %s", dst,
strerror(errno));
}
}
return false;
}
log_debug(" [LINK] nullfs mounted: %s -> %s", src_path, dst);
return true;
}
bool path_matches_root_or_child(const char *path, const char *root) {
if (!path || !root || root[0] == '\0')
return false;
size_t root_len = strlen(root);
if (strncmp(path, root, root_len) != 0)
return false;
return (path[root_len] == '\0' || path[root_len] == '/');
}
typedef struct {
const char *removed_source_root;
bool unmount_system_ex_bind;
bool tried_image_recovery;
} 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;
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];
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->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 = (has_image_source && access(image_source_path, F_OK) != 0) ||
source_path_needs_cleanup(source_path,
&ctx->tried_image_recovery);
} else {
should_remove = (has_image_source && access(image_source_path, F_OK) != 0) ||
source_path_needs_cleanup(source_path,
&ctx->tried_image_recovery);
}
}
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;
bool inspected = inspect_title_stack(title_id, source_path,
ctx->removed_source_root, &state);
bool top_is_ours = state.top_is_our_nullfs || state.has_our_backport;
bool stack_matches_link = false;
if (source_path[0] != '\0') {
stack_matches_link = state.has_our_nullfs;
} else if (matches_removed_source && ctx->removed_source_root &&
ctx->removed_source_root[0] != '\0') {
stack_matches_link = state.has_nullfs_from_root;
}
if (inspected && 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 (inspected && top_is_ours && stack_matches_link) {
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);
}
}
}
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 mount link after failed cleanup: %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,
.tried_image_recovery = false,
};
for_each_title_app_dir("", !unmount_system_ex_bind, cleanup_mount_links_entry,
&ctx);
}
static bool shutdown_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) ||
!state.has_our_nullfs ||
(!state.top_is_our_nullfs && !state.has_our_backport)) {
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);
}
return true;
}
void shutdown_title_mounts(void) {
for_each_title_app_dir(" during shutdown", false,
shutdown_title_mounts_entry, NULL);
}