Files
ShadowMountPlus/src/sm_image.c
T

1172 lines
40 KiB
C

#include "sm_platform.h"
#include "sm_runtime.h"
#include "sm_image.h"
#include "sm_hash.h"
#include "sm_image_cache.h"
#include "sm_game_cache.h"
#include "sm_log.h"
#include "sm_config_mount.h"
#include "sm_limits.h"
#include "sm_mount_defs.h"
#include "sm_mount_device.h"
#include "sm_filesystem.h"
#include "sm_path_state.h"
#include "sm_path_utils.h"
#include "sm_paths.h"
static uint32_t get_lvd_sector_size_fallback(image_fs_type_t fs_type) {
const runtime_config_t *cfg = runtime_config();
switch (fs_type) {
case IMAGE_FS_UFS:
return cfg->lvd_sector_ufs;
case IMAGE_FS_PFS:
return cfg->lvd_sector_pfs;
case IMAGE_FS_EXFAT:
default:
return cfg->lvd_sector_exfat;
}
}
static uint32_t get_image_sector_size_override_or_default(
const char *path, uint32_t fallback) {
uint32_t override = 0;
const char *filename = get_filename_component(path);
if (filename && filename[0] != '\0' &&
get_image_sector_size_override(filename, &override)) {
return override;
}
return fallback;
}
static uint32_t get_lvd_sector_size(const char *path, image_fs_type_t fs_type) {
uint32_t fallback = get_image_sector_size_override_or_default(
path, get_lvd_sector_size_fallback(fs_type));
struct statfs sfs;
if (statfs(path, &sfs) != 0)
return fallback;
uint64_t fs_cluster_size = (uint64_t)sfs.f_bsize;
if (fs_cluster_size == 0)
fs_cluster_size = (uint64_t)sfs.f_iosize;
if (fs_cluster_size == 0 || fs_cluster_size >= (uint64_t)fallback)
return fallback;
return (uint32_t)fs_cluster_size;
}
static uint32_t get_lvd_secondary_unit(const char *path,
image_fs_type_t fs_type) {
if (fs_type == IMAGE_FS_EXFAT)
return LVD_SECONDARY_UNIT_SINGLE_IMAGE;
return get_lvd_sector_size(path, fs_type);
}
static uint32_t get_md_sector_size(image_fs_type_t fs_type) {
const runtime_config_t *cfg = runtime_config();
uint32_t fallback = 0;
switch (fs_type) {
case IMAGE_FS_UFS:
fallback = cfg->md_sector_ufs;
break;
case IMAGE_FS_EXFAT:
default:
fallback = cfg->md_sector_exfat;
break;
}
return fallback;
}
static uint32_t get_md_sector_size_for_path(const char *path,
image_fs_type_t fs_type) {
return get_image_sector_size_override_or_default(path,
get_md_sector_size(fs_type));
}
static unsigned int get_md_attach_options(bool mount_read_only) {
unsigned int options = MD_AUTOUNIT | MD_ASYNC;
if (mount_read_only)
options |= MD_READONLY;
return options;
}
static uint16_t get_lvd_attach_raw_flags(image_fs_type_t fs_type,
bool mount_read_only) {
if (fs_type == IMAGE_FS_UFS) {
return mount_read_only ? LVD_ATTACH_RAW_FLAGS_DD_RO
: LVD_ATTACH_RAW_FLAGS_DD_RW;
}
return mount_read_only ? LVD_ATTACH_RAW_FLAGS_SINGLE_RO
: LVD_ATTACH_RAW_FLAGS_SINGLE_RW;
}
static unsigned int get_nmount_flags(image_fs_type_t fs_type,
bool mount_read_only,
const char **mount_mode_out) {
if (fs_type == IMAGE_FS_UFS) {
if (mount_mode_out)
*mount_mode_out = mount_read_only ? "dd_ro" : "dd_rw";
return mount_read_only ? UFS_NMOUNT_FLAG_RO : UFS_NMOUNT_FLAG_RW;
}
if (mount_mode_out)
*mount_mode_out = mount_read_only ? "rdonly" : "rw";
return mount_read_only ? MNT_RDONLY : 0;
}
static uint16_t normalize_lvd_raw_flags(uint16_t raw_flags) {
if ((raw_flags & 0x800Eu) != 0u) {
uint32_t raw = (uint32_t)raw_flags;
uint32_t len = (raw & 0xFFFF8000u) + ((raw & 2u) << 6) +
(8u * (raw & 1u)) + (2u * ((raw >> 2) & 1u)) +
(2u * (raw & 8u)) + 4u;
return (uint16_t)len;
}
return (uint16_t)(8u * ((uint32_t)raw_flags & 1u) + 4u);
}
static uint16_t get_lvd_image_type(image_fs_type_t fs_type) {
if (fs_type == IMAGE_FS_UFS)
return LVD_ATTACH_IMAGE_TYPE_UFS_DOWNLOAD_DATA;
if (fs_type == IMAGE_FS_PFS)
return LVD_ATTACH_IMAGE_TYPE_PFS_SAVE_DATA;
return LVD_ATTACH_IMAGE_TYPE_SINGLE;
}
static uint16_t get_lvd_source_type(const char *path) {
if (strncmp(path, "/dev/sbram0", strlen("/dev/sbram0")) == 0)
return LVD_ENTRY_TYPE_SPECIAL;
struct stat st;
if (stat(path, &st) == 0) {
if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode))
return LVD_ENTRY_TYPE_SPECIAL;
}
return LVD_ENTRY_TYPE_FILE;
}
// --- Image Path and Naming Helpers ---
static image_fs_type_t detect_image_fs_type(const char *name) {
const char *dot = strrchr(name, '.');
if (!dot)
return IMAGE_FS_UNKNOWN;
if (strcasecmp(dot, ".ffpkg") == 0)
return IMAGE_FS_UFS;
if (strcasecmp(dot, ".exfat") == 0)
return IMAGE_FS_EXFAT;
if (strcasecmp(dot, ".ffpfs") == 0)
return IMAGE_FS_PFS;
return IMAGE_FS_UNKNOWN;
}
bool is_supported_image_file_name(const char *name) {
return detect_image_fs_type(name) != IMAGE_FS_UNKNOWN;
}
static const char *image_fs_name(image_fs_type_t fs_type) {
switch (fs_type) {
case IMAGE_FS_UFS:
return "ufs";
case IMAGE_FS_EXFAT:
return "exfatfs";
case IMAGE_FS_PFS:
return "pfs";
default:
return "unknown";
}
}
void log_fs_stats(const char *tag, const char *path,
const char *type_hint) {
struct statfs sfs;
if (statfs(path, &sfs) != 0) {
log_debug(" [%s] FS stats read failed for %s: %s", tag, path,
strerror(errno));
return;
}
const char *type_name = type_hint;
if (sfs.f_fstypename[0] != '\0')
type_name = sfs.f_fstypename;
if (!type_name)
type_name = "unknown";
uint64_t bsize = (uint64_t)sfs.f_bsize;
uint64_t iosize = (uint64_t)sfs.f_iosize;
uint64_t blocks = (uint64_t)sfs.f_blocks;
uint64_t bfree = (uint64_t)sfs.f_bfree;
uint64_t bavail = (uint64_t)sfs.f_bavail;
uint64_t files = (uint64_t)sfs.f_files;
uint64_t ffree = (uint64_t)sfs.f_ffree;
uint64_t total_bytes = blocks * bsize;
uint64_t free_bytes = bfree * bsize;
uint64_t avail_bytes = bavail * bsize;
log_debug(" [%s] FS stats: path=%s type=%s bsize=%llu iosize=%llu "
"blocks=%llu bfree=%llu bavail=%llu files=%llu ffree=%llu "
"flags=0x%lX total=%lluB free=%lluB avail=%lluB",
tag, path, type_name, (unsigned long long)bsize,
(unsigned long long)iosize, (unsigned long long)blocks,
(unsigned long long)bfree, (unsigned long long)bavail,
(unsigned long long)files, (unsigned long long)ffree,
(unsigned long)sfs.f_flags, (unsigned long long)total_bytes,
(unsigned long long)free_bytes, (unsigned long long)avail_bytes);
}
static void strip_extension(const char *filename, char *out, size_t out_size) {
const char *dot = strrchr(filename, '.');
size_t len = dot ? (size_t)(dot - filename) : strlen(filename);
if (len >= out_size)
len = out_size - 1;
memcpy(out, filename, len);
out[len] = '\0';
}
static void build_image_mount_point(const char *file_path,
char mount_point[MAX_PATH]) {
const char *filename = get_filename_component(file_path);
char base_name[MAX_PATH];
char mount_name[MAX_PATH];
strip_extension(filename, base_name, sizeof(base_name));
size_t base_len = strlen(base_name);
size_t max_base_len = sizeof(mount_name) - 1u - 9u;
if (base_len > max_base_len)
base_len = max_base_len;
memcpy(mount_name, base_name, base_len);
mount_name[base_len] = '\0';
snprintf(mount_name + base_len, sizeof(mount_name) - base_len, "_%08x",
sm_fnv1a32(file_path));
snprintf(mount_point, MAX_PATH, "%s/%s", IMAGE_MOUNT_BASE, mount_name);
}
void get_image_mount_point_for_source(const char *file_path,
char mount_point[MAX_PATH]) {
build_image_mount_point(file_path, mount_point);
}
typedef bool (*image_attach_fn)(const char *file_path, image_fs_type_t fs_type,
bool mount_read_only, off_t file_size,
int *unit_id_out, char *devname_out,
size_t devname_size);
typedef struct {
attach_backend_t id;
image_attach_fn attach;
} image_backend_ops_t;
static bool attach_md_backend(const char *file_path, image_fs_type_t fs_type,
bool mount_read_only, off_t file_size,
int *unit_id_out, char *devname_out,
size_t devname_size) {
int md_fd = open(MD_CTRL_PATH, O_RDWR);
if (md_fd < 0) {
log_debug(" [IMG][%s] open %s failed: %s",
attach_backend_name(ATTACH_BACKEND_MD), MD_CTRL_PATH,
strerror(errno));
return false;
}
struct md_ioctl req;
memset(&req, 0, sizeof(req));
req.md_version = MDIOVERSION;
req.md_type = MD_VNODE;
req.md_file = (char *)file_path;
req.md_mediasize = file_size;
req.md_sectorsize = get_md_sector_size_for_path(file_path, fs_type);
req.md_options = get_md_attach_options(mount_read_only);
int last_errno = 0;
log_debug(" [IMG][%s] attach try: options=0x%x",
attach_backend_name(ATTACH_BACKEND_MD), req.md_options);
int ret = ioctl(md_fd, MDIOCATTACH, &req);
if (ret != 0)
last_errno = errno;
close(md_fd);
if (ret != 0) {
errno = last_errno;
log_debug(" [IMG][%s] attach failed: %s (ret: 0x%x)",
attach_backend_name(ATTACH_BACKEND_MD), strerror(errno), ret);
return false;
}
int unit_id = (int)req.md_unit;
if (unit_id < 0) {
log_debug(" [IMG][%s] attach returned invalid unit: %d",
attach_backend_name(ATTACH_BACKEND_MD), unit_id);
return false;
}
snprintf(devname_out, devname_size, "/dev/md%d", unit_id);
if (!wait_for_dev_node_state(devname_out, true)) {
log_debug(" [IMG][%s] device node did not appear: %s",
attach_backend_name(ATTACH_BACKEND_MD), devname_out);
(void)detach_attached_unit(ATTACH_BACKEND_MD, unit_id);
return false;
}
log_debug(" [IMG][%s] attach returned unit=%d",
attach_backend_name(ATTACH_BACKEND_MD), unit_id);
*unit_id_out = unit_id;
return true;
}
static bool attach_lvd_backend(const char *file_path, image_fs_type_t fs_type,
bool mount_read_only, off_t file_size,
int *unit_id_out, char *devname_out,
size_t devname_size) {
int lvd_fd = open(LVD_CTRL_PATH, O_RDWR);
if (lvd_fd < 0) {
log_debug(" [IMG][%s] open %s failed: %s",
attach_backend_name(ATTACH_BACKEND_LVD), LVD_CTRL_PATH,
strerror(errno));
return false;
}
lvd_ioctl_layer_v0_t layers[LVD_ATTACH_LAYER_COUNT];
memset(layers, 0, sizeof(layers));
layers[0].source_type = get_lvd_source_type(file_path);
layers[0].flags = LVD_ENTRY_FLAG_NO_BITMAP;
layers[0].path = file_path;
layers[0].offset = 0;
layers[0].size = (uint64_t)file_size;
uint32_t sector_size = get_lvd_sector_size(file_path, fs_type);
uint32_t secondary_unit = get_lvd_secondary_unit(file_path, fs_type);
uint16_t raw_flags = get_lvd_attach_raw_flags(fs_type, mount_read_only);
uint16_t normalized_flags = normalize_lvd_raw_flags(raw_flags);
lvd_ioctl_attach_v0_t req;
memset(&req, 0, sizeof(req));
req.io_version = LVD_ATTACH_IO_VERSION_V0;
req.image_type = get_lvd_image_type(fs_type);
req.layer_count = LVD_ATTACH_LAYER_COUNT;
req.device_size = (uint64_t)file_size;
req.layers_ptr = layers;
req.sector_size = sector_size;
req.secondary_unit = secondary_unit;
req.flags = normalized_flags;
req.device_id = -1;
int last_errno = 0;
log_debug(" [IMG][%s] attach try: ver=%u sec=%u sec2=%u raw=0x%x "
"flags=0x%x img=%u",
attach_backend_name(ATTACH_BACKEND_LVD), req.io_version,
req.sector_size, req.secondary_unit, raw_flags, req.flags,
req.image_type);
int ret = ioctl(lvd_fd, SCE_LVD_IOC_ATTACH_V0, &req);
if (ret != 0)
last_errno = errno;
close(lvd_fd);
int unit_id = req.device_id;
if (ret != 0) {
errno = last_errno;
log_debug(" [IMG][%s] attach failed: %s (ret: 0x%x)",
attach_backend_name(ATTACH_BACKEND_LVD), strerror(errno), ret);
return false;
}
if (unit_id < 0) {
log_debug(" [IMG][%s] attach returned invalid unit: %d",
attach_backend_name(ATTACH_BACKEND_LVD), unit_id);
return false;
}
log_debug(" [IMG][%s] attach returned unit=%d",
attach_backend_name(ATTACH_BACKEND_LVD), unit_id);
snprintf(devname_out, devname_size, "/dev/lvd%d", unit_id);
if (!wait_for_dev_node_state(devname_out, true)) {
log_debug(" [IMG][%s] device node did not appear: %s",
attach_backend_name(ATTACH_BACKEND_LVD), devname_out);
(void)detach_attached_unit(ATTACH_BACKEND_LVD, unit_id);
return false;
}
*unit_id_out = unit_id;
return true;
}
static const image_backend_ops_t *get_image_backend_ops(attach_backend_t backend) {
static const image_backend_ops_t md_ops = {
.id = ATTACH_BACKEND_MD, .attach = attach_md_backend};
static const image_backend_ops_t lvd_ops = {
.id = ATTACH_BACKEND_LVD, .attach = attach_lvd_backend};
if (backend == ATTACH_BACKEND_MD)
return &md_ops;
if (backend == ATTACH_BACKEND_LVD)
return &lvd_ops;
return NULL;
}
static bool is_cached_image_path(const char *file_path) {
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
image_cache_entry_t cached_entry;
if (get_image_cache_entry(k, &cached_entry) &&
strcmp(cached_entry.path, file_path) == 0) {
return true;
}
}
return false;
}
static bool directory_has_visible_entries(const char *path) {
DIR *dir = opendir(path);
if (!dir)
return false;
bool found = false;
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_name[0] == '.')
continue;
found = true;
break;
}
closedir(dir);
return found;
}
static bool is_image_mount_root_accessible(const char *mount_point,
int *error_out) {
if (error_out)
*error_out = 0;
DIR *dir = opendir(mount_point);
if (!dir) {
if (error_out)
*error_out = errno;
return false;
}
closedir(dir);
return true;
}
static bool reject_mounted_image_io(const char *file_path,
attach_backend_t attach_backend, int unit_id,
const char *devname,
const char *mount_point, int io_err,
const char *stage) {
sm_error_set("IMG", io_err, file_path, "Mounted image is unreadable or damaged");
log_debug(" [IMG][%s] unreadable or damaged mount (%s -> %s, %s): %s",
attach_backend_name(attach_backend), devname, mount_point, stage,
strerror(io_err));
(void)unmount_image(file_path, unit_id, attach_backend);
errno = io_err;
return false;
}
static bool prepare_image_mount_retry(const image_cache_entry_t *cached_entry,
char mount_point[MAX_PATH],
char source_path[MAX_PATH]) {
build_image_mount_point(cached_entry->path, mount_point);
(void)strlcpy(source_path, cached_entry->path, MAX_PATH);
if (is_active_image_mount_point(mount_point)) {
int root_err = 0;
if (is_image_mount_root_accessible(mount_point, &root_err))
return false;
log_debug(" [IMG][%s] mount unreadable, retrying: %s -> %s: %s",
attach_backend_name(cached_entry->backend), source_path,
mount_point, strerror(root_err));
if (!unmount_image(source_path, cached_entry->unit_id, cached_entry->backend))
return false;
} else {
log_debug(" [IMG][%s] mount lost, retrying: %s -> %s",
attach_backend_name(cached_entry->backend), source_path,
mount_point);
clear_cached_game(mount_point);
}
clear_missing_param_entry(mount_point);
return true;
}
static bool reuse_existing_image_mount(const char *file_path,
const char *mount_point,
bool *cache_failed_out) {
if (cache_failed_out)
*cache_failed_out = false;
struct stat mount_st;
if (stat(mount_point, &mount_st) != 0 || !S_ISDIR(mount_st.st_mode))
return false;
attach_backend_t existing_backend = ATTACH_BACKEND_NONE;
int existing_unit = -1;
if (resolve_device_from_mount(mount_point, &existing_backend, &existing_unit)) {
int root_err = 0;
if (!is_image_mount_root_accessible(mount_point, &root_err)) {
log_debug(" [IMG][%s] Existing mount unreadable, reattaching: %s -> %s: %s",
attach_backend_name(existing_backend), file_path, mount_point,
strerror(root_err));
if (!unmount_image(file_path, existing_unit, existing_backend)) {
if (cache_failed_out)
*cache_failed_out = true;
return false;
}
return false;
}
if (!cache_image_mount(file_path, mount_point, existing_unit,
existing_backend)) {
sm_error_set("IMG", ENOSPC, file_path,
"Image cache full (%u entries), cannot track mount %s",
(unsigned)MAX_IMAGE_MOUNTS, mount_point);
log_debug(" [IMG] image cache full, refusing unmanaged mount reuse: %s",
mount_point);
errno = ENOSPC;
if (cache_failed_out)
*cache_failed_out = true;
return false;
}
log_debug(" [IMG][%s] Already mounted: %s",
attach_backend_name(existing_backend), mount_point);
return true;
}
if (!directory_has_visible_entries(mount_point))
return false;
log_debug(" [IMG] Mount point exists and is non-empty but is not an active "
"mount, reattaching: %s", mount_point);
return false;
}
static bool handle_cached_or_existing_image_mount_locked(
const char *file_path, const char *mount_point, bool *success_out) {
if (success_out)
*success_out = false;
if (runtime_sleep_mode_active())
return true;
if (is_cached_image_path(file_path)) {
if (success_out)
*success_out = true;
return true;
}
bool cache_failed = false;
if (reuse_existing_image_mount(file_path, mount_point, &cache_failed)) {
if (success_out)
*success_out = !runtime_sleep_mode_active();
return true;
}
return cache_failed;
}
static bool stat_image_file(const char *file_path, struct stat *st_out) {
if (stat(file_path, st_out) != 0) {
log_debug(" [IMG] stat failed for %s: %s", file_path, strerror(errno));
return false;
}
if (st_out->st_size < 0) {
log_debug(" [IMG] invalid file size for %s: %lld", file_path,
(long long)st_out->st_size);
errno = EINVAL;
return false;
}
return true;
}
static void ensure_mount_dirs(const char *mount_point) {
mkdir(IMAGE_MOUNT_BASE, 0777);
mkdir(mount_point, 0777);
}
static attach_backend_t select_image_backend(const runtime_config_t *cfg,
image_fs_type_t fs_type) {
if (fs_type == IMAGE_FS_EXFAT)
return cfg->exfat_backend;
if (fs_type == IMAGE_FS_UFS)
return cfg->ufs_backend;
return ATTACH_BACKEND_LVD;
}
static bool attach_image_device(const char *file_path, image_fs_type_t fs_type,
bool mount_read_only, off_t file_size,
attach_backend_t attach_backend, int *unit_id_out,
char *devname_out, size_t devname_size) {
const image_backend_ops_t *backend_ops = get_image_backend_ops(attach_backend);
if (!backend_ops) {
log_debug(" [IMG] unsupported attach backend for %s", file_path);
errno = EINVAL;
return false;
}
if (!backend_ops->attach(file_path, fs_type, mount_read_only, file_size,
unit_id_out, devname_out, devname_size)) {
return false;
}
log_debug(" [IMG][%s] Attached as %s", attach_backend_name(attach_backend),
devname_out);
return true;
}
static bool perform_image_nmount(const char *file_path, image_fs_type_t fs_type,
attach_backend_t attach_backend, int unit_id,
const char *devname, const char *mount_point,
bool mount_read_only, bool force_mount) {
struct iovec *iov = NULL;
unsigned int iovlen = 0;
char mount_errmsg[256];
memset(mount_errmsg, 0, sizeof(mount_errmsg));
const char *sigverify = PFS_MOUNT_SIGVERIFY ? "1" : "0";
const char *playgo = PFS_MOUNT_PLAYGO ? "1" : "0";
const char *disc = PFS_MOUNT_DISC ? "1" : "0";
const char *ekpfs_key = PFS_ZERO_EKPFS_KEY_HEX;
struct iovec iov_ufs[] = {
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("ufs"),
IOVEC_ENTRY("from"), IOVEC_ENTRY(devname),
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_point),
IOVEC_ENTRY("budgetid"), IOVEC_ENTRY(DEVPFS_BUDGET_GAME),
IOVEC_ENTRY("async"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("noatime"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("automounted"), IOVEC_ENTRY(NULL),
// IOVEC_ENTRY("compressedfile"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("errmsg"), {(void *)mount_errmsg, sizeof(mount_errmsg)},
IOVEC_ENTRY("force"), IOVEC_ENTRY(NULL)};
struct iovec iov_exfat[] = {
IOVEC_ENTRY("from"), IOVEC_ENTRY(devname),
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_point),
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("exfatfs"),
IOVEC_ENTRY("budgetid"), IOVEC_ENTRY(DEVPFS_BUDGET_GAME),
IOVEC_ENTRY("large"), IOVEC_ENTRY("yes"),
IOVEC_ENTRY("timezone"), IOVEC_ENTRY("static"),
IOVEC_ENTRY("async"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("noatime"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("ignoreacl"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("automounted"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("errmsg"), {(void *)mount_errmsg, sizeof(mount_errmsg)},
IOVEC_ENTRY("force"), IOVEC_ENTRY(NULL)};
struct iovec iov_pfs[] = {
IOVEC_ENTRY("from"), IOVEC_ENTRY(devname),
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_point),
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("pfs"),
IOVEC_ENTRY("sigverify"), IOVEC_ENTRY(sigverify),
IOVEC_ENTRY("mkeymode"), IOVEC_ENTRY(PFS_MOUNT_MKEYMODE),
IOVEC_ENTRY("budgetid"), IOVEC_ENTRY(PFS_MOUNT_BUDGET_ID),
IOVEC_ENTRY("playgo"), IOVEC_ENTRY(playgo),
IOVEC_ENTRY("disc"), IOVEC_ENTRY(disc),
IOVEC_ENTRY("ekpfs"), IOVEC_ENTRY(ekpfs_key),
IOVEC_ENTRY("async"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("noatime"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("automounted"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("errmsg"), {(void *)mount_errmsg, sizeof(mount_errmsg)},
IOVEC_ENTRY("force"), IOVEC_ENTRY(NULL)};
if (fs_type == IMAGE_FS_UFS) {
iov = iov_ufs;
iovlen = (unsigned int)IOVEC_SIZE(iov_ufs) - (force_mount ? 0u : 2u);
} else if (fs_type == IMAGE_FS_EXFAT) {
iov = iov_exfat;
iovlen = (unsigned int)IOVEC_SIZE(iov_exfat) - (force_mount ? 0u : 2u);
} else if (fs_type == IMAGE_FS_PFS) {
log_debug(" [IMG][%s] PFS ro=%d budgetid=%s mkeymode=%s "
"sigverify=%s playgo=%s disc=%s ekpfs=zero",
attach_backend_name(attach_backend), mount_read_only ? 1 : 0,
PFS_MOUNT_BUDGET_ID, PFS_MOUNT_MKEYMODE, sigverify, playgo, disc);
iov = iov_pfs;
iovlen = (unsigned int)IOVEC_SIZE(iov_pfs) - (force_mount ? 0u : 2u);
} else {
log_debug(" [IMG][%s] unsupported fstype=%s",
attach_backend_name(attach_backend), image_fs_name(fs_type));
(void)detach_attached_unit(attach_backend, unit_id);
errno = EINVAL;
return false;
}
const char *mount_mode = NULL;
unsigned int mount_flags =
get_nmount_flags(fs_type, mount_read_only, &mount_mode);
if (nmount(iov, iovlen, (int)mount_flags) == 0)
return true;
int mount_errno = errno;
if (mount_errmsg[0] != '\0') {
sm_error_set("IMG", mount_errno, file_path, "%s", mount_errmsg);
log_debug(" [IMG][%s] nmount %s errmsg: %s",
attach_backend_name(attach_backend), mount_mode, mount_errmsg);
}
log_debug(" [IMG][%s] nmount %s failed: %s",
attach_backend_name(attach_backend), mount_mode,
strerror(mount_errno));
(void)detach_attached_unit(attach_backend, unit_id);
errno = mount_errno;
return false;
}
static bool validate_mounted_image(const char *file_path, image_fs_type_t fs_type,
attach_backend_t attach_backend, int unit_id,
const char *devname,
const char *mount_point) {
struct statfs mounted_sfs;
if (statfs(mount_point, &mounted_sfs) != 0) {
return reject_mounted_image_io(file_path, attach_backend, unit_id, devname,
mount_point, errno, "statfs");
}
uint32_t min_device_sector =
(attach_backend == ATTACH_BACKEND_MD) ? get_md_sector_size_for_path(file_path, fs_type)
: get_lvd_sector_size(file_path,
fs_type);
uint64_t fs_block_size = (uint64_t)mounted_sfs.f_bsize;
if (fs_block_size < (uint64_t)min_device_sector) {
uint32_t tuned_sector_size = 0;
bool autotuned =
fs_block_size <= UINT32_MAX &&
upsert_image_sector_size_autotune(get_filename_component(file_path),
(uint32_t)fs_block_size,
&tuned_sector_size);
sm_error_set("IMG", EINVAL, file_path,
"Filesystem cluster size (%llu) is smaller than device "
"sector size (%u) for %s",
(unsigned long long)fs_block_size, min_device_sector, devname);
log_debug(" [IMG][%s] %s", attach_backend_name(attach_backend),
sm_last_error()->message);
if (autotuned) {
log_debug(" [CFG] image sector autotuned: %s=%u",
get_filename_component(file_path), tuned_sector_size);
notify_system("Image mount rejected:\n%s\nCluster size is too small "
"(%llu < %u).\nSaved image_sector override: %u.\nTry "
"mounting again.",
file_path, (unsigned long long)fs_block_size,
min_device_sector, tuned_sector_size);
} else {
notify_system("Image mount rejected:\n%s\nCluster size is too small "
"(%llu < %u).\nUse a larger cluster size in the image or "
"lower sector size in config.",
file_path, (unsigned long long)fs_block_size,
min_device_sector);
}
sm_error_mark_notified();
(void)unmount_image(file_path, unit_id, attach_backend);
errno = EINVAL;
return false;
}
int root_err = 0;
if (!is_image_mount_root_accessible(mount_point, &root_err)) {
return reject_mounted_image_io(file_path, attach_backend, unit_id, devname,
mount_point, root_err, "root access");
}
return true;
}
// --- Image Attach + nmount Pipeline ---
bool mount_image(const char *file_path, image_fs_type_t fs_type) {
sm_error_clear();
const runtime_config_t *cfg = runtime_config();
bool mount_read_only = cfg->mount_read_only;
bool mount_mode_overridden = false;
bool force_mount = cfg->force_mount;
const char *filename = get_filename_component(file_path);
if (filename[0] != '\0')
mount_mode_overridden =
get_image_mode_override(filename, &mount_read_only);
if (runtime_sleep_mode_active())
return false;
char mount_point[MAX_PATH];
build_image_mount_point(file_path, mount_point);
bool handled = false;
bool success = false;
runtime_mount_state_lock();
handled =
handle_cached_or_existing_image_mount_locked(file_path, mount_point,
&success);
runtime_mount_state_unlock();
if (handled)
return success;
struct stat st;
if (!stat_image_file(file_path, &st))
return false;
if (runtime_sleep_mode_active())
return false;
runtime_mount_state_lock();
if (runtime_sleep_mode_active()) {
runtime_mount_state_unlock();
return false;
}
handled =
handle_cached_or_existing_image_mount_locked(file_path, mount_point,
&success);
if (handled) {
runtime_mount_state_unlock();
return success;
}
log_debug(" [IMG] Mounting image (%s): %s -> %s", image_fs_name(fs_type),
file_path, mount_point);
if (mount_mode_overridden) {
log_debug(" [CFG] Image mode override: %s -> %s", file_path,
mount_read_only ? "ro" : "rw");
}
ensure_mount_dirs(mount_point);
attach_backend_t attach_backend = select_image_backend(cfg, fs_type);
log_debug(" [IMG][%s] attach backend selected for %s",
attach_backend_name(attach_backend), file_path);
int unit_id = -1;
char devname[64];
memset(devname, 0, sizeof(devname));
if (!attach_image_device(file_path, fs_type, mount_read_only, st.st_size,
attach_backend, &unit_id, devname, sizeof(devname))) {
runtime_mount_state_unlock();
return false;
}
if (runtime_sleep_mode_active()) {
(void)detach_attached_unit(attach_backend, unit_id);
runtime_mount_state_unlock();
return false;
}
if (!perform_image_nmount(file_path, fs_type, attach_backend, unit_id, devname,
mount_point, mount_read_only, force_mount)) {
runtime_mount_state_unlock();
return false;
}
if (runtime_sleep_mode_active()) {
(void)unmount_image(file_path, unit_id, attach_backend);
runtime_mount_state_unlock();
return false;
}
if (!validate_mounted_image(file_path, fs_type, attach_backend, unit_id,
devname, mount_point)) {
runtime_mount_state_unlock();
return false;
}
log_debug(" [IMG][%s] Mounted (%s) %s -> %s",
attach_backend_name(attach_backend), image_fs_name(fs_type),
devname, mount_point);
log_fs_stats("IMG", mount_point, image_fs_name(fs_type));
if (!cache_image_mount(file_path, mount_point, unit_id, attach_backend)) {
sm_error_set("IMG", ENOSPC, file_path,
"Image cache full (%u entries), rolling back mount",
(unsigned)MAX_IMAGE_MOUNTS);
log_debug(" [IMG] image cache full, rolling back mount: %s -> %s",
file_path, mount_point);
(void)unmount_image(file_path, unit_id, attach_backend);
errno = ENOSPC;
runtime_mount_state_unlock();
return false;
}
runtime_mount_state_unlock();
return true;
}
bool unmount_image(const char *file_path, int unit_id, attach_backend_t backend) {
char mount_point[MAX_PATH];
build_image_mount_point(file_path, mount_point);
int resolved_unit = unit_id;
attach_backend_t resolved_backend = backend;
if (resolved_unit < 0 || resolved_backend == ATTACH_BACKEND_NONE) {
if (!resolve_device_from_mount(mount_point, &resolved_backend,
&resolved_unit)) {
resolved_backend = ATTACH_BACKEND_NONE;
resolved_unit = -1;
}
}
log_debug(" [IMG][%s] unmount start: source=%s mount=%s unit=%d",
attach_backend_name(resolved_backend), file_path, mount_point,
resolved_unit);
// Remove mount.lnk and unmount /system_ex/app/<titleid> that point to this
// source before unmounting the virtual disk itself.
cleanup_mount_links_for_source_unmount(mount_point);
clear_cached_game(mount_point);
// Unmount stacked layers (unionfs over image fs).
for (int i = 0; i < MAX_LAYERED_UNMOUNT_ATTEMPTS; i++) {
if (!is_active_image_mount_point(mount_point))
break;
if (unmount(mount_point, 0) == 0)
continue;
if (errno == ENOENT || errno == EINVAL)
break;
if (unmount(mount_point, MNT_FORCE) != 0 && errno != ENOENT &&
errno != EINVAL) {
log_debug(" [IMG][%s] unmount failed for %s: %s",
attach_backend_name(resolved_backend), mount_point,
strerror(errno));
return false;
}
}
if (is_active_image_mount_point(mount_point)) {
log_debug(" [IMG][%s] unmount incomplete for %s",
attach_backend_name(resolved_backend), mount_point);
return false;
}
bool detach_ok = true;
if (resolved_backend != ATTACH_BACKEND_NONE && resolved_unit >= 0)
detach_ok = detach_attached_unit(resolved_backend, resolved_unit);
if (rmdir(mount_point) == 0) {
log_debug(" [IMG] Removed mount directory: %s", mount_point);
log_debug(" [IMG][%s] unmount complete: source=%s mount=%s unit=%d",
attach_backend_name(resolved_backend), file_path, mount_point,
resolved_unit);
return detach_ok;
}
int err = errno;
if (err == ENOENT) {
log_debug(" [IMG][%s] unmount complete: source=%s mount=%s unit=%d",
attach_backend_name(resolved_backend), file_path, mount_point,
resolved_unit);
return detach_ok;
}
if (err == ENOTEMPTY || err == EBUSY) {
log_debug(" [IMG] Mount directory not removed (%s): %s",
strerror(err), mount_point);
if (detach_ok) {
log_debug(" [IMG][%s] unmount complete with leftover dir: source=%s "
"mount=%s unit=%d",
attach_backend_name(resolved_backend), file_path, mount_point,
resolved_unit);
}
return detach_ok;
}
log_debug(" [IMG] Failed to remove mount directory %s: %s", mount_point,
strerror(err));
return detach_ok;
}
void cleanup_stale_image_mounts(void) {
if (should_stop_requested())
return;
log_debug(" [IMG] stale image cleanup begin");
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
image_cache_entry_t cached_entry;
if (should_stop_requested())
return;
if (!get_image_cache_entry(k, &cached_entry))
continue;
log_debug(" [IMG][%s] stale cleanup check: slot=%d source=%s mount=%s",
attach_backend_name(cached_entry.backend), k, cached_entry.path,
cached_entry.mount_point);
if (!path_exists(cached_entry.path)) {
log_debug(" [IMG][%s] Source removed, unmounting: %s",
attach_backend_name(cached_entry.backend), cached_entry.path);
if (unmount_image(cached_entry.path, cached_entry.unit_id,
cached_entry.backend))
invalidate_image_cache_entry(k);
continue;
}
image_fs_type_t fs_type = detect_image_fs_type(cached_entry.path);
char mount_point[MAX_PATH];
char source_path[MAX_PATH];
if (!prepare_image_mount_retry(&cached_entry, mount_point, source_path))
continue;
invalidate_image_cache_entry(k);
if (mount_image(source_path, fs_type)) {
clear_image_mount_attempts(source_path);
continue;
}
int mount_err = errno;
if (bump_image_mount_attempts(source_path) == 1 && !sm_error_notified()) {
notify_image_mount_failed(source_path, mount_err);
}
}
log_debug(" [IMG] stale image cleanup done");
}
void cleanup_stale_image_mounts_for_root(const char *root) {
if (!root || root[0] == '\0') {
cleanup_stale_image_mounts();
return;
}
if (should_stop_requested())
return;
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
image_cache_entry_t cached_entry;
if (should_stop_requested())
return;
if (!get_image_cache_entry(k, &cached_entry))
continue;
if (!path_matches_root_or_child(cached_entry.path, root) &&
!path_matches_root_or_child(cached_entry.mount_point, root)) {
continue;
}
if (!path_exists(cached_entry.path)) {
log_debug(" [IMG][%s] Source removed, unmounting: %s",
attach_backend_name(cached_entry.backend), cached_entry.path);
if (unmount_image(cached_entry.path, cached_entry.unit_id,
cached_entry.backend))
invalidate_image_cache_entry(k);
continue;
}
image_fs_type_t fs_type = detect_image_fs_type(cached_entry.path);
char mount_point[MAX_PATH];
char source_path[MAX_PATH];
if (!prepare_image_mount_retry(&cached_entry, mount_point, source_path))
continue;
invalidate_image_cache_entry(k);
if (mount_image(source_path, fs_type)) {
clear_image_mount_attempts(source_path);
continue;
}
int mount_err = errno;
if (bump_image_mount_attempts(source_path) == 1 && !sm_error_notified()) {
notify_image_mount_failed(source_path, mount_err);
}
}
}
bool unmount_usb_image_mounts_for_suspend(void) {
bool all_unmounted = true;
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
image_cache_entry_t cached_entry;
if (!get_image_cache_entry(k, &cached_entry))
continue;
if (!is_usb_storage_path(cached_entry.path))
continue;
log_debug(" [IMG][%s] USB suspend unmount: %s",
attach_backend_name(cached_entry.backend), cached_entry.path);
if (unmount_image(cached_entry.path, cached_entry.unit_id,
cached_entry.backend)) {
log_debug(" [IMG][%s] USB suspend unmounted: %s",
attach_backend_name(cached_entry.backend), cached_entry.path);
invalidate_image_cache_entry(k);
continue;
}
all_unmounted = false;
log_debug(" [IMG][%s] USB suspend unmount pending: %s",
attach_backend_name(cached_entry.backend), cached_entry.path);
}
return all_unmounted;
}
void cleanup_mount_dirs(void) {
DIR *d = opendir(IMAGE_MOUNT_BASE);
if (!d) {
if (errno != ENOENT)
log_debug(" [IMG] open %s failed: %s", IMAGE_MOUNT_BASE, strerror(errno));
return;
}
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
if (should_stop_requested())
break;
if (entry->d_name[0] == '.')
continue;
char full_path[MAX_PATH];
snprintf(full_path, sizeof(full_path), "%s/%s", IMAGE_MOUNT_BASE,
entry->d_name);
bool is_dir = false;
if (entry->d_type == DT_DIR) {
is_dir = true;
} else if (entry->d_type == DT_UNKNOWN) {
struct stat st;
if (stat(full_path, &st) == 0)
is_dir = S_ISDIR(st.st_mode);
}
if (!is_dir)
continue;
if (rmdir(full_path) == 0) {
log_debug(" [IMG] removed empty mount dir: %s", full_path);
continue;
}
if (errno == ENOTEMPTY || errno == EBUSY || errno == ENOENT)
continue;
log_debug(" [IMG] failed to remove mount dir %s: %s", full_path,
strerror(errno));
}
closedir(d);
}
bool maybe_mount_image_file(const char *full_path, const char *display_name,
bool *unstable_out) {
image_fs_type_t fs_type = detect_image_fs_type(display_name);
if (fs_type == IMAGE_FS_UNKNOWN)
return false;
if (!is_source_stable_for_mount(full_path, display_name, "IMG")) {
if (unstable_out)
*unstable_out = true;
return false;
}
if (is_image_mount_limited(full_path))
return false;
if (mount_image(full_path, fs_type)) {
clear_image_mount_attempts(full_path);
return true;
}
int mount_err = errno;
if (bump_image_mount_attempts(full_path) == 1 && !sm_error_notified()) {
notify_image_mount_failed(full_path, mount_err);
}
return false;
}
bool shutdown_image_mounts(void) {
for (int pass = 0; pass < MAX_LAYERED_UNMOUNT_ATTEMPTS; pass++) {
bool any_remaining = false;
bool progress = false;
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
image_cache_entry_t cached_entry;
if (!get_image_cache_entry(k, &cached_entry))
continue;
if (unmount_image(cached_entry.path, cached_entry.unit_id,
cached_entry.backend)) {
invalidate_image_cache_entry(k);
progress = true;
continue;
}
any_remaining = true;
log_debug(" [IMG] shutdown unmount pending (%d/%d): %s", pass + 1,
MAX_LAYERED_UNMOUNT_ATTEMPTS, cached_entry.path);
}
if (!any_remaining)
return true;
if (!progress)
break;
}
return false;
}