mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-20 05:40:07 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24fba2be21 |
No files matched your search
@@ -1,8 +1,5 @@
|
||||
*.o
|
||||
*.elf
|
||||
*.so
|
||||
*.pyc
|
||||
__pycache__/
|
||||
daemon.lock
|
||||
debug.log
|
||||
notify.txt
|
||||
|
||||
@@ -18,8 +18,7 @@ KERNEL_SYS_STUB_SRCS := $(PS5_SCE_STUBS_DIR)/libkernel_sys.c src/libkernel_sys_e
|
||||
|
||||
ASSET_SRCS := src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
SRCS := src/main.c $(wildcard src/sm_*.c) $(ASSET_SRCS)
|
||||
ASM_SRCS := src/sm_shellcore_bridge.S
|
||||
OBJS := $(SRCS:.c=.o) $(ASM_SRCS:.S=.o)
|
||||
OBJS := $(SRCS:.c=.o)
|
||||
HEADERS := $(wildcard include/*.h)
|
||||
|
||||
# Targets
|
||||
@@ -44,8 +43,5 @@ src/config_ini_example_asset.c: config.ini.example
|
||||
src/%.o: src/%.c $(HEADERS)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
src/%.o: src/%.S
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
clean:
|
||||
rm -f shadowmountplus.elf kill.elf src/*.o $(KERNEL_SYS_STUB_SO) src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
@@ -56,11 +56,13 @@ Supported keys (all optional):
|
||||
- `quiet_mode=1|0` (`1` suppresses plain informational popups but keeps rich toasts; default is `0`)
|
||||
- `mount_read_only=1|0` (default: `1`)
|
||||
- `force_mount=1|0` (mounting even damaged file systems; default: `0`)
|
||||
- `app_install_all=1|0` (`1` stages new titles and submits them through the stock batch `sceAppInstUtilAppInstallAll`; default: `0` on every firmware. On FW `12.00+`, the default per-title path is provided by the SceShellCore TitleDir bridge.)
|
||||
- `app_install_all=1|0` (`1` stages new titles and submits them through `sceAppInstUtilAppInstallAll`; default: `0` on FW below `12.00`, forced `1` on FW `12.00+`)
|
||||
- `image_ro=<image_filename>` (repeatable; force read-only mode for this image filename)
|
||||
- `image_rw=<image_filename>` (repeatable; force read-write mode for this image filename)
|
||||
- `image_sector=<image_filename>:<sector_size>` (repeatable; force sector size for this image filename)
|
||||
- `scan_depth=<1..2>` (`1` = scan only first-level subfolders, `2` = also scan one additional nested level; default: `1`)
|
||||
- `watch_subdirs=1|0` (`1` keeps vnode watchers on scan root subdirectories; default: `0`)
|
||||
- `watch_image_files=1|0` (`1` keeps vnode watchers on discovered image files; default: `1`)
|
||||
- `recursive_scan=1|0` (deprecated compatibility key; `1` forces `scan_depth=2`)
|
||||
- `scan_interval_seconds=<1..3600>` (full scan loop interval; default: `15`)
|
||||
- `stability_wait_seconds=<0..3600>` (minimum source age before processing; default: `10`)
|
||||
@@ -124,7 +126,7 @@ Backport overlay behavior:
|
||||
- The `backports` folder is ignored during normal game scanning.
|
||||
- A backport is applied automatically to the matching mounted game from any configured scan path.
|
||||
- If multiple scan paths provide the same title backport, the game's own scan path wins; otherwise scan path order is used.
|
||||
- If `/mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib2` exists while the game is running, ShadowMount+ mounts it into that game's sandbox `common/lib`; otherwise it falls back to `app0/fakelib`.
|
||||
- If `/mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib` exists while the game is running, ShadowMount+ mounts it into that game's sandbox `common/lib`.
|
||||
- If `global_fakelib=1` and `global_fakelib_path` exists as a directory, ShadowMount+ also mounts that folder into the same sandbox `common/lib`.
|
||||
- When both global and per-game fakelib exist, the default gives the game's own `fakelib` priority by mounting `/data/shadowmount/fakelib` first and then the game-specific fakelib.
|
||||
- `global_fakelib_priority=global` reverses that priority.
|
||||
@@ -207,7 +209,7 @@ mkpfs pack folder --verify --no-compress --no-adjust-output-file-extension --ver
|
||||
'./pfs_image.dat'
|
||||
```
|
||||
|
||||
Then pack the nested image (**pfs_image.dat**) into a compressed PFS container:
|
||||
Then pack the nested image into a compressed PFS container:
|
||||
|
||||
```bash
|
||||
mkpfs pack file --verify --version PS5 --inode-bits 32 \
|
||||
|
||||
+13
-1
@@ -45,6 +45,18 @@
|
||||
# Default: 1
|
||||
# scan_depth=1
|
||||
|
||||
# Directory watcher granularity:
|
||||
# 1 -> keep vnode watchers on scan root subdirectories
|
||||
# 0 -> watch only scan roots/parents and rely on targeted/full scans for children
|
||||
# Default: 0
|
||||
# watch_subdirs=0
|
||||
|
||||
# Image file watcher granularity:
|
||||
# 1 -> keep vnode watchers on discovered image files
|
||||
# 0 -> detect image file changes through targeted/full scans
|
||||
# Default: 1
|
||||
# watch_image_files=1
|
||||
|
||||
# Legacy compatibility:
|
||||
# recursive_scan=1 forces scan_depth=2
|
||||
# Default: disabled
|
||||
@@ -67,7 +79,7 @@
|
||||
# ufs_backend=lvd
|
||||
|
||||
# Sandbox fakelib backport watcher:
|
||||
# 1/true/yes/on -> mount app0/fakelib2 when present, otherwise app0/fakelib, into common/lib
|
||||
# 1/true/yes/on -> mount /mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib into common/lib
|
||||
# 0/false/no/off -> disable fakelib watcher
|
||||
# Default: 1
|
||||
# backport_fakelib=1
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
|
||||
// Check whether a title is present in the installed app set.
|
||||
bool is_installed(const char *title_id);
|
||||
// Check whether /user/appmeta/<TITLE_ID>/param.json exists.
|
||||
@@ -15,18 +13,6 @@ bool has_appmeta_data(const char *title_id);
|
||||
bool is_data_mounted(const char *title_id);
|
||||
// Read the mount.lnk source path for a title tracked under /user/app.
|
||||
bool read_mount_link(const char *title_id, char *out, size_t out_size);
|
||||
// Atomically persist the mount.lnk source path for a title.
|
||||
bool write_mount_link(const char *title_id, const char *source_path);
|
||||
// Read the optional backing image path retained for an image-backed title.
|
||||
bool read_mount_image_link(const char *title_id, char *out, size_t out_size);
|
||||
// Persist the outer backing-image path for an already staged title.
|
||||
bool write_mount_image_link(const char *title_id, const char *image_path);
|
||||
bool read_mount_image_chain(
|
||||
const char *title_id,
|
||||
char paths[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH], size_t *count_out);
|
||||
bool write_mount_image_chain(
|
||||
const char *title_id,
|
||||
const char paths[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH], size_t count);
|
||||
// Validate a /user/app/<TITLE_ID> tracker entry and optionally build its path.
|
||||
bool resolve_title_app_dir(const struct dirent *entry, char *app_dir,
|
||||
size_t app_dir_size);
|
||||
@@ -40,10 +26,6 @@ int remount_system_ex(void);
|
||||
bool mount_title_nullfs(const char *title_id, const char *src_path);
|
||||
// Roll back a just-mounted title nullfs layer when publication is aborted.
|
||||
bool rollback_title_nullfs_mount(const char *title_id, const char *src_path);
|
||||
// Drop runtime layers for a title while preserving its metadata/link files.
|
||||
bool unmount_title_runtime_layers(const char *title_id);
|
||||
// Release every managed title layer while preserving link metadata.
|
||||
void unmount_all_title_runtime_layers(void);
|
||||
// Reconcile the title mount stack against the expected source/backport state.
|
||||
bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
|
||||
const char *expected_backport_path,
|
||||
|
||||
@@ -11,13 +11,6 @@ void log_fs_stats(const char *tag, const char *path, const char *type_hint);
|
||||
bool mount_image(const char *file_path, image_fs_type_t fs_type);
|
||||
// Unmount an image mount point and detach its backing device.
|
||||
bool unmount_image(const char *file_path, int unit_id, attach_backend_t backend);
|
||||
// Unmount a runtime image without deleting persistent title metadata links.
|
||||
bool unmount_runtime_image(const char *file_path, int unit_id,
|
||||
attach_backend_t backend);
|
||||
// Release one cached runtime image mount. An absent live mount is harmless.
|
||||
bool release_runtime_image_mount(const char *file_path);
|
||||
// Release all discovery/runtime image mounts without deleting title metadata.
|
||||
bool release_runtime_image_mounts(void);
|
||||
// Reconcile cached image mounts with current sources and remount if needed.
|
||||
void cleanup_stale_image_mounts(void);
|
||||
// Reconcile cached image mounts that belong to a specific scan root.
|
||||
|
||||
@@ -20,9 +20,6 @@ bool cache_image_mount(const char *path, const char *mount_point,
|
||||
bool cache_image_source_mapping(const char *path, const char *mount_point);
|
||||
// Return a cached image mount entry by index.
|
||||
bool get_image_cache_entry(int index, image_cache_entry_t *entry_out);
|
||||
// Find a cached source path with one cache lock acquisition.
|
||||
bool find_image_cache_entry(const char *path, image_cache_entry_t *entry_out,
|
||||
int *index_out);
|
||||
// Mark a cached image mount entry as invalid.
|
||||
void invalidate_image_cache_entry(int index);
|
||||
// Resolve a device mapping from the in-memory mount cache.
|
||||
@@ -33,16 +30,5 @@ bool resolve_device_from_mount_cache(const char *mount_point,
|
||||
bool resolve_image_source_from_mount_cache(const char *mount_point,
|
||||
char *path_out,
|
||||
size_t path_out_size);
|
||||
// Resolve the image whose mount point owns a mounted child path.
|
||||
bool resolve_owning_image_source_from_mount_cache(const char *path,
|
||||
char *path_out,
|
||||
size_t path_out_size);
|
||||
// Resolve every owning image from the outermost source to the innermost one.
|
||||
size_t resolve_image_source_chain_from_mount_cache(
|
||||
const char *path,
|
||||
char chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH]);
|
||||
// Resolve only the outermost owning image source.
|
||||
bool resolve_outermost_image_source_from_mount_cache(
|
||||
const char *path, char *path_out, size_t path_out_size);
|
||||
|
||||
#endif
|
||||
@@ -1,24 +0,0 @@
|
||||
#ifndef SM_IMAGE_INDEX_H
|
||||
#define SM_IMAGE_INDEX_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
struct AppDbTitleList;
|
||||
|
||||
// Return true when an image must be opened to discover/install its contents.
|
||||
bool sm_image_index_needs_scan(const char *path, const struct stat *st,
|
||||
const struct AppDbTitleList *app_db_titles,
|
||||
bool app_db_titles_ready);
|
||||
// Start a new fingerprint generation before mounting an image.
|
||||
void sm_image_index_begin_scan(const char *path, const struct stat *st);
|
||||
// Associate a discovered mounted game with its outer backing image.
|
||||
void sm_image_index_record_game(const char *game_path, const char *title_id);
|
||||
// Commit one fingerprint after its mounted content was scanned.
|
||||
void sm_image_index_complete_scan(const char *path);
|
||||
// Atomically persist accumulated index changes at the scan-cycle boundary.
|
||||
void sm_image_index_flush(void);
|
||||
// Drop persistent entries whose backing image disappeared.
|
||||
void sm_image_index_prune(void);
|
||||
|
||||
#endif
|
||||
@@ -24,7 +24,5 @@ bool sm_install_queue_candidate(const scan_candidate_t *candidate,
|
||||
bool sm_install_submit_queued(void);
|
||||
// Record retry state for every queued title after a batch submit failure.
|
||||
void sm_install_note_submit_failure(void);
|
||||
// Return true while an install still needs its staged mounts.
|
||||
bool sm_install_has_pending_work(void);
|
||||
|
||||
#endif
|
||||
@@ -22,13 +22,11 @@
|
||||
#define MAX_MISSING_PARAM_SCAN_ATTEMPTS 3
|
||||
#define MAX_IMAGE_MOUNT_ATTEMPTS 3
|
||||
#define MAX_LAYERED_UNMOUNT_ATTEMPTS 4
|
||||
#define MAX_IMAGE_CHAIN_DEPTH 8
|
||||
|
||||
#define LVD_RELEASE_WAIT_MAX_US 15000000u
|
||||
#define LVD_RELEASE_WAIT_POLL_US 500000u
|
||||
#define GAME_LIFECYCLE_POLL_INTERVAL_US 250000u
|
||||
#define GAME_APPINFO_LOOKUP_TIMEOUT_US 1000000u
|
||||
#define GAME_SLEEP_EXIT_TIMEOUT_US 5000000u
|
||||
#define KSTUFF_RESTORE_RETRY_US 1000000u
|
||||
#define SHELLCORE_FLAG_POLL_INTERVAL_US 100000u
|
||||
#define MIN_SCAN_DEPTH 1u
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#define AUTOTUNE_FILE "/data/shadowmount/autotune.ini"
|
||||
#define MANUAL_LIST_FILE "/data/shadowmount/manual.lst"
|
||||
#define MANUAL_STATUS_FILE "/data/shadowmount/manual.status"
|
||||
#define IMAGE_INDEX_FILE_PATH "/data/shadowmount/image_index.bin"
|
||||
#define APPMETA_BASE "/user/appmeta"
|
||||
#define APP_BASE "/user/app"
|
||||
#define KSTUFF_NOAUTOMOUNT_FILE "/data/.kstuff_noautomount"
|
||||
|
||||
@@ -41,16 +41,9 @@ static inline uint32_t sm_firmware_major_version(void) {
|
||||
return ((major_bcd >> 4) & 0xFu) * 10u + (major_bcd & 0xFu);
|
||||
}
|
||||
|
||||
static inline bool sm_set_fd_nonblocking(int fd) {
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0)
|
||||
return false;
|
||||
return fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
|
||||
}
|
||||
|
||||
// --- SDK Imports ---
|
||||
int sceAppInstUtilInitialize(void);
|
||||
int sceAppInstUtilAppInstallAll(void *reserved);
|
||||
int sceAppInstUtilAppInstallAll(void);
|
||||
int sceAppInstUtilAppUnInstall(const char *title_id);
|
||||
int sceKernelGetAppInfo(pid_t pid, app_info_t *info);
|
||||
int sceKernelUsleep(unsigned int microseconds);
|
||||
|
||||
+2
-7
@@ -21,12 +21,7 @@ int collect_scan_candidates_for_scan_root(const char *scan_root,
|
||||
int max_candidates,
|
||||
int *total_found_out,
|
||||
bool *unstable_found_out);
|
||||
// Mount the stable backport overlay, when present, for one runtime title.
|
||||
bool mount_backport_overlay_for_title(const char *source_path,
|
||||
const char *title_id,
|
||||
const char *owning_scan_root,
|
||||
bool *unstable_found_out);
|
||||
// Remove discovery title/image mounts while preserving persistent metadata.
|
||||
bool release_scan_runtime_mounts(void);
|
||||
// Mount stable backport overlays for already mounted titles.
|
||||
void mount_backport_overlays(bool *unstable_found_out);
|
||||
|
||||
#endif
|
||||
@@ -1,19 +0,0 @@
|
||||
#ifndef SM_SHELLCORE_HOOKS_H
|
||||
#define SM_SHELLCORE_HOOKS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
// Install/remove the fail-open SceShellCore lifecycle bridge.
|
||||
bool sm_shellcore_hooks_start(void);
|
||||
void sm_shellcore_hooks_stop(void);
|
||||
|
||||
// Return whether this firmware uses the internal TitleDir bridge.
|
||||
bool sm_shellcore_install_bridge_enabled(void);
|
||||
|
||||
// Invoke the internal TitleDir path through one armed AppInstallAll dispatch.
|
||||
// Returns false when the bridge is unavailable or was not consumed.
|
||||
bool sm_shellcore_install_title_dir(const char *title_id,
|
||||
const char *install_dir,
|
||||
int *result_out);
|
||||
|
||||
#endif
|
||||
@@ -1,29 +0,0 @@
|
||||
#ifndef SM_SHELLCORE_OFFSETS_H
|
||||
#define SM_SHELLCORE_OFFSETS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum {
|
||||
SM_SHELLCORE_TARGET_LAUNCH_APP = 0,
|
||||
SM_SHELLCORE_TARGET_UNMOUNT_WORKSPACE,
|
||||
SM_SHELLCORE_TARGET_INSTALL_TITLE_DIR,
|
||||
SM_SHELLCORE_TARGET_INSTALL_ALL,
|
||||
SM_SHELLCORE_TARGET_COUNT
|
||||
} sm_shellcore_target_t;
|
||||
|
||||
typedef struct {
|
||||
uintptr_t offset;
|
||||
uint8_t patch_size;
|
||||
} sm_shellcore_target_offset_t;
|
||||
|
||||
typedef struct {
|
||||
uint16_t firmware;
|
||||
const char *name;
|
||||
sm_shellcore_target_offset_t targets[SM_SHELLCORE_TARGET_COUNT];
|
||||
} sm_shellcore_firmware_offsets_t;
|
||||
|
||||
const sm_shellcore_firmware_offsets_t *
|
||||
sm_shellcore_offsets_for_firmware(uint32_t raw_firmware);
|
||||
const char *sm_shellcore_target_name(sm_shellcore_target_t target);
|
||||
|
||||
#endif
|
||||
@@ -1,28 +0,0 @@
|
||||
#ifndef SM_SHELLCORE_PROTOCOL_H
|
||||
#define SM_SHELLCORE_PROTOCOL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
|
||||
#define SM_SHELLCORE_SOCKET_PATH "/system_tmp/shadowmount.sock"
|
||||
#define SM_SHELLCORE_PROTOCOL_MAGIC 0x534d5343u
|
||||
#define SM_SHELLCORE_PROTOCOL_VERSION 1u
|
||||
|
||||
typedef enum {
|
||||
SM_SHELLCORE_REQUEST_LAUNCH = 1,
|
||||
SM_SHELLCORE_REQUEST_LAUNCH_FAILED = 2
|
||||
} sm_shellcore_request_op_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
uint16_t version;
|
||||
uint16_t operation;
|
||||
char title_id[MAX_TITLE_ID];
|
||||
} sm_shellcore_request_t;
|
||||
|
||||
typedef struct {
|
||||
int32_t status;
|
||||
} sm_shellcore_response_t;
|
||||
|
||||
#endif
|
||||
@@ -1,26 +0,0 @@
|
||||
#ifndef SM_SHELLCORE_REMOTE_H
|
||||
#define SM_SHELLCORE_REMOTE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "sm_shellcore_offsets.h"
|
||||
|
||||
typedef struct {
|
||||
pid_t pid;
|
||||
uintptr_t image_base;
|
||||
uintptr_t targets[SM_SHELLCORE_TARGET_COUNT];
|
||||
const sm_shellcore_firmware_offsets_t *offsets;
|
||||
} sm_shellcore_remote_t;
|
||||
|
||||
// Resolve and validate every lifecycle target in a running SceShellCore.
|
||||
bool sm_shellcore_remote_resolve(pid_t pid, sm_shellcore_remote_t *remote_out);
|
||||
// Read or write another process while handling post-8.20 write restrictions.
|
||||
bool sm_remote_process_read(pid_t pid, uintptr_t address, void *buffer,
|
||||
size_t size);
|
||||
bool sm_remote_process_write(pid_t pid, uintptr_t address, const void *buffer,
|
||||
size_t size);
|
||||
|
||||
#endif
|
||||
@@ -1,19 +0,0 @@
|
||||
#ifndef SM_SHELLCORE_SERVICE_H
|
||||
#define SM_SHELLCORE_SERVICE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
// Start/stop the local Unix-socket owner used by the SceShellCore bridge.
|
||||
bool sm_shellcore_service_start(void);
|
||||
void sm_shellcore_service_stop(void);
|
||||
|
||||
// Release transient title/image mounts after the lifecycle watcher confirms
|
||||
// that the game process exited.
|
||||
bool sm_shellcore_release_title_runtime(const char *title_id);
|
||||
// Ensure that a managed title has its image/nullfs/backport runtime stack.
|
||||
// Unmanaged stock titles are treated as already ready.
|
||||
bool sm_shellcore_ensure_title_runtime(const char *title_id);
|
||||
// Return true while ShellCore owns a title mount prepared for launch or exit.
|
||||
bool sm_shellcore_service_has_prepared_mount(void);
|
||||
|
||||
#endif
|
||||
@@ -109,12 +109,15 @@ typedef struct runtime_config {
|
||||
bool mount_read_only;
|
||||
bool force_mount;
|
||||
bool app_install_all_enabled;
|
||||
bool app_install_all_forced;
|
||||
bool backport_fakelib_enabled;
|
||||
bool global_fakelib_enabled;
|
||||
bool global_fakelib_mount_first;
|
||||
bool kstuff_game_auto_toggle;
|
||||
bool kstuff_crash_detection_enabled;
|
||||
bool legacy_recursive_scan_forced;
|
||||
bool watch_subdirs;
|
||||
bool watch_image_files;
|
||||
char global_fakelib_path[MAX_PATH];
|
||||
uint32_t global_fakelib_exclude_title_count;
|
||||
char global_fakelib_exclude_title_ids[MAX_FAKELIB_EXCLUDE_RULES][MAX_TITLE_ID];
|
||||
|
||||
-11
@@ -8,8 +8,6 @@
|
||||
#include "sm_types.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_shellcore_flags.h"
|
||||
#include "sm_shellcore_hooks.h"
|
||||
#include "sm_shellcore_service.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_kstuff.h"
|
||||
@@ -475,13 +473,6 @@ int main(void) {
|
||||
log_debug(" [SHELLFLAG] monitor unavailable");
|
||||
sm_mdbg_init();
|
||||
sm_kstuff_init();
|
||||
if (!sm_shellcore_service_start())
|
||||
log_debug(" [SHELLCORE] Unix socket service unavailable: %s",
|
||||
strerror(errno));
|
||||
else {
|
||||
if (!sm_shellcore_hooks_start())
|
||||
log_debug(" [SHELLCORE] lifecycle hooks unavailable; stock behavior kept");
|
||||
}
|
||||
if (!refresh_game_lifecycle_watcher())
|
||||
log_debug(" [GAME] lifecycle watcher unavailable");
|
||||
|
||||
@@ -523,8 +514,6 @@ int main(void) {
|
||||
sm_scanner_run_loop();
|
||||
|
||||
shutdown:
|
||||
sm_shellcore_hooks_stop();
|
||||
sm_shellcore_service_stop();
|
||||
sm_shellcore_flags_stop();
|
||||
stop_game_lifecycle_watcher();
|
||||
sm_scanner_shutdown();
|
||||
|
||||
+43
-2
@@ -98,6 +98,7 @@ static bool lookup_kstuff_delay_override_in_file(const char *path,
|
||||
static bool upsert_kstuff_delay_override_in_file(const char *path,
|
||||
const char *title_id,
|
||||
uint32_t delay_seconds);
|
||||
static void apply_firmware_runtime_overrides(runtime_config_state_t *state);
|
||||
|
||||
static char *trim_ascii(char *s) {
|
||||
while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n')
|
||||
@@ -151,6 +152,19 @@ static bool parse_ini_line(char *line, char **key_out, char **value_out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static void apply_firmware_runtime_overrides(runtime_config_state_t *state) {
|
||||
if (!state)
|
||||
return;
|
||||
|
||||
if (sm_firmware_major_version() >= 12u) {
|
||||
state->cfg.app_install_all_enabled = true;
|
||||
state->cfg.app_install_all_forced = true;
|
||||
return;
|
||||
}
|
||||
|
||||
state->cfg.app_install_all_forced = false;
|
||||
}
|
||||
|
||||
static const runtime_config_state_t *active_runtime_state(void) {
|
||||
return &g_runtime_state_slots[atomic_load_explicit(&g_runtime_state_active_index,
|
||||
memory_order_acquire)];
|
||||
@@ -236,12 +250,15 @@ static void init_runtime_config_defaults(runtime_config_state_t *state) {
|
||||
state->cfg.mount_read_only = (IMAGE_MOUNT_READ_ONLY != 0);
|
||||
state->cfg.force_mount = false;
|
||||
state->cfg.app_install_all_enabled = false;
|
||||
state->cfg.app_install_all_forced = false;
|
||||
state->cfg.backport_fakelib_enabled = true;
|
||||
state->cfg.global_fakelib_enabled = true;
|
||||
state->cfg.global_fakelib_mount_first = true;
|
||||
state->cfg.kstuff_game_auto_toggle = true;
|
||||
state->cfg.kstuff_crash_detection_enabled = true;
|
||||
state->cfg.legacy_recursive_scan_forced = false;
|
||||
state->cfg.watch_subdirs = false;
|
||||
state->cfg.watch_image_files = true;
|
||||
(void)strlcpy(state->cfg.global_fakelib_path, DEFAULT_GLOBAL_FAKELIB_PATH,
|
||||
sizeof(state->cfg.global_fakelib_path));
|
||||
state->cfg.scan_depth = DEFAULT_SCAN_DEPTH;
|
||||
@@ -261,6 +278,7 @@ static void init_runtime_config_defaults(runtime_config_state_t *state) {
|
||||
memset(state->image_mode_rules, 0, sizeof(state->image_mode_rules));
|
||||
clear_kstuff_title_rules(state);
|
||||
init_runtime_scan_paths_defaults(state);
|
||||
apply_firmware_runtime_overrides(state);
|
||||
}
|
||||
|
||||
static config_file_stamp_t read_config_file_stamp(void) {
|
||||
@@ -1180,6 +1198,7 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
continue;
|
||||
}
|
||||
state->cfg.app_install_all_enabled = bval;
|
||||
state->cfg.app_install_all_forced = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1224,6 +1243,24 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "watch_subdirs") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.watch_subdirs = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "watch_image_files") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.watch_image_files = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "backport_fakelib") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
@@ -1452,6 +1489,7 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
log_debug(" [CFG] recursive_scan=1 is deprecated; forcing scan_depth=2");
|
||||
}
|
||||
|
||||
apply_firmware_runtime_overrides(state);
|
||||
|
||||
int image_rule_count = 0;
|
||||
for (int k = 0; k < MAX_IMAGE_MODE_RULES; k++) {
|
||||
@@ -1466,10 +1504,11 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
}
|
||||
|
||||
log_debug(" [CFG] loaded: debug=%d quiet=%d ro=%d force=%d "
|
||||
"app_install_all=%d scan_depth=%u "
|
||||
"app_install_all=%d app_install_all_forced=%d scan_depth=%u "
|
||||
"legacy_recursive_scan_forced=%d backport_fakelib=%d "
|
||||
"global_fakelib=%d global_fakelib_priority=%s "
|
||||
"global_fakelib_path=%s global_fakelib_exclude=%u "
|
||||
"watch_subdirs=%d watch_image_files=%d "
|
||||
"kstuff_game_auto_toggle=%d kstuff_crash_detection=%d "
|
||||
"kstuff_pause_delay_image_s=%u kstuff_pause_delay_direct_s=%u "
|
||||
"exfat_backend=%s ufs_backend=%s "
|
||||
@@ -1480,13 +1519,15 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
state->cfg.mount_read_only ? 1 : 0,
|
||||
state->cfg.force_mount ? 1 : 0,
|
||||
state->cfg.app_install_all_enabled ? 1 : 0,
|
||||
state->cfg.scan_depth,
|
||||
state->cfg.app_install_all_forced ? 1 : 0, state->cfg.scan_depth,
|
||||
state->cfg.legacy_recursive_scan_forced ? 1 : 0,
|
||||
state->cfg.backport_fakelib_enabled ? 1 : 0,
|
||||
state->cfg.global_fakelib_enabled ? 1 : 0,
|
||||
state->cfg.global_fakelib_mount_first ? "game" : "global",
|
||||
state->cfg.global_fakelib_path,
|
||||
state->cfg.global_fakelib_exclude_title_count,
|
||||
state->cfg.watch_subdirs ? 1 : 0,
|
||||
state->cfg.watch_image_files ? 1 : 0,
|
||||
state->cfg.kstuff_game_auto_toggle ? 1 : 0,
|
||||
state->cfg.kstuff_crash_detection_enabled ? 1 : 0,
|
||||
state->cfg.kstuff_pause_delay_image_seconds,
|
||||
|
||||
+2
-8
@@ -125,17 +125,11 @@ static bool resolve_sandbox_context(const char *title_id,
|
||||
return false;
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
snprintf(source_path, sizeof(source_path), "/mnt/sandbox/%s/app0/fakelib2",
|
||||
snprintf(source_path, sizeof(source_path), "/mnt/sandbox/%s/app0/fakelib",
|
||||
sandbox_id);
|
||||
struct stat st;
|
||||
if (stat(source_path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||
if (stat(source_path, &st) == 0 && S_ISDIR(st.st_mode))
|
||||
(void)strlcpy(game_source_path, source_path, MAX_PATH);
|
||||
} else {
|
||||
snprintf(source_path, sizeof(source_path), "/mnt/sandbox/%s/app0/fakelib",
|
||||
sandbox_id);
|
||||
if (stat(source_path, &st) == 0 && S_ISDIR(st.st_mode))
|
||||
(void)strlcpy(game_source_path, source_path, MAX_PATH);
|
||||
}
|
||||
|
||||
char sandbox_root[MAX_PATH];
|
||||
snprintf(sandbox_root, sizeof(sandbox_root), "/mnt/sandbox/%s", sandbox_id);
|
||||
|
||||
+11
-188
@@ -9,8 +9,6 @@
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
static bool unmount_controlled_mount_stack(const char *path);
|
||||
|
||||
// --- FILESYSTEM ---
|
||||
bool is_installed(const char *title_id) {
|
||||
char path[MAX_PATH];
|
||||
@@ -126,131 +124,6 @@ bool read_mount_link(const char *title_id, char *out, size_t out_size) {
|
||||
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)
|
||||
@@ -292,13 +165,8 @@ static bool source_path_needs_cleanup(const char *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 (has_image_source && !path_exists(image_source_path))
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!path_exists(source_path))
|
||||
@@ -633,43 +501,6 @@ bool rollback_title_nullfs_mount(const char *title_id, const char *src_path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool unmount_title_runtime_layers(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
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(path))
|
||||
return false;
|
||||
log_debug(" [LINK] runtime layers released: %s", title_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool unmount_managed_title_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) {
|
||||
return true;
|
||||
}
|
||||
(void)unmount_title_runtime_layers(title_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
void unmount_all_title_runtime_layers(void) {
|
||||
for_each_title_app_dir(" during runtime mount cleanup", false,
|
||||
unmount_managed_title_entry, NULL);
|
||||
}
|
||||
|
||||
bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
|
||||
const char *expected_backport_path,
|
||||
bool *overlay_active_out) {
|
||||
@@ -1140,7 +971,6 @@ typedef struct {
|
||||
bool tried_image_recovery;
|
||||
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,
|
||||
@@ -1212,19 +1042,17 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
stack_matches_link = state.has_nullfs_from_root;
|
||||
}
|
||||
|
||||
if (stack_matches_link && !top_is_ours) {
|
||||
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 (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;
|
||||
} 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",
|
||||
@@ -1238,9 +1066,6 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -1259,7 +1084,8 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
}
|
||||
|
||||
if (mount_link_staged && rename(paths->staged_mount_link, paths->mount_link) == 0) {
|
||||
log_debug(" [LINK] restored staged mount link: %s", paths->mount_link);
|
||||
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));
|
||||
@@ -1275,7 +1101,6 @@ void cleanup_mount_links(const char *removed_source_root,
|
||||
.tried_image_recovery = false,
|
||||
.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);
|
||||
@@ -1291,7 +1116,6 @@ void cleanup_mount_links_for_source_unmount(const char *source_root) {
|
||||
.tried_image_recovery = false,
|
||||
.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);
|
||||
@@ -1304,7 +1128,6 @@ void cleanup_usb_mount_links_for_suspend(void) {
|
||||
.tried_image_recovery = false,
|
||||
.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);
|
||||
|
||||
+2
-14
@@ -18,18 +18,6 @@ struct GameCache {
|
||||
|
||||
static struct GameCache g_game_cache[MAX_PENDING];
|
||||
|
||||
static bool game_cache_source_exists(const struct GameCache *entry) {
|
||||
if (path_exists(entry->path))
|
||||
return true;
|
||||
if (!is_under_image_mount_base(entry->path))
|
||||
return false;
|
||||
|
||||
char image_path[MAX_PATH];
|
||||
return read_mount_image_link(entry->title_id, image_path,
|
||||
sizeof(image_path)) &&
|
||||
path_exists(image_path);
|
||||
}
|
||||
|
||||
static bool resolve_game_cache_owning_scan_root(const char *path,
|
||||
char owning_scan_root[MAX_PATH]) {
|
||||
char resolved_source_path[MAX_PATH];
|
||||
@@ -126,7 +114,7 @@ void prune_game_cache(void) {
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
if (game_cache_source_exists(&g_game_cache[k]))
|
||||
if (path_exists(g_game_cache[k].path))
|
||||
continue;
|
||||
clear_game_cache_slot(k, "source removed");
|
||||
}
|
||||
@@ -147,7 +135,7 @@ void prune_game_cache_for_root(const char *root) {
|
||||
: g_game_cache[k].path;
|
||||
if (!path_matches_root_or_child(entry_root, root))
|
||||
continue;
|
||||
if (game_cache_source_exists(&g_game_cache[k]))
|
||||
if (path_exists(g_game_cache[k].path))
|
||||
continue;
|
||||
clear_game_cache_slot(k, "source removed");
|
||||
}
|
||||
|
||||
+42
-128
@@ -1,5 +1,6 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <sys/event.h>
|
||||
@@ -7,17 +8,13 @@
|
||||
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_fakelib.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_kstuff.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mdbg.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_scan.h"
|
||||
#include "sm_scanner.h"
|
||||
#include "sm_shellcore_service.h"
|
||||
#include "sm_time.h"
|
||||
|
||||
#define MAX_PENDING_GAME_EXEC_CANDIDATES 32
|
||||
@@ -42,12 +39,10 @@ static pthread_cond_t g_game_lifecycle_start_cond = PTHREAD_COND_INITIALIZER;
|
||||
static bool g_game_lifecycle_start_ready = false;
|
||||
static bool g_game_lifecycle_start_success = false;
|
||||
static _Atomic pid_t g_active_game_pid = 0;
|
||||
// Title storage is owned exclusively by the lifecycle thread. Other threads
|
||||
// consume only the atomically published pid through the public state helper.
|
||||
static pthread_mutex_t g_active_game_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static char g_active_game_title_id[MAX_TITLE_ID];
|
||||
|
||||
static bool register_game_exit_watch(int kq, pid_t pid);
|
||||
static void handle_game_exit(pid_t pid);
|
||||
|
||||
static void publish_active_game_pid(pid_t pid) {
|
||||
pid_t previous_pid = atomic_exchange(&g_active_game_pid, pid);
|
||||
@@ -57,40 +52,29 @@ static void publish_active_game_pid(pid_t pid) {
|
||||
}
|
||||
|
||||
static void publish_active_game(pid_t pid, const char *title_id) {
|
||||
pthread_mutex_lock(&g_active_game_mutex);
|
||||
if (pid > 0 && title_id && title_id[0] != '\0')
|
||||
(void)strlcpy(g_active_game_title_id, title_id,
|
||||
sizeof(g_active_game_title_id));
|
||||
else
|
||||
g_active_game_title_id[0] = '\0';
|
||||
pthread_mutex_unlock(&g_active_game_mutex);
|
||||
publish_active_game_pid(pid);
|
||||
}
|
||||
|
||||
static bool consume_active_game_title(pid_t pid,
|
||||
char title_id_out[MAX_TITLE_ID]) {
|
||||
if (pid <= 0 || atomic_load(&g_active_game_pid) != pid ||
|
||||
g_active_game_title_id[0] == '\0')
|
||||
return false;
|
||||
(void)strlcpy(title_id_out, g_active_game_title_id, MAX_TITLE_ID);
|
||||
g_active_game_title_id[0] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool snapshot_active_game(pid_t pid, char title_id[MAX_TITLE_ID]) {
|
||||
title_id[0] = '\0';
|
||||
if (pid <= 0 || g_active_game_title_id[0] == '\0')
|
||||
return false;
|
||||
(void)strlcpy(title_id, g_active_game_title_id, MAX_TITLE_ID);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool title_is_usb_backed(const char *title_id, const char *source_path,
|
||||
bool has_mount_link) {
|
||||
char image_path[MAX_PATH];
|
||||
if (read_mount_image_link(title_id, image_path, sizeof(image_path)) &&
|
||||
is_usb_storage_path(image_path)) {
|
||||
return true;
|
||||
bool matched = false;
|
||||
pthread_mutex_lock(&g_active_game_mutex);
|
||||
if (pid > 0 && atomic_load(&g_active_game_pid) == pid &&
|
||||
g_active_game_title_id[0] != '\0') {
|
||||
(void)strlcpy(title_id_out, g_active_game_title_id, MAX_TITLE_ID);
|
||||
matched = true;
|
||||
}
|
||||
return has_mount_link && is_usb_storage_path(source_path);
|
||||
if (matched)
|
||||
g_active_game_title_id[0] = '\0';
|
||||
pthread_mutex_unlock(&g_active_game_mutex);
|
||||
return matched;
|
||||
}
|
||||
|
||||
static bool is_supported_title_id(const char *title_id) {
|
||||
@@ -182,11 +166,6 @@ static bool is_process_alive(pid_t pid) {
|
||||
|
||||
static bool dispatch_game_launch(int kq, pid_t pid, uint64_t exec_time_us,
|
||||
const char *title_id, uint32_t app_id) {
|
||||
if (!is_data_mounted(title_id) &&
|
||||
!sm_shellcore_ensure_title_runtime(title_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!register_game_exit_watch(kq, pid)) {
|
||||
log_debug(" [GAME] skipping launch tracking for %s pid=%ld without exit watch",
|
||||
title_id, (long)pid);
|
||||
@@ -226,7 +205,7 @@ static void drain_game_lifecycle_wake_pipe(void) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool drain_game_lifecycle_events_nowait(int kq) {
|
||||
static bool discard_game_lifecycle_events_nowait(int kq) {
|
||||
struct kevent events[16];
|
||||
struct timespec timeout;
|
||||
memset(&timeout, 0, sizeof(timeout));
|
||||
@@ -242,15 +221,17 @@ static bool drain_game_lifecycle_events_nowait(int kq) {
|
||||
}
|
||||
if (nev == 0)
|
||||
return true;
|
||||
for (int i = 0; i < nev; ++i) {
|
||||
if (events[i].filter == EVFILT_PROC &&
|
||||
(events[i].fflags & NOTE_EXIT) != 0) {
|
||||
handle_game_exit((pid_t)events[i].ident);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool set_fd_nonblocking(int fd) {
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0)
|
||||
return false;
|
||||
|
||||
return fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
|
||||
}
|
||||
|
||||
static void set_game_lifecycle_start_result(bool success) {
|
||||
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
|
||||
g_game_lifecycle_start_success = success;
|
||||
@@ -445,13 +426,11 @@ static void handle_game_exit(pid_t pid) {
|
||||
pending_game_launch_t *entry = find_pending_game_launch(pid);
|
||||
if (entry)
|
||||
clear_pending_game_launch(entry);
|
||||
if (had_active_title)
|
||||
if (atomic_load(&g_active_game_pid) == pid)
|
||||
publish_active_game_pid(0);
|
||||
sm_fakelib_game_on_exit(pid);
|
||||
sm_kstuff_game_on_exit(pid);
|
||||
if (had_active_title) {
|
||||
if (!sm_shellcore_release_title_runtime(title_id))
|
||||
log_debug(" [SHELLCORE] runtime release deferred/failed: %s", title_id);
|
||||
int snd0_updates = normalize_snd0info_for_title(title_id);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info normalized after game exit rows=%d title=%s",
|
||||
@@ -459,57 +438,21 @@ static void handle_game_exit(pid_t pid) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool terminate_usb_game_for_sleep(int kq, pid_t pid,
|
||||
const char *title_id) {
|
||||
if (kill(pid, SIGKILL) != 0 && errno != ESRCH) {
|
||||
log_debug(" [SLEEP] failed to kill USB game: %s pid=%ld: %s", title_id,
|
||||
(long)pid, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
log_debug(" [SLEEP] killing USB game before suspend: %s pid=%ld", title_id,
|
||||
(long)pid);
|
||||
static void restore_suspended_game_if_alive(int kq, pid_t pid) {
|
||||
if (pid <= 0 || !is_process_alive(pid))
|
||||
return;
|
||||
|
||||
char title_id[MAX_TITLE_ID];
|
||||
uint32_t app_id = 0;
|
||||
if (!resolve_game_title_id(pid, title_id, &app_id))
|
||||
return;
|
||||
if (!is_supported_title_id(title_id))
|
||||
return;
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
uint64_t deadline_us =
|
||||
now_us == 0 ? 0 : now_us + GAME_SLEEP_EXIT_TIMEOUT_US;
|
||||
while (!g_game_lifecycle_stop_requested && !should_stop_requested()) {
|
||||
struct kevent event;
|
||||
struct timespec timeout = {
|
||||
.tv_sec = 0,
|
||||
.tv_nsec = (long)GAME_LIFECYCLE_POLL_INTERVAL_US * 1000L,
|
||||
};
|
||||
int nev = kevent(kq, NULL, 0, &event, 1, &timeout);
|
||||
if (nev < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
log_debug(" [SLEEP] NOTE_EXIT wait failed for %s pid=%ld: %s",
|
||||
title_id, (long)pid, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (nev > 0) {
|
||||
if (event.filter == EVFILT_READ &&
|
||||
event.ident == (uintptr_t)g_game_lifecycle_wake_pipe[0]) {
|
||||
drain_game_lifecycle_wake_pipe();
|
||||
} else if (event.filter == EVFILT_PROC &&
|
||||
(event.fflags & NOTE_EXIT) != 0) {
|
||||
pid_t exited_pid = (pid_t)event.ident;
|
||||
handle_game_exit(exited_pid);
|
||||
if (exited_pid == pid) {
|
||||
log_debug(" [SLEEP] USB game stopped: %s pid=%ld", title_id,
|
||||
(long)pid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
now_us = monotonic_time_us();
|
||||
if (deadline_us != 0 && now_us != 0 && now_us >= deadline_us) {
|
||||
log_debug(" [SLEEP] timed out waiting NOTE_EXIT: %s pid=%ld", title_id,
|
||||
(long)pid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
log_debug(" [GAME] resumed: %s pid=%ld app_id=0x%08X", title_id,
|
||||
(long)pid, app_id);
|
||||
(void)dispatch_game_launch(kq, pid, now_us, title_id, app_id);
|
||||
}
|
||||
|
||||
static void *game_lifecycle_watcher_main(void *arg) {
|
||||
@@ -558,36 +501,10 @@ static void *game_lifecycle_watcher_main(void *arg) {
|
||||
if (runtime_sleep_mode_active()) {
|
||||
if (!sleep_cleanup_done) {
|
||||
suspended_game_pid = atomic_load(&g_active_game_pid);
|
||||
char usb_game_title_id[MAX_TITLE_ID];
|
||||
bool active_game = snapshot_active_game(suspended_game_pid,
|
||||
usb_game_title_id);
|
||||
char source_path[MAX_PATH];
|
||||
bool has_mount_link =
|
||||
active_game && read_mount_link(usb_game_title_id, source_path,
|
||||
sizeof(source_path));
|
||||
bool usb_game =
|
||||
active_game && title_is_usb_backed(
|
||||
usb_game_title_id, source_path, has_mount_link);
|
||||
clear_all_pending_game_launches();
|
||||
sm_fakelib_game_shutdown();
|
||||
sm_kstuff_sleep_enter();
|
||||
bool usb_cleanup_allowed = true;
|
||||
if (usb_game) {
|
||||
usb_cleanup_allowed = terminate_usb_game_for_sleep(
|
||||
kq, suspended_game_pid, usb_game_title_id);
|
||||
if (usb_cleanup_allowed) {
|
||||
suspended_game_pid = 0;
|
||||
}
|
||||
}
|
||||
if (usb_cleanup_allowed) {
|
||||
runtime_mount_state_lock();
|
||||
unmount_usb_sources_for_suspend();
|
||||
runtime_mount_state_unlock();
|
||||
} else {
|
||||
log_debug("[SLEEP] USB cleanup skipped while game process remains");
|
||||
}
|
||||
// Keep a live non-USB game published while it is suspended so scanner
|
||||
// work cannot race its unchanged runtime mount.
|
||||
publish_active_game(0, NULL);
|
||||
sleep_cleanup_done = true;
|
||||
}
|
||||
|
||||
@@ -606,13 +523,10 @@ static void *game_lifecycle_watcher_main(void *arg) {
|
||||
continue;
|
||||
}
|
||||
if (sleep_cleanup_done) {
|
||||
if (!drain_game_lifecycle_events_nowait(kq))
|
||||
if (!discard_game_lifecycle_events_nowait(kq))
|
||||
break;
|
||||
sleep_cleanup_done = false;
|
||||
if (runtime_sleep_mode_active())
|
||||
continue;
|
||||
if (suspended_game_pid > 0 && is_process_alive(suspended_game_pid))
|
||||
handle_game_exec(kq, suspended_game_pid);
|
||||
restore_suspended_game_if_alive(kq, suspended_game_pid);
|
||||
sm_kstuff_sleep_leave();
|
||||
suspended_game_pid = 0;
|
||||
}
|
||||
@@ -664,8 +578,8 @@ bool start_game_lifecycle_watcher(void) {
|
||||
log_debug(" [GAME] wake pipe creation failed: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (!sm_set_fd_nonblocking(g_game_lifecycle_wake_pipe[0]) ||
|
||||
!sm_set_fd_nonblocking(g_game_lifecycle_wake_pipe[1])) {
|
||||
if (!set_fd_nonblocking(g_game_lifecycle_wake_pipe[0]) ||
|
||||
!set_fd_nonblocking(g_game_lifecycle_wake_pipe[1])) {
|
||||
log_debug(" [GAME] wake pipe nonblocking setup failed: %s",
|
||||
strerror(errno));
|
||||
close_game_lifecycle_wake_pipe();
|
||||
|
||||
+31
-147
@@ -515,20 +515,15 @@ static const image_backend_ops_t *get_image_backend_ops(attach_backend_t backend
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool get_cached_image_mount(const char *file_path,
|
||||
image_cache_entry_t *entry_out,
|
||||
int *index_out) {
|
||||
image_cache_entry_t entry;
|
||||
int index = -1;
|
||||
if (!find_image_cache_entry(file_path, &entry, &index) ||
|
||||
entry.backend == ATTACH_BACKEND_NONE || entry.unit_id < 0) {
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (entry_out)
|
||||
*entry_out = entry;
|
||||
if (index_out)
|
||||
*index_out = index;
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool directory_has_visible_entries(const char *path) {
|
||||
@@ -572,7 +567,7 @@ static bool reject_mounted_image_io(const char *file_path,
|
||||
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_runtime_image(file_path, unit_id, attach_backend);
|
||||
(void)unmount_image(file_path, unit_id, attach_backend);
|
||||
errno = io_err;
|
||||
return false;
|
||||
}
|
||||
@@ -591,8 +586,7 @@ static bool prepare_image_mount_retry(const image_cache_entry_t *cached_entry,
|
||||
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_runtime_image(source_path, cached_entry->unit_id,
|
||||
cached_entry->backend))
|
||||
if (!unmount_image(source_path, cached_entry->unit_id, cached_entry->backend))
|
||||
return false;
|
||||
} else {
|
||||
log_debug(" [IMG][%s] mount lost, retrying: %s -> %s",
|
||||
@@ -623,8 +617,7 @@ static bool reuse_existing_image_mount(const char *file_path,
|
||||
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_runtime_image(file_path, existing_unit,
|
||||
existing_backend)) {
|
||||
if (!unmount_image(file_path, existing_unit, existing_backend)) {
|
||||
if (cache_failed_out)
|
||||
*cache_failed_out = true;
|
||||
return false;
|
||||
@@ -664,26 +657,10 @@ static bool handle_cached_or_existing_image_mount_locked(
|
||||
if (runtime_sleep_mode_active())
|
||||
return true;
|
||||
|
||||
image_cache_entry_t cached_entry;
|
||||
int cached_index = -1;
|
||||
if (get_cached_image_mount(file_path, &cached_entry, &cached_index)) {
|
||||
int root_err = 0;
|
||||
if (is_active_image_mount_point(mount_point) &&
|
||||
is_image_mount_root_accessible(mount_point, &root_err)) {
|
||||
if (success_out)
|
||||
*success_out = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(" [IMG][%s] cached mount is not active, reattaching: %s -> "
|
||||
"%s",
|
||||
attach_backend_name(cached_entry.backend), file_path,
|
||||
mount_point);
|
||||
if (!unmount_runtime_image(cached_entry.path, cached_entry.unit_id,
|
||||
cached_entry.backend)) {
|
||||
return true;
|
||||
}
|
||||
invalidate_image_cache_entry(cached_index);
|
||||
if (is_cached_image_path(file_path)) {
|
||||
if (success_out)
|
||||
*success_out = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cache_failed = false;
|
||||
@@ -937,7 +914,7 @@ static bool validate_mounted_image(const char *file_path, image_fs_type_t fs_typ
|
||||
min_device_sector);
|
||||
}
|
||||
sm_error_mark_notified();
|
||||
(void)unmount_runtime_image(file_path, unit_id, attach_backend);
|
||||
(void)unmount_image(file_path, unit_id, attach_backend);
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
@@ -1031,7 +1008,7 @@ bool mount_image(const char *file_path, image_fs_type_t fs_type) {
|
||||
return false;
|
||||
}
|
||||
if (runtime_sleep_mode_active()) {
|
||||
(void)unmount_runtime_image(file_path, unit_id, attach_backend);
|
||||
(void)unmount_image(file_path, unit_id, attach_backend);
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
@@ -1053,7 +1030,7 @@ bool mount_image(const char *file_path, image_fs_type_t fs_type) {
|
||||
(unsigned)MAX_IMAGE_MOUNTS);
|
||||
log_debug(" [IMG] image cache full, rolling back mount: %s -> %s",
|
||||
file_path, mount_point);
|
||||
(void)unmount_runtime_image(file_path, unit_id, attach_backend);
|
||||
(void)unmount_image(file_path, unit_id, attach_backend);
|
||||
errno = ENOSPC;
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
@@ -1062,31 +1039,20 @@ bool mount_image(const char *file_path, image_fs_type_t fs_type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool unmount_child_image_mounts_for_container(
|
||||
const char *mount_point, bool cleanup_source_state) {
|
||||
static bool unmount_child_image_mounts_for_container(const char *mount_point) {
|
||||
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 (cached_entry.backend == ATTACH_BACKEND_NONE ||
|
||||
cached_entry.unit_id < 0) {
|
||||
continue;
|
||||
}
|
||||
if (!path_matches_root_or_child(cached_entry.path, mount_point))
|
||||
continue;
|
||||
|
||||
log_debug(" [IMG] container child unmount: %s source=%s",
|
||||
mount_point, cached_entry.path);
|
||||
bool unmounted = cleanup_source_state
|
||||
? unmount_image(cached_entry.path,
|
||||
cached_entry.unit_id,
|
||||
cached_entry.backend)
|
||||
: unmount_runtime_image(cached_entry.path,
|
||||
cached_entry.unit_id,
|
||||
cached_entry.backend);
|
||||
if (unmounted) {
|
||||
if (unmount_image(cached_entry.path, cached_entry.unit_id,
|
||||
cached_entry.backend)) {
|
||||
invalidate_image_cache_entry(k);
|
||||
continue;
|
||||
}
|
||||
@@ -1099,9 +1065,7 @@ static bool unmount_child_image_mounts_for_container(
|
||||
return all_unmounted;
|
||||
}
|
||||
|
||||
static bool unmount_image_impl(const char *file_path, int unit_id,
|
||||
attach_backend_t backend,
|
||||
bool cleanup_source_state) {
|
||||
bool unmount_image(const char *file_path, int unit_id, attach_backend_t backend) {
|
||||
char mount_point[MAX_PATH];
|
||||
image_fs_type_t fs_type = detect_image_fs_type_for_path(file_path, NULL);
|
||||
build_image_mount_point_for_fs(file_path, fs_type, mount_point);
|
||||
@@ -1121,16 +1085,14 @@ static bool unmount_image_impl(const char *file_path, int unit_id,
|
||||
resolved_unit);
|
||||
|
||||
if (fs_type == IMAGE_FS_PFSC_CONTAINER &&
|
||||
!unmount_child_image_mounts_for_container(mount_point,
|
||||
cleanup_source_state)) {
|
||||
!unmount_child_image_mounts_for_container(mount_point)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cleanup_source_state) {
|
||||
// Permanent source cleanup removes title links before releasing the image.
|
||||
cleanup_mount_links_for_source_unmount(mount_point);
|
||||
clear_cached_game(mount_point);
|
||||
}
|
||||
// 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++) {
|
||||
@@ -1190,72 +1152,10 @@ static bool unmount_image_impl(const char *file_path, int unit_id,
|
||||
return detach_ok;
|
||||
}
|
||||
|
||||
bool unmount_image(const char *file_path, int unit_id,
|
||||
attach_backend_t backend) {
|
||||
return unmount_image_impl(file_path, unit_id, backend, true);
|
||||
}
|
||||
|
||||
bool unmount_runtime_image(const char *file_path, int unit_id,
|
||||
attach_backend_t backend) {
|
||||
return unmount_image_impl(file_path, unit_id, backend, false);
|
||||
}
|
||||
|
||||
bool release_runtime_image_mount(const char *file_path) {
|
||||
image_cache_entry_t entry;
|
||||
int index = -1;
|
||||
if (!find_image_cache_entry(file_path, &entry, &index) ||
|
||||
entry.backend == ATTACH_BACKEND_NONE || entry.unit_id < 0) {
|
||||
return true;
|
||||
}
|
||||
if (!unmount_runtime_image(entry.path, entry.unit_id, entry.backend))
|
||||
return false;
|
||||
invalidate_image_cache_entry(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool release_runtime_image_mounts(void) {
|
||||
bool all_released = true;
|
||||
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 entry;
|
||||
if (!get_image_cache_entry(k, &entry))
|
||||
continue;
|
||||
if (entry.backend == ATTACH_BACKEND_NONE || entry.unit_id < 0)
|
||||
continue;
|
||||
if (unmount_runtime_image(entry.path, entry.unit_id, entry.backend)) {
|
||||
invalidate_image_cache_entry(k);
|
||||
progress = true;
|
||||
} else {
|
||||
any_remaining = true;
|
||||
all_released = false;
|
||||
}
|
||||
}
|
||||
if (!any_remaining)
|
||||
return true;
|
||||
if (!progress)
|
||||
break;
|
||||
}
|
||||
return all_released;
|
||||
}
|
||||
|
||||
void cleanup_stale_image_mounts(void) {
|
||||
if (should_stop_requested())
|
||||
return;
|
||||
|
||||
bool has_cached_mount = false;
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; ++k) {
|
||||
image_cache_entry_t entry;
|
||||
if (get_image_cache_entry(k, &entry) &&
|
||||
entry.backend != ATTACH_BACKEND_NONE && entry.unit_id >= 0) {
|
||||
has_cached_mount = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!has_cached_mount)
|
||||
return;
|
||||
|
||||
log_debug(" [IMG] stale image cleanup begin");
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
image_cache_entry_t cached_entry;
|
||||
@@ -1263,10 +1163,6 @@ void cleanup_stale_image_mounts(void) {
|
||||
return;
|
||||
if (!get_image_cache_entry(k, &cached_entry))
|
||||
continue;
|
||||
if (cached_entry.backend == ATTACH_BACKEND_NONE ||
|
||||
cached_entry.unit_id < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log_debug(" [IMG][%s] stale cleanup check: slot=%d source=%s mount=%s",
|
||||
attach_backend_name(cached_entry.backend), k, cached_entry.path,
|
||||
@@ -1317,10 +1213,6 @@ void cleanup_stale_image_mounts_for_root(const char *root) {
|
||||
return;
|
||||
if (!get_image_cache_entry(k, &cached_entry))
|
||||
continue;
|
||||
if (cached_entry.backend == ATTACH_BACKEND_NONE ||
|
||||
cached_entry.unit_id < 0) {
|
||||
continue;
|
||||
}
|
||||
if (!path_matches_root_or_child(cached_entry.path, root) &&
|
||||
!path_matches_root_or_child(cached_entry.mount_point, root)) {
|
||||
continue;
|
||||
@@ -1362,17 +1254,13 @@ bool unmount_usb_image_mounts_for_suspend(void) {
|
||||
image_cache_entry_t cached_entry;
|
||||
if (!get_image_cache_entry(k, &cached_entry))
|
||||
continue;
|
||||
if (cached_entry.backend == ATTACH_BACKEND_NONE ||
|
||||
cached_entry.unit_id < 0) {
|
||||
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_runtime_image(cached_entry.path, cached_entry.unit_id,
|
||||
cached_entry.backend)) {
|
||||
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);
|
||||
@@ -1471,13 +1359,9 @@ bool shutdown_image_mounts(void) {
|
||||
image_cache_entry_t cached_entry;
|
||||
if (!get_image_cache_entry(k, &cached_entry))
|
||||
continue;
|
||||
if (cached_entry.backend == ATTACH_BACKEND_NONE ||
|
||||
cached_entry.unit_id < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (unmount_runtime_image(cached_entry.path, cached_entry.unit_id,
|
||||
cached_entry.backend)) {
|
||||
if (unmount_image(cached_entry.path, cached_entry.unit_id,
|
||||
cached_entry.backend)) {
|
||||
invalidate_image_cache_entry(k);
|
||||
progress = true;
|
||||
continue;
|
||||
|
||||
+7
-121
@@ -15,16 +15,6 @@ struct ImageCache {
|
||||
static struct ImageCache g_image_cache[MAX_IMAGE_MOUNTS];
|
||||
static pthread_mutex_t g_image_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static void copy_cache_entry(int index, image_cache_entry_t *entry_out) {
|
||||
memset(entry_out, 0, sizeof(*entry_out));
|
||||
(void)strlcpy(entry_out->path, g_image_cache[index].path,
|
||||
sizeof(entry_out->path));
|
||||
(void)strlcpy(entry_out->mount_point, g_image_cache[index].mount_point,
|
||||
sizeof(entry_out->mount_point));
|
||||
entry_out->unit_id = g_image_cache[index].unit_id;
|
||||
entry_out->backend = g_image_cache[index].backend;
|
||||
}
|
||||
|
||||
static int find_cache_index(const char *path, const char *mount_point) {
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
if (!g_image_cache[k].valid)
|
||||
@@ -65,18 +55,6 @@ static int upsert_image_source_mapping(const char *path, const char *mount_point
|
||||
|
||||
bool cache_image_source_mapping(const char *path, const char *mount_point) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = find_cache_index(path, mount_point);
|
||||
if (entry_index >= 0) {
|
||||
const struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
// A metadata-only path mapping must never retarget a live mount entry.
|
||||
// The live entry owns its actual filesystem mount point until release.
|
||||
if (entry->backend != ATTACH_BACKEND_NONE && entry->unit_id >= 0 &&
|
||||
(strcmp(entry->path, path) != 0 ||
|
||||
strcmp(entry->mount_point, mount_point) != 0)) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
bool ok = upsert_image_source_mapping(path, mount_point) >= 0;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return ok;
|
||||
@@ -105,25 +83,13 @@ bool get_image_cache_entry(int index, image_cache_entry_t *entry_out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
copy_cache_entry(index, entry_out);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool find_image_cache_entry(const char *path, image_cache_entry_t *entry_out,
|
||||
int *index_out) {
|
||||
if (!path || !entry_out)
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int index = find_cache_index(path, NULL);
|
||||
if (index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
copy_cache_entry(index, entry_out);
|
||||
if (index_out)
|
||||
*index_out = index;
|
||||
memset(entry_out, 0, sizeof(*entry_out));
|
||||
(void)strlcpy(entry_out->path, g_image_cache[index].path,
|
||||
sizeof(entry_out->path));
|
||||
(void)strlcpy(entry_out->mount_point, g_image_cache[index].mount_point,
|
||||
sizeof(entry_out->mount_point));
|
||||
entry_out->unit_id = g_image_cache[index].unit_id;
|
||||
entry_out->backend = g_image_cache[index].backend;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
@@ -174,83 +140,3 @@ bool resolve_image_source_from_mount_cache(const char *mount_point,
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
static int find_owning_cache_index(const char *path) {
|
||||
int best_index = -1;
|
||||
size_t best_length = 0;
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
if (!g_image_cache[i].valid)
|
||||
continue;
|
||||
size_t length = strlen(g_image_cache[i].mount_point);
|
||||
if (length <= best_length || strncmp(path, g_image_cache[i].mount_point,
|
||||
length) != 0) {
|
||||
continue;
|
||||
}
|
||||
if (path[length] != '\0' && path[length] != '/')
|
||||
continue;
|
||||
best_index = i;
|
||||
best_length = length;
|
||||
}
|
||||
return best_index;
|
||||
}
|
||||
|
||||
static size_t find_owning_chain_indices(const char *path,
|
||||
int indices[MAX_IMAGE_CHAIN_DEPTH]) {
|
||||
char current[MAX_PATH];
|
||||
(void)strlcpy(current, path, sizeof(current));
|
||||
|
||||
size_t count = 0;
|
||||
while (count < MAX_IMAGE_CHAIN_DEPTH) {
|
||||
int index = find_owning_cache_index(current);
|
||||
if (index < 0)
|
||||
break;
|
||||
indices[count++] = index;
|
||||
(void)strlcpy(current, g_image_cache[index].path, sizeof(current));
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool resolve_owning_image_source_from_mount_cache(const char *path,
|
||||
char *path_out,
|
||||
size_t path_out_size) {
|
||||
if (!path || !path_out || path_out_size == 0)
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int index = find_owning_cache_index(path);
|
||||
if (index >= 0)
|
||||
(void)strlcpy(path_out, g_image_cache[index].path, path_out_size);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return index >= 0;
|
||||
}
|
||||
|
||||
size_t resolve_image_source_chain_from_mount_cache(
|
||||
const char *path,
|
||||
char chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH]) {
|
||||
if (!path || !chain)
|
||||
return 0;
|
||||
|
||||
int indices[MAX_IMAGE_CHAIN_DEPTH];
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
size_t count = find_owning_chain_indices(path, indices);
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
(void)strlcpy(chain[i], g_image_cache[indices[count - i - 1u]].path,
|
||||
MAX_PATH);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return count;
|
||||
}
|
||||
|
||||
bool resolve_outermost_image_source_from_mount_cache(
|
||||
const char *path, char *path_out, size_t path_out_size) {
|
||||
if (!path || !path_out || path_out_size == 0)
|
||||
return false;
|
||||
|
||||
int indices[MAX_IMAGE_CHAIN_DEPTH];
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
size_t count = find_owning_chain_indices(path, indices);
|
||||
if (count > 0)
|
||||
(void)strlcpy(path_out, g_image_cache[indices[count - 1u]].path,
|
||||
path_out_size);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return count > 0;
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_image_index.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
#define IMAGE_INDEX_FILE IMAGE_INDEX_FILE_PATH
|
||||
#define IMAGE_INDEX_TEMP_FILE LOG_DIR "/image_index.bin.tmp"
|
||||
#define IMAGE_INDEX_MAGIC 0x58444953u
|
||||
#define IMAGE_INDEX_VERSION 3u
|
||||
|
||||
typedef struct {
|
||||
char path[MAX_PATH];
|
||||
int64_t size;
|
||||
int64_t mtime_sec;
|
||||
int32_t mtime_nsec;
|
||||
uint8_t complete;
|
||||
uint8_t valid;
|
||||
uint8_t reserved[2];
|
||||
} image_index_entry_t;
|
||||
|
||||
typedef struct {
|
||||
uint16_t image_index;
|
||||
uint8_t valid;
|
||||
uint8_t reserved;
|
||||
char title_id[MAX_TITLE_ID];
|
||||
} image_index_title_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
uint32_t version;
|
||||
uint32_t entry_capacity;
|
||||
uint32_t title_capacity;
|
||||
} image_index_header_t;
|
||||
|
||||
static image_index_entry_t g_image_index[MAX_IMAGE_MOUNTS];
|
||||
static image_index_title_t g_image_titles[MAX_PENDING];
|
||||
static bool g_image_index_loaded;
|
||||
static bool g_image_index_dirty;
|
||||
|
||||
static bool stamp_matches(const image_index_entry_t *entry,
|
||||
const struct stat *st) {
|
||||
return entry->size == (int64_t)st->st_size &&
|
||||
entry->mtime_sec == (int64_t)st->st_mtim.tv_sec &&
|
||||
entry->mtime_nsec == (int32_t)st->st_mtim.tv_nsec;
|
||||
}
|
||||
|
||||
static int find_entry(const char *path) {
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
if (g_image_index[i].valid && strcmp(g_image_index[i].path, path) == 0)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void clear_entry_titles(int entry_index) {
|
||||
for (int i = 0; i < MAX_PENDING; ++i) {
|
||||
if (g_image_titles[i].valid &&
|
||||
g_image_titles[i].image_index == (uint16_t)entry_index) {
|
||||
memset(&g_image_titles[i], 0, sizeof(g_image_titles[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void clear_entry(int entry_index) {
|
||||
clear_entry_titles(entry_index);
|
||||
memset(&g_image_index[entry_index], 0, sizeof(g_image_index[entry_index]));
|
||||
g_image_index_dirty = true;
|
||||
}
|
||||
|
||||
static bool save_index(void) {
|
||||
if (!g_image_index_dirty)
|
||||
return true;
|
||||
|
||||
FILE *file = fopen(IMAGE_INDEX_TEMP_FILE, "wb");
|
||||
if (!file)
|
||||
return false;
|
||||
image_index_header_t header = {
|
||||
.magic = IMAGE_INDEX_MAGIC,
|
||||
.version = IMAGE_INDEX_VERSION,
|
||||
.entry_capacity = MAX_IMAGE_MOUNTS,
|
||||
.title_capacity = MAX_PENDING,
|
||||
};
|
||||
bool ok = fwrite(&header, sizeof(header), 1, file) == 1 &&
|
||||
fwrite(g_image_index, sizeof(g_image_index), 1, file) == 1 &&
|
||||
fwrite(g_image_titles, sizeof(g_image_titles), 1, file) == 1 &&
|
||||
fflush(file) == 0;
|
||||
if (ok && fsync(fileno(file)) != 0)
|
||||
ok = false;
|
||||
if (fclose(file) != 0)
|
||||
ok = false;
|
||||
if (!ok || rename(IMAGE_INDEX_TEMP_FILE, IMAGE_INDEX_FILE) != 0) {
|
||||
int saved_errno = errno;
|
||||
if (saved_errno == 0)
|
||||
saved_errno = EIO;
|
||||
(void)unlink(IMAGE_INDEX_TEMP_FILE);
|
||||
errno = saved_errno;
|
||||
return false;
|
||||
}
|
||||
g_image_index_dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool loaded_index_is_valid(void) {
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
const image_index_entry_t *entry = &g_image_index[i];
|
||||
if (!entry->valid)
|
||||
continue;
|
||||
if (entry->complete > 1 || entry->path[0] == '\0' ||
|
||||
memchr(entry->path, '\0', sizeof(entry->path)) == NULL) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < MAX_PENDING; ++i) {
|
||||
const image_index_title_t *title = &g_image_titles[i];
|
||||
if (!title->valid)
|
||||
continue;
|
||||
if (title->image_index >= MAX_IMAGE_MOUNTS ||
|
||||
!g_image_index[title->image_index].valid ||
|
||||
title->title_id[0] == '\0' ||
|
||||
memchr(title->title_id, '\0', sizeof(title->title_id)) == NULL) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void load_index(void) {
|
||||
if (g_image_index_loaded)
|
||||
return;
|
||||
g_image_index_loaded = true;
|
||||
|
||||
FILE *file = fopen(IMAGE_INDEX_FILE, "rb");
|
||||
if (!file)
|
||||
return;
|
||||
image_index_header_t header;
|
||||
bool ok = fread(&header, sizeof(header), 1, file) == 1 &&
|
||||
header.magic == IMAGE_INDEX_MAGIC &&
|
||||
header.version == IMAGE_INDEX_VERSION &&
|
||||
header.entry_capacity == MAX_IMAGE_MOUNTS &&
|
||||
header.title_capacity == MAX_PENDING &&
|
||||
fread(g_image_index, sizeof(g_image_index), 1, file) == 1 &&
|
||||
fread(g_image_titles, sizeof(g_image_titles), 1, file) == 1;
|
||||
(void)fclose(file);
|
||||
if (ok)
|
||||
ok = loaded_index_is_valid();
|
||||
if (!ok) {
|
||||
memset(g_image_index, 0, sizeof(g_image_index));
|
||||
memset(g_image_titles, 0, sizeof(g_image_titles));
|
||||
(void)unlink(IMAGE_INDEX_FILE);
|
||||
log_debug(" [IMGIDX] invalid cache discarded: %s", IMAGE_INDEX_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
static int reserve_entry(const char *path) {
|
||||
int entry_index = find_entry(path);
|
||||
if (entry_index >= 0)
|
||||
return entry_index;
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
if (!g_image_index[i].valid) {
|
||||
g_image_index[i].valid = 1;
|
||||
(void)strlcpy(g_image_index[i].path, path, sizeof(g_image_index[i].path));
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool cached_titles_ready(int entry_index,
|
||||
const struct AppDbTitleList *app_db_titles,
|
||||
bool app_db_titles_ready) {
|
||||
const char *image_path = g_image_index[entry_index].path;
|
||||
for (int i = 0; i < MAX_PENDING; ++i) {
|
||||
if (!g_image_titles[i].valid ||
|
||||
g_image_titles[i].image_index != (uint16_t)entry_index) {
|
||||
continue;
|
||||
}
|
||||
const char *title_id = g_image_titles[i].title_id;
|
||||
if (!is_installed(title_id) || !has_appmeta_data(title_id))
|
||||
return false;
|
||||
char runtime_path[MAX_PATH];
|
||||
char linked_image[MAX_PATH];
|
||||
if (!read_mount_link(title_id, runtime_path, sizeof(runtime_path)) ||
|
||||
!is_under_image_mount_base(runtime_path) ||
|
||||
!read_mount_image_link(title_id, linked_image, sizeof(linked_image)) ||
|
||||
strcmp(linked_image, image_path) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (app_db_titles_ready &&
|
||||
!app_db_title_list_contains(app_db_titles, title_id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sm_image_index_needs_scan(const char *path, const struct stat *st,
|
||||
const struct AppDbTitleList *app_db_titles,
|
||||
bool app_db_titles_ready) {
|
||||
load_index();
|
||||
int entry_index = find_entry(path);
|
||||
if (entry_index < 0 || !g_image_index[entry_index].complete ||
|
||||
!stamp_matches(&g_image_index[entry_index], st)) {
|
||||
return true;
|
||||
}
|
||||
return !cached_titles_ready(entry_index, app_db_titles, app_db_titles_ready);
|
||||
}
|
||||
|
||||
void sm_image_index_begin_scan(const char *path, const struct stat *st) {
|
||||
load_index();
|
||||
int entry_index = reserve_entry(path);
|
||||
if (entry_index < 0) {
|
||||
log_debug(" [IMGIDX] cache full, scanning without fingerprint: %s", path);
|
||||
return;
|
||||
}
|
||||
clear_entry_titles(entry_index);
|
||||
image_index_entry_t *entry = &g_image_index[entry_index];
|
||||
entry->size = (int64_t)st->st_size;
|
||||
entry->mtime_sec = (int64_t)st->st_mtim.tv_sec;
|
||||
entry->mtime_nsec = (int32_t)st->st_mtim.tv_nsec;
|
||||
entry->complete = 0;
|
||||
entry->valid = 1;
|
||||
g_image_index_dirty = true;
|
||||
}
|
||||
|
||||
void sm_image_index_record_game(const char *game_path, const char *title_id) {
|
||||
load_index();
|
||||
char image_path[MAX_PATH];
|
||||
if (!resolve_outermost_image_source_from_mount_cache(
|
||||
game_path, image_path, sizeof(image_path))) {
|
||||
return;
|
||||
}
|
||||
int entry_index = find_entry(image_path);
|
||||
if (entry_index < 0) {
|
||||
struct stat st;
|
||||
if (stat(image_path, &st) != 0)
|
||||
return;
|
||||
sm_image_index_begin_scan(image_path, &st);
|
||||
entry_index = find_entry(image_path);
|
||||
if (entry_index < 0)
|
||||
return;
|
||||
}
|
||||
char tracked_path[MAX_PATH];
|
||||
char linked_image[MAX_PATH];
|
||||
if (is_installed(title_id) &&
|
||||
read_mount_link(title_id, tracked_path, sizeof(tracked_path)) &&
|
||||
strcmp(tracked_path, game_path) == 0 &&
|
||||
(!read_mount_image_link(title_id, linked_image, sizeof(linked_image)) ||
|
||||
strcmp(linked_image, image_path) != 0) &&
|
||||
!write_mount_image_link(title_id, image_path)) {
|
||||
log_debug(" [IMGIDX] failed to repair image link: %s -> %s", title_id,
|
||||
image_path);
|
||||
}
|
||||
for (int i = 0; i < MAX_PENDING; ++i) {
|
||||
if (g_image_titles[i].valid &&
|
||||
g_image_titles[i].image_index == (uint16_t)entry_index &&
|
||||
strcmp(g_image_titles[i].title_id, title_id) == 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < MAX_PENDING; ++i) {
|
||||
if (g_image_titles[i].valid)
|
||||
continue;
|
||||
g_image_titles[i].valid = 1;
|
||||
g_image_titles[i].image_index = (uint16_t)entry_index;
|
||||
(void)strlcpy(g_image_titles[i].title_id, title_id,
|
||||
sizeof(g_image_titles[i].title_id));
|
||||
g_image_index_dirty = true;
|
||||
return;
|
||||
}
|
||||
log_debug(" [IMGIDX] title cache full: %s", title_id);
|
||||
}
|
||||
|
||||
void sm_image_index_complete_scan(const char *path) {
|
||||
load_index();
|
||||
int entry_index = find_entry(path);
|
||||
if (entry_index < 0)
|
||||
return;
|
||||
image_index_entry_t *entry = &g_image_index[entry_index];
|
||||
if (!entry->complete) {
|
||||
entry->complete = 1;
|
||||
g_image_index_dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
void sm_image_index_flush(void) {
|
||||
load_index();
|
||||
if (!save_index())
|
||||
log_debug(" [IMGIDX] failed to persist scan index: %s", strerror(errno));
|
||||
}
|
||||
|
||||
void sm_image_index_prune(void) {
|
||||
struct stat st;
|
||||
|
||||
load_index();
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
if (g_image_index[i].valid && stat(g_image_index[i].path, &st) != 0 &&
|
||||
errno == ENOENT) {
|
||||
clear_entry(i);
|
||||
}
|
||||
}
|
||||
if (!save_index())
|
||||
log_debug(" [IMGIDX] failed to persist prune: %s", strerror(errno));
|
||||
}
|
||||
+44
-36
@@ -14,7 +14,6 @@
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_paths.h"
|
||||
#include "sm_manual.h"
|
||||
#include "sm_shellcore_hooks.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
|
||||
@@ -62,6 +61,29 @@ static app_install_title_dir_fn_t resolve_app_install_title_dir(void) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool write_link_file(const char *path, const char *value) {
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) {
|
||||
log_debug(" [LINK] open failed for %s: %s", path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
int saved_errno = 0;
|
||||
if (fprintf(f, "%s", value) < 0)
|
||||
saved_errno = errno;
|
||||
if (fflush(f) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
if (fclose(f) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
|
||||
if (saved_errno != 0) {
|
||||
errno = saved_errno;
|
||||
log_debug(" [LINK] write failed for %s: %s", path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_appmeta_file(const char *name) {
|
||||
if (!name)
|
||||
return false;
|
||||
@@ -117,11 +139,11 @@ static bool copy_optional_trophy_metadata_file(const char *src_sce_sys,
|
||||
char dst_path[MAX_PATH];
|
||||
|
||||
snprintf(src_path, sizeof(src_path), "%s/%s", src_sce_sys, relative_path);
|
||||
if (!path_exists(src_path))
|
||||
if (access(src_path, F_OK) != 0)
|
||||
return true;
|
||||
|
||||
snprintf(dst_path, sizeof(dst_path), "%s/%s", dst_base, relative_path);
|
||||
if (path_exists(dst_path))
|
||||
if (access(dst_path, F_OK) == 0)
|
||||
return true;
|
||||
|
||||
if (copy_file(src_path, dst_path) == 0)
|
||||
@@ -171,8 +193,7 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
char user_sce_sys[MAX_PATH];
|
||||
char src_sce_sys[MAX_PATH];
|
||||
char src_snd0[MAX_PATH];
|
||||
char image_source_chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH];
|
||||
size_t image_source_count = 0;
|
||||
char image_source_path[MAX_PATH];
|
||||
bool has_image_source = false;
|
||||
bool appmeta_missing = false;
|
||||
bool metadata_restaged = false;
|
||||
@@ -185,9 +206,8 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
snprintf(user_app_dir, sizeof(user_app_dir), "%s/%s", APP_BASE, title_id);
|
||||
|
||||
if (is_under_image_mount_base(src_path)) {
|
||||
image_source_count = resolve_image_source_chain_from_mount_cache(
|
||||
src_path, image_source_chain);
|
||||
has_image_source = image_source_count > 0;
|
||||
has_image_source = resolve_image_source_from_mount_cache(
|
||||
src_path, image_source_path, sizeof(image_source_path));
|
||||
if (!has_image_source) {
|
||||
log_debug(" [LINK] image source lookup failed for %s: %s", title_id,
|
||||
src_path);
|
||||
@@ -269,8 +289,7 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
mkdir(APP_BASE, 0777);
|
||||
mkdir(user_app_dir, 0777);
|
||||
snprintf(lnk_path, sizeof(lnk_path), "%s/mount.lnk", user_app_dir);
|
||||
if (!write_mount_link(title_id, src_path)) {
|
||||
log_debug(" [LINK] write failed for %s: %s", lnk_path, strerror(errno));
|
||||
if (!write_link_file(lnk_path, src_path)) {
|
||||
(void)rollback_title_nullfs_mount(title_id, src_path);
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
@@ -282,18 +301,17 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
snprintf(img_lnk_path, sizeof(img_lnk_path), "%s/mount_img.lnk",
|
||||
user_app_dir);
|
||||
if (has_image_source) {
|
||||
if (!write_mount_image_chain(title_id, image_source_chain,
|
||||
image_source_count)) {
|
||||
if (!write_link_file(img_lnk_path, image_source_path)) {
|
||||
(void)unlink(lnk_path);
|
||||
(void)rollback_title_nullfs_mount(title_id, src_path);
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
log_debug(" [LINK] mount_img.lnk created: %s -> %s layers=%zu",
|
||||
img_lnk_path, image_source_chain[0], image_source_count);
|
||||
if (!cache_image_source_mapping(image_source_chain[0], src_path)) {
|
||||
log_debug(" [LINK] mount_img.lnk created: %s -> %s", img_lnk_path,
|
||||
image_source_path);
|
||||
if (!cache_image_source_mapping(image_source_path, src_path)) {
|
||||
log_debug(" [LINK] image source cache update failed: %s -> %s",
|
||||
image_source_chain[0], src_path);
|
||||
src_path, image_source_path);
|
||||
}
|
||||
} else if (unlink(img_lnk_path) != 0 && errno != ENOENT) {
|
||||
log_debug(" [LINK] remove failed for %s: %s", img_lnk_path,
|
||||
@@ -324,27 +342,17 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
}
|
||||
|
||||
// REGISTER
|
||||
int res = 0;
|
||||
if (sm_shellcore_install_bridge_enabled()) {
|
||||
if (!sm_shellcore_install_title_dir(title_id, APP_BASE "/", &res)) {
|
||||
log_debug(" [REG] internal AppInstallTitleDir bridge unavailable");
|
||||
notify_system("Register failed: %s (%s)\nTitleDir bridge unavailable",
|
||||
title_name, title_id);
|
||||
return false;
|
||||
}
|
||||
mark_register_attempted(title_id);
|
||||
} else {
|
||||
app_install_title_dir_fn_t app_install_title_dir_fn =
|
||||
resolve_app_install_title_dir();
|
||||
if (!app_install_title_dir_fn) {
|
||||
log_debug(" [REG] sceAppInstUtilAppInstallTitleDir unavailable");
|
||||
notify_system("Register failed: %s (%s)\nAppInstallTitleDir unavailable",
|
||||
title_name, title_id);
|
||||
return false;
|
||||
}
|
||||
mark_register_attempted(title_id);
|
||||
res = app_install_title_dir_fn(title_id, APP_BASE "/", NULL);
|
||||
app_install_title_dir_fn_t app_install_title_dir_fn =
|
||||
resolve_app_install_title_dir();
|
||||
if (!app_install_title_dir_fn) {
|
||||
log_debug(" [REG] sceAppInstUtilAppInstallTitleDir unavailable");
|
||||
notify_system("Register failed: %s (%s)\nAppInstallTitleDir unavailable",
|
||||
title_name, title_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
mark_register_attempted(title_id);
|
||||
int res = app_install_title_dir_fn(title_id, APP_BASE "/", 0);
|
||||
sceKernelUsleep(200000);
|
||||
|
||||
if (res == 0) {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "sm_log.h"
|
||||
#include "sm_manual.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_scan.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_title_state.h"
|
||||
|
||||
@@ -50,10 +49,6 @@ static bool install_queue_active(void) {
|
||||
return runtime_config()->app_install_all_enabled || g_tracked_install_count > 0;
|
||||
}
|
||||
|
||||
bool sm_install_has_pending_work(void) {
|
||||
return g_tracked_install_count > 0;
|
||||
}
|
||||
|
||||
static pending_install_entry_t *find_pending_install_entry(
|
||||
const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
@@ -285,8 +280,6 @@ static void poll_pending_installs(void) {
|
||||
if (g_submitted_install_count == 0)
|
||||
schedule_queued_install_submit(now_us);
|
||||
free_app_db_title_list(&app_db_titles);
|
||||
if (!sm_install_has_pending_work() && !release_scan_runtime_mounts())
|
||||
log_debug(" [IMG] deferred release after install completion");
|
||||
}
|
||||
|
||||
uint64_t sm_install_next_wake_us(uint64_t now_us) {
|
||||
@@ -432,7 +425,7 @@ bool sm_install_submit_queued(void) {
|
||||
log_queued_install_batch();
|
||||
notify_queued_install_batch();
|
||||
|
||||
int res = sceAppInstUtilAppInstallAll(NULL);
|
||||
int res = sceAppInstUtilAppInstallAll();
|
||||
if (res != 0) {
|
||||
log_debug(" [REG] Batch install request failed: 0x%x", res);
|
||||
if (!g_queued_install_submit_failure_notified) {
|
||||
|
||||
+34
-82
@@ -4,7 +4,6 @@
|
||||
#include "sm_scan_tree.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_gameinfo.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_config_mount.h"
|
||||
@@ -17,11 +16,9 @@
|
||||
#include "sm_stability.h"
|
||||
#include "sm_title_state.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_image_index.h"
|
||||
#include "sm_image.h"
|
||||
#include "sm_install_queue.h"
|
||||
#include "sm_manual.h"
|
||||
#include "sm_shellcore_service.h"
|
||||
|
||||
typedef struct {
|
||||
char discovered_param_roots[MAX_PENDING][MAX_PATH];
|
||||
@@ -270,11 +267,9 @@ static existing_directory_result_t handle_existing_directory_candidate(
|
||||
link_matches_source = installed && link_matches_source;
|
||||
bool source_valid = false;
|
||||
if (link_matches_source) {
|
||||
char eboot_path[MAX_PATH];
|
||||
int written = snprintf(eboot_path, sizeof(eboot_path), "%s/eboot.bin",
|
||||
full_path);
|
||||
source_valid = written > 0 && (size_t)written < sizeof(eboot_path) &&
|
||||
path_exists(eboot_path);
|
||||
source_valid = is_data_mounted(info->title_id);
|
||||
if (!source_valid && mount_title_nullfs(info->title_id, full_path))
|
||||
source_valid = is_data_mounted(info->title_id);
|
||||
}
|
||||
|
||||
if (source_valid && appmeta_present) {
|
||||
@@ -370,8 +365,6 @@ static bool try_collect_candidate_for_directory(
|
||||
if (probe_result == DIRECTORY_CANDIDATE_DESCEND)
|
||||
return false;
|
||||
|
||||
sm_image_index_record_game(full_path, info.title_id);
|
||||
|
||||
int duplicate_candidate_index = find_scan_candidate_index_by_title_id(
|
||||
candidates, *candidate_count, info.title_id);
|
||||
const char *duplicate_candidate_path =
|
||||
@@ -474,36 +467,17 @@ static bool collect_candidate_image_visit(const char *image_path,
|
||||
(void)depth_from_root;
|
||||
|
||||
collect_candidates_walk_ctx_t *ctx = (collect_candidates_walk_ctx_t *)ctx_ptr;
|
||||
struct stat image_st;
|
||||
if (stat(image_path, &image_st) != 0)
|
||||
if (!maybe_mount_image_file(image_path, image_name, ctx->unstable_found_out))
|
||||
return true;
|
||||
bool persistent_image = !is_under_image_mount_base(image_path);
|
||||
if (persistent_image &&
|
||||
!sm_image_index_needs_scan(image_path, &image_st, ctx->app_db->titles,
|
||||
ctx->app_db->titles_ready)) {
|
||||
return true;
|
||||
}
|
||||
bool image_unstable = false;
|
||||
if (!maybe_mount_image_file(image_path, image_name, &image_unstable)) {
|
||||
if (image_unstable && ctx->unstable_found_out)
|
||||
*ctx->unstable_found_out = true;
|
||||
return true;
|
||||
}
|
||||
if (persistent_image)
|
||||
sm_image_index_begin_scan(image_path, &image_st);
|
||||
|
||||
char mount_point[MAX_PATH];
|
||||
get_image_mount_point_for_source(image_path, mount_point);
|
||||
collect_scan_candidates_from_manual_root(
|
||||
mount_point, ctx->manual_source_path, ctx->candidates,
|
||||
ctx->max_candidates, ctx->candidate_count, ctx->app_db,
|
||||
ctx->discovered_param_roots, ctx->discovered_param_root_count,
|
||||
&image_unstable);
|
||||
if (image_unstable && ctx->unstable_found_out)
|
||||
*ctx->unstable_found_out = true;
|
||||
if (persistent_image && !image_unstable && !should_stop_requested() &&
|
||||
!runtime_sleep_mode_active()) {
|
||||
sm_image_index_complete_scan(image_path);
|
||||
if (ctx->manual_source_path && is_pfsc_image_mount_base_or_child(image_path)) {
|
||||
char mount_point[MAX_PATH];
|
||||
get_image_mount_point_for_source(image_path, mount_point);
|
||||
collect_scan_candidates_from_manual_root(
|
||||
mount_point, ctx->manual_source_path, ctx->candidates,
|
||||
ctx->max_candidates, ctx->candidate_count, ctx->app_db,
|
||||
ctx->discovered_param_roots, ctx->discovered_param_root_count,
|
||||
ctx->unstable_found_out);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -572,29 +546,15 @@ static void collect_scan_candidates_from_manual_path(
|
||||
const char *name = get_filename_component(manual_path);
|
||||
if (S_ISREG(st.st_mode) &&
|
||||
is_supported_image_file_path(manual_path, name)) {
|
||||
if (!sm_image_index_needs_scan(manual_path, &st, app_db->titles,
|
||||
app_db->titles_ready))
|
||||
if (!maybe_mount_image_file(manual_path, name, unstable_found_out))
|
||||
return;
|
||||
bool image_unstable = false;
|
||||
if (!maybe_mount_image_file(manual_path, name, &image_unstable)) {
|
||||
if (image_unstable && unstable_found_out)
|
||||
*unstable_found_out = true;
|
||||
return;
|
||||
}
|
||||
sm_image_index_begin_scan(manual_path, &st);
|
||||
|
||||
char mount_point[MAX_PATH];
|
||||
get_image_mount_point_for_source(manual_path, mount_point);
|
||||
collect_scan_candidates_from_manual_root(
|
||||
mount_point, manual_path, candidates, max_candidates, candidate_count,
|
||||
app_db, discovered_param_roots, discovered_param_root_count,
|
||||
&image_unstable);
|
||||
if (image_unstable && unstable_found_out)
|
||||
*unstable_found_out = true;
|
||||
if (!image_unstable && !should_stop_requested() &&
|
||||
!runtime_sleep_mode_active()) {
|
||||
sm_image_index_complete_scan(manual_path);
|
||||
}
|
||||
unstable_found_out);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -693,13 +653,21 @@ static bool resolve_backport_path(const char *title_id,
|
||||
return false;
|
||||
}
|
||||
|
||||
bool mount_backport_overlay_for_title(const char *source_path,
|
||||
const char *title_id,
|
||||
const char *owning_scan_root,
|
||||
bool *unstable_found_out) {
|
||||
typedef struct {
|
||||
bool *unstable_found_out;
|
||||
} backport_overlay_ctx_t;
|
||||
|
||||
static bool mount_backport_overlay_for_cached_game(const char *source_path,
|
||||
const char *title_id,
|
||||
const char *title_name,
|
||||
const char *owning_scan_root,
|
||||
void *ctx_ptr) {
|
||||
(void)title_name;
|
||||
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return false;
|
||||
|
||||
backport_overlay_ctx_t *ctx = (backport_overlay_ctx_t *)ctx_ptr;
|
||||
char backport_path[MAX_PATH];
|
||||
if (!resolve_backport_path(title_id, owning_scan_root, backport_path)) {
|
||||
return true;
|
||||
@@ -714,9 +682,9 @@ bool mount_backport_overlay_for_title(const char *source_path,
|
||||
return true;
|
||||
}
|
||||
if (!wait_for_stability_fast(backport_path, "BKP")) {
|
||||
if (unstable_found_out)
|
||||
*unstable_found_out = true;
|
||||
return false;
|
||||
if (ctx->unstable_found_out)
|
||||
*ctx->unstable_found_out = true;
|
||||
return true;
|
||||
}
|
||||
overlay_active = false;
|
||||
if (!reconcile_title_backport_mount(title_id, source_path, backport_path,
|
||||
@@ -736,23 +704,11 @@ bool mount_backport_overlay_for_title(const char *source_path,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool release_scan_runtime_mounts(void) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return false;
|
||||
if (sm_game_lifecycle_has_active_game() ||
|
||||
sm_shellcore_service_has_prepared_mount()) {
|
||||
return true;
|
||||
}
|
||||
runtime_mount_state_lock();
|
||||
if (sm_game_lifecycle_has_active_game() ||
|
||||
sm_shellcore_service_has_prepared_mount()) {
|
||||
runtime_mount_state_unlock();
|
||||
return true;
|
||||
}
|
||||
unmount_all_title_runtime_layers();
|
||||
bool released = release_runtime_image_mounts();
|
||||
runtime_mount_state_unlock();
|
||||
return released;
|
||||
void mount_backport_overlays(bool *unstable_found_out) {
|
||||
backport_overlay_ctx_t ctx = {
|
||||
.unstable_found_out = unstable_found_out,
|
||||
};
|
||||
for_each_cached_game_entry(NULL, mount_backport_overlay_for_cached_game, &ctx);
|
||||
}
|
||||
|
||||
// --- Unified Scan Pass (images + game candidates) ---
|
||||
@@ -763,7 +719,6 @@ void cleanup_lost_sources_before_scan(void) {
|
||||
cleanup_mount_links(NULL, true);
|
||||
// 3) Unmount stale image mounts for deleted image files.
|
||||
cleanup_stale_image_mounts();
|
||||
sm_image_index_prune();
|
||||
// 4) Drop stale path-state entries.
|
||||
prune_path_state();
|
||||
}
|
||||
@@ -772,7 +727,6 @@ void cleanup_lost_sources_for_scan_root(const char *scan_root) {
|
||||
prune_game_cache_for_root(scan_root);
|
||||
cleanup_mount_links(scan_root, true);
|
||||
cleanup_stale_image_mounts_for_root(scan_root);
|
||||
sm_image_index_prune();
|
||||
prune_path_state_for_root(scan_root);
|
||||
}
|
||||
|
||||
@@ -894,7 +848,6 @@ int collect_scan_candidates_for_scan_root(const char *scan_root,
|
||||
|
||||
if (total_found_out)
|
||||
*total_found_out = discovered_param_root_count;
|
||||
sm_image_index_flush();
|
||||
free_app_db_title_list(&blocked_ppsa_titles);
|
||||
free_app_db_title_list(&app_db_titles);
|
||||
return candidate_count;
|
||||
@@ -944,7 +897,6 @@ int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates,
|
||||
|
||||
if (total_found_out)
|
||||
*total_found_out = discovered_param_root_count;
|
||||
sm_image_index_flush();
|
||||
free_app_db_title_list(&blocked_ppsa_titles);
|
||||
free_app_db_title_list(&app_db_titles);
|
||||
return candidate_count;
|
||||
|
||||
+78
-59
@@ -1,5 +1,6 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/event.h>
|
||||
#include <sys/select.h>
|
||||
@@ -22,7 +23,6 @@
|
||||
#include "sm_scan.h"
|
||||
#include "sm_scan_tree.h"
|
||||
#include "sm_scanner.h"
|
||||
#include "sm_shellcore_service.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
@@ -109,6 +109,14 @@ static void schedule_manual_probe(uint64_t now_us) {
|
||||
g_scanner_manual_probe_due_us = now_us + SCANNER_MANUAL_PROBE_INTERVAL_US;
|
||||
}
|
||||
|
||||
static bool set_fd_nonblocking(int fd) {
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0)
|
||||
return false;
|
||||
|
||||
return fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
|
||||
}
|
||||
|
||||
static void reset_scanner_root_states(void) {
|
||||
memset(g_scanner_root_states, 0, sizeof(g_scanner_root_states));
|
||||
}
|
||||
@@ -232,7 +240,12 @@ static bool fakelib_runtime_config_changed(const runtime_config_t *old_cfg,
|
||||
static uint32_t scanner_config_topology_hash(void) {
|
||||
uint32_t hash = 2166136261u;
|
||||
int scan_path_count = get_scan_path_count();
|
||||
uint32_t values[] = {runtime_config()->scan_depth, (uint32_t)scan_path_count};
|
||||
uint32_t values[] = {
|
||||
runtime_config()->scan_depth,
|
||||
runtime_config()->watch_subdirs ? 1u : 0u,
|
||||
runtime_config()->watch_image_files ? 1u : 0u,
|
||||
(uint32_t)scan_path_count,
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
|
||||
for (unsigned shift = 0; shift < 32u; shift += 8u) {
|
||||
@@ -576,6 +589,7 @@ static bool resolve_watch_tree_rebuild_target(const scanner_watch_entry_t *entry
|
||||
typedef struct {
|
||||
int kq;
|
||||
int scan_root_index;
|
||||
bool watch_subdirs;
|
||||
} register_watch_tree_ctx_t;
|
||||
|
||||
static sm_scan_tree_dir_visit_t register_watch_directory_visit(
|
||||
@@ -584,9 +598,11 @@ static sm_scan_tree_dir_visit_t register_watch_directory_visit(
|
||||
scanner_watch_kind_t kind =
|
||||
(depth_from_root == 0u) ? SCANNER_WATCH_SCAN_ROOT
|
||||
: SCANNER_WATCH_SCAN_SUBDIR;
|
||||
if (!register_scanner_watch_entry(ctx->kq, ctx->scan_root_index, dir_path, kind,
|
||||
(uint8_t)depth_from_root)) {
|
||||
return SM_SCAN_TREE_DIR_ABORT;
|
||||
if (depth_from_root == 0u || ctx->watch_subdirs) {
|
||||
if (!register_scanner_watch_entry(ctx->kq, ctx->scan_root_index, dir_path,
|
||||
kind, (uint8_t)depth_from_root)) {
|
||||
return SM_SCAN_TREE_DIR_ABORT;
|
||||
}
|
||||
}
|
||||
|
||||
return SM_SCAN_TREE_DIR_DESCEND;
|
||||
@@ -629,19 +645,28 @@ static bool rebuild_scan_root_watch_tree(int kq, int scan_root_index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
unsigned int scan_depth = get_scan_depth_for_root(scan_root);
|
||||
|
||||
register_watch_tree_ctx_t walk_ctx = {
|
||||
.kq = kq,
|
||||
.scan_root_index = scan_root_index,
|
||||
};
|
||||
sm_scan_tree_callbacks_t callbacks = {
|
||||
.on_directory = register_watch_directory_visit,
|
||||
.on_image_file = register_watch_image_visit,
|
||||
};
|
||||
if (!sm_scan_tree_walk(scan_root, scan_root, 0u, scan_depth, &callbacks,
|
||||
&walk_ctx)) {
|
||||
return false;
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
if (!cfg->watch_subdirs && !cfg->watch_image_files) {
|
||||
if (!register_scanner_watch_entry(kq, scan_root_index, scan_root,
|
||||
SCANNER_WATCH_SCAN_ROOT, 0u)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
unsigned int scan_depth = get_scan_depth_for_root(scan_root);
|
||||
register_watch_tree_ctx_t walk_ctx = {
|
||||
.kq = kq,
|
||||
.scan_root_index = scan_root_index,
|
||||
.watch_subdirs = cfg->watch_subdirs,
|
||||
};
|
||||
sm_scan_tree_callbacks_t callbacks = {
|
||||
.on_directory = register_watch_directory_visit,
|
||||
.on_image_file =
|
||||
cfg->watch_image_files ? register_watch_image_visit : NULL,
|
||||
};
|
||||
if (!sm_scan_tree_walk(scan_root, scan_root, 0u, scan_depth, &callbacks,
|
||||
&walk_ctx)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
char backport_root[MAX_PATH];
|
||||
@@ -682,10 +707,15 @@ static bool rebuild_scan_root_watch_subtree(int kq, int scan_root_index,
|
||||
return true;
|
||||
}
|
||||
|
||||
unsigned int scan_depth = get_scan_depth_for_root(scan_root);
|
||||
|
||||
if (!remove_scan_root_watch_entries_for_path(scan_root_index, rebuild_path))
|
||||
return false;
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
if (!cfg->watch_subdirs && !cfg->watch_image_files) {
|
||||
clear_scan_root_watch_tree_state(scan_root_index);
|
||||
return true;
|
||||
}
|
||||
|
||||
unsigned int scan_depth = get_scan_depth_for_root(scan_root);
|
||||
if (rebuild_depth > scan_depth) {
|
||||
clear_scan_root_watch_tree_state(scan_root_index);
|
||||
return true;
|
||||
@@ -694,10 +724,11 @@ static bool rebuild_scan_root_watch_subtree(int kq, int scan_root_index,
|
||||
register_watch_tree_ctx_t walk_ctx = {
|
||||
.kq = kq,
|
||||
.scan_root_index = scan_root_index,
|
||||
.watch_subdirs = cfg->watch_subdirs,
|
||||
};
|
||||
sm_scan_tree_callbacks_t callbacks = {
|
||||
.on_directory = register_watch_directory_visit,
|
||||
.on_image_file = register_watch_image_visit,
|
||||
.on_image_file = cfg->watch_image_files ? register_watch_image_visit : NULL,
|
||||
};
|
||||
if (!sm_scan_tree_walk(scan_root, rebuild_path, rebuild_depth,
|
||||
scan_depth - rebuild_depth, &callbacks, &walk_ctx)) {
|
||||
@@ -977,8 +1008,9 @@ static bool run_full_scan_cycle(bool startup_sync, const char *reason,
|
||||
if (should_abort_scan_cycle())
|
||||
return false;
|
||||
|
||||
if (!sm_install_has_pending_work() && !release_scan_runtime_mounts())
|
||||
log_debug(" [IMG] some discovery mounts remain busy");
|
||||
mount_backport_overlays(&unstable_found);
|
||||
if (should_abort_scan_cycle())
|
||||
return false;
|
||||
|
||||
if (unstable_found_out)
|
||||
*unstable_found_out = unstable_found;
|
||||
@@ -1015,8 +1047,9 @@ static bool run_targeted_scan_cycle(int scan_root_index,
|
||||
if (should_abort_scan_cycle())
|
||||
return false;
|
||||
|
||||
if (!sm_install_has_pending_work() && !release_scan_runtime_mounts())
|
||||
log_debug(" [IMG] some discovery mounts remain busy");
|
||||
mount_backport_overlays(&unstable_found);
|
||||
if (should_abort_scan_cycle())
|
||||
return false;
|
||||
|
||||
if (unstable_found_out)
|
||||
*unstable_found_out = unstable_found;
|
||||
@@ -1063,8 +1096,7 @@ static int find_due_dirty_scan_root(uint64_t now_us) {
|
||||
}
|
||||
|
||||
static uint64_t compute_next_scan_deadline_us(uint64_t now_us,
|
||||
uint64_t full_resync_due_us,
|
||||
bool include_scan_work) {
|
||||
uint64_t full_resync_due_us) {
|
||||
uint64_t next_deadline = full_resync_due_us;
|
||||
uint64_t install_wake_us = sm_install_next_wake_us(now_us);
|
||||
|
||||
@@ -1084,7 +1116,7 @@ static uint64_t compute_next_scan_deadline_us(uint64_t now_us,
|
||||
next_deadline = g_scanner_config_probe_due_us;
|
||||
}
|
||||
|
||||
if (include_scan_work && g_scanner_manual_scan_due_us != 0 &&
|
||||
if (g_scanner_manual_scan_due_us != 0 &&
|
||||
(next_deadline == 0 || g_scanner_manual_scan_due_us < next_deadline)) {
|
||||
next_deadline = g_scanner_manual_scan_due_us;
|
||||
}
|
||||
@@ -1094,14 +1126,12 @@ static uint64_t compute_next_scan_deadline_us(uint64_t now_us,
|
||||
next_deadline = g_scanner_manual_probe_due_us;
|
||||
}
|
||||
|
||||
if (include_scan_work) {
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
const scanner_root_state_t *state = &g_scanner_root_states[i];
|
||||
if (!state->dirty)
|
||||
continue;
|
||||
if (next_deadline == 0 || state->ready_after_us < next_deadline)
|
||||
next_deadline = state->ready_after_us;
|
||||
}
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
const scanner_root_state_t *state = &g_scanner_root_states[i];
|
||||
if (!state->dirty)
|
||||
continue;
|
||||
if (next_deadline == 0 || state->ready_after_us < next_deadline)
|
||||
next_deadline = state->ready_after_us;
|
||||
}
|
||||
|
||||
return next_deadline;
|
||||
@@ -1284,8 +1314,8 @@ bool sm_scanner_init(void) {
|
||||
close_scanner_wake_pipe();
|
||||
return false;
|
||||
}
|
||||
if (!sm_set_fd_nonblocking(g_scanner_wake_pipe[0]) ||
|
||||
!sm_set_fd_nonblocking(g_scanner_wake_pipe[1])) {
|
||||
if (!set_fd_nonblocking(g_scanner_wake_pipe[0]) ||
|
||||
!set_fd_nonblocking(g_scanner_wake_pipe[1])) {
|
||||
log_debug(" [SCAN] wake pipe nonblocking setup failed: %s",
|
||||
strerror(errno));
|
||||
close_scanner_wake_pipe();
|
||||
@@ -1374,7 +1404,6 @@ void sm_scanner_run_loop(void) {
|
||||
uint64_t next_full_resync_us =
|
||||
monotonic_time_us() + scanner_full_resync_interval_us();
|
||||
bool was_sleeping = false;
|
||||
bool scan_work_blocked = false;
|
||||
|
||||
while (true) {
|
||||
if (should_stop_requested()) {
|
||||
@@ -1410,11 +1439,8 @@ void sm_scanner_run_loop(void) {
|
||||
}
|
||||
}
|
||||
|
||||
bool game_mount_busy = sm_game_lifecycle_has_active_game() ||
|
||||
sm_shellcore_service_has_prepared_mount();
|
||||
char scan_reason[128];
|
||||
if (!game_mount_busy &&
|
||||
consume_scan_now_request(scan_reason, sizeof(scan_reason))) {
|
||||
if (consume_scan_now_request(scan_reason, sizeof(scan_reason))) {
|
||||
bool unstable_found = false;
|
||||
if (!run_full_scan_cycle(false, scan_reason, &unstable_found)) {
|
||||
if (runtime_sleep_mode_active())
|
||||
@@ -1441,14 +1467,10 @@ void sm_scanner_run_loop(void) {
|
||||
}
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
game_mount_busy = sm_game_lifecycle_has_active_game() ||
|
||||
sm_shellcore_service_has_prepared_mount();
|
||||
if (game_mount_busy) {
|
||||
if (sm_game_lifecycle_has_active_game()) {
|
||||
next_full_resync_us = 0;
|
||||
scan_work_blocked = true;
|
||||
} else if (scan_work_blocked) {
|
||||
scan_work_blocked = false;
|
||||
next_full_resync_us = now_us + scanner_full_resync_interval_us();
|
||||
} else if (next_full_resync_us == 0) {
|
||||
next_full_resync_us = now_us;
|
||||
}
|
||||
|
||||
if (config_probe_due(now_us)) {
|
||||
@@ -1504,7 +1526,7 @@ void sm_scanner_run_loop(void) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!game_mount_busy && g_scanner_manual_scan_due_us != 0 &&
|
||||
if (g_scanner_manual_scan_due_us != 0 &&
|
||||
now_us >= g_scanner_manual_scan_due_us) {
|
||||
g_scanner_manual_scan_due_us = 0;
|
||||
invalidate_app_db_title_cache();
|
||||
@@ -1540,8 +1562,7 @@ void sm_scanner_run_loop(void) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!game_mount_busy && next_full_resync_us != 0 &&
|
||||
now_us >= next_full_resync_us) {
|
||||
if (next_full_resync_us != 0 && now_us >= next_full_resync_us) {
|
||||
bool unstable_found = false;
|
||||
if (!run_full_scan_cycle(false, NULL, &unstable_found)) {
|
||||
if (runtime_sleep_mode_active())
|
||||
@@ -1567,16 +1588,14 @@ void sm_scanner_run_loop(void) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int cleanup_root_index =
|
||||
game_mount_busy ? -1 : find_pending_cleanup_scan_root();
|
||||
int cleanup_root_index = find_pending_cleanup_scan_root();
|
||||
if (cleanup_root_index >= 0) {
|
||||
g_scanner_root_states[cleanup_root_index].cleanup_pending = false;
|
||||
cleanup_lost_sources_for_scan_root(get_scan_path(cleanup_root_index));
|
||||
continue;
|
||||
}
|
||||
|
||||
int dirty_root_index =
|
||||
game_mount_busy ? -1 : find_due_dirty_scan_root(now_us);
|
||||
int dirty_root_index = find_due_dirty_scan_root(now_us);
|
||||
if (dirty_root_index >= 0) {
|
||||
bool cleanup_pending =
|
||||
g_scanner_root_states[dirty_root_index].cleanup_pending;
|
||||
@@ -1639,8 +1658,8 @@ void sm_scanner_run_loop(void) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uint64_t deadline_us = compute_next_scan_deadline_us(
|
||||
now_us, next_full_resync_us, !game_mount_busy);
|
||||
uint64_t deadline_us =
|
||||
compute_next_scan_deadline_us(now_us, next_full_resync_us);
|
||||
struct timespec timeout;
|
||||
const struct timespec *timeout_ptr =
|
||||
build_wait_timeout(&timeout, now_us, deadline_us);
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
/* Position-independent bridge copied into SceShellCore executable memory. */
|
||||
.text
|
||||
.p2align 4
|
||||
.global sm_shellcore_bridge_blob_start
|
||||
.global sm_shellcore_bridge_blob_end
|
||||
.global sm_shellcore_bridge_launch_hook
|
||||
.global sm_shellcore_bridge_install_all_hook
|
||||
.global sm_shellcore_bridge_launch_trampoline
|
||||
.global sm_shellcore_bridge_install_all_trampoline
|
||||
.global sm_shellcore_bridge_install_title_dir
|
||||
.global sm_shellcore_bridge_install_armed
|
||||
.global sm_shellcore_bridge_install_title_id
|
||||
.global sm_shellcore_bridge_install_dir
|
||||
.global sm_shellcore_bridge_socket
|
||||
.global sm_shellcore_bridge_connect
|
||||
.global sm_shellcore_bridge_read
|
||||
.global sm_shellcore_bridge_write
|
||||
.global sm_shellcore_bridge_close
|
||||
|
||||
sm_shellcore_bridge_blob_start:
|
||||
sm_shellcore_bridge_launch_hook:
|
||||
pushq %rbx
|
||||
pushq %r12
|
||||
movq %rsi, %r12
|
||||
pushfq
|
||||
pushq %rax
|
||||
pushq %rdi
|
||||
pushq %rsi
|
||||
pushq %rdx
|
||||
pushq %rcx
|
||||
pushq %r8
|
||||
pushq %r9
|
||||
pushq %r10
|
||||
pushq %r11
|
||||
subq $8, %rsp
|
||||
movl $1, %edi
|
||||
/* RSI still contains title_id. */
|
||||
call sm_shellcore_bridge_exchange
|
||||
addq $8, %rsp
|
||||
popq %r11
|
||||
popq %r10
|
||||
popq %r9
|
||||
popq %r8
|
||||
popq %rcx
|
||||
popq %rdx
|
||||
popq %rsi
|
||||
popq %rdi
|
||||
popq %rax
|
||||
popfq
|
||||
subq $8, %rsp
|
||||
call *sm_shellcore_bridge_launch_trampoline(%rip)
|
||||
addq $8, %rsp
|
||||
movq %rax, %rbx
|
||||
testl %eax, %eax
|
||||
jns 1f
|
||||
movl $2, %edi
|
||||
movq %r12, %rsi
|
||||
subq $8, %rsp
|
||||
call sm_shellcore_bridge_exchange
|
||||
addq $8, %rsp
|
||||
1:
|
||||
movq %rbx, %rax
|
||||
popq %r12
|
||||
popq %rbx
|
||||
ret
|
||||
|
||||
.p2align 4
|
||||
/*
|
||||
* FW 12+ removed the public AppInstallTitleDir RPC. AppInstallAll still
|
||||
* reaches this internal dispatcher, so one explicitly armed request is
|
||||
* redirected to the internal TitleDir implementation. Every ordinary
|
||||
* AppInstallAll call follows its original trampoline unchanged.
|
||||
*/
|
||||
.p2align 4
|
||||
sm_shellcore_bridge_install_all_hook:
|
||||
cmpb $0, sm_shellcore_bridge_install_armed(%rip)
|
||||
je 1f
|
||||
movb $0, sm_shellcore_bridge_install_armed(%rip)
|
||||
leaq sm_shellcore_bridge_install_title_id(%rip), %rdi
|
||||
leaq sm_shellcore_bridge_install_dir(%rip), %rsi
|
||||
movl $1, %edx
|
||||
xorl %ecx, %ecx
|
||||
jmp *sm_shellcore_bridge_install_title_dir(%rip)
|
||||
1:
|
||||
jmp *sm_shellcore_bridge_install_all_trampoline(%rip)
|
||||
|
||||
/* int exchange(uint16_t operation, const char *title_id) */
|
||||
.p2align 4
|
||||
sm_shellcore_bridge_exchange:
|
||||
pushq %rbp
|
||||
movq %rsp, %rbp
|
||||
pushq %rbx
|
||||
pushq %r12
|
||||
subq $160, %rsp
|
||||
|
||||
/* Build the fixed 40-byte request. */
|
||||
movq $0, -64(%rbp)
|
||||
movq $0, -56(%rbp)
|
||||
movq $0, -48(%rbp)
|
||||
movq $0, -40(%rbp)
|
||||
movq $0, -32(%rbp)
|
||||
movl $0x534d5343, -64(%rbp)
|
||||
movw $1, -60(%rbp)
|
||||
movw %di, -58(%rbp)
|
||||
testq %rsi, %rsi
|
||||
je 2f
|
||||
leaq -56(%rbp), %rdi
|
||||
movl $31, %ecx
|
||||
1:
|
||||
movb (%rsi), %al
|
||||
testb %al, %al
|
||||
je 2f
|
||||
movb %al, (%rdi)
|
||||
incq %rsi
|
||||
incq %rdi
|
||||
decl %ecx
|
||||
jne 1b
|
||||
2:
|
||||
/* Use libkernel_sys wrappers. Direct syscalls from SceShellCore are fatal. */
|
||||
movl $1, %edi
|
||||
movl $1, %esi
|
||||
xorl %edx, %edx
|
||||
call *sm_shellcore_bridge_socket(%rip)
|
||||
testl %eax, %eax
|
||||
js 8f
|
||||
movl %eax, %ebx
|
||||
|
||||
/* connect(fd, &sockaddr, sockaddr.sun_len) */
|
||||
movl %ebx, %edi
|
||||
leaq sm_shellcore_bridge_sockaddr(%rip), %rsi
|
||||
movl $31, %edx
|
||||
call *sm_shellcore_bridge_connect(%rip)
|
||||
testl %eax, %eax
|
||||
jne 7f
|
||||
|
||||
/* write(fd, &request, sizeof(request)) */
|
||||
movl %ebx, %edi
|
||||
leaq -64(%rbp), %rsi
|
||||
movl $40, %edx
|
||||
call *sm_shellcore_bridge_write(%rip)
|
||||
cmpq $40, %rax
|
||||
jne 7f
|
||||
|
||||
/* Only status is needed by launch and launch-failure requests. */
|
||||
movl %ebx, %edi
|
||||
leaq -68(%rbp), %rsi
|
||||
movl $4, %edx
|
||||
call *sm_shellcore_bridge_read(%rip)
|
||||
cmpq $4, %rax
|
||||
jne 7f
|
||||
movl -68(%rbp), %r12d
|
||||
jmp 5f
|
||||
7:
|
||||
movl $5, %r12d
|
||||
5:
|
||||
movl %ebx, %edi
|
||||
call *sm_shellcore_bridge_close(%rip)
|
||||
movl %r12d, %eax
|
||||
jmp 9f
|
||||
8:
|
||||
movl $5, %eax
|
||||
9:
|
||||
addq $160, %rsp
|
||||
popq %r12
|
||||
popq %rbx
|
||||
popq %rbp
|
||||
ret
|
||||
|
||||
.p2align 3
|
||||
sm_shellcore_bridge_launch_trampoline:
|
||||
.quad 0
|
||||
sm_shellcore_bridge_install_all_trampoline:
|
||||
.quad 0
|
||||
sm_shellcore_bridge_install_title_dir:
|
||||
.quad 0
|
||||
sm_shellcore_bridge_socket:
|
||||
.quad 0
|
||||
sm_shellcore_bridge_connect:
|
||||
.quad 0
|
||||
sm_shellcore_bridge_read:
|
||||
.quad 0
|
||||
sm_shellcore_bridge_write:
|
||||
.quad 0
|
||||
sm_shellcore_bridge_close:
|
||||
.quad 0
|
||||
sm_shellcore_bridge_sockaddr:
|
||||
.byte 31, 1
|
||||
.asciz "/system_tmp/shadowmount.sock"
|
||||
.zero 75
|
||||
.p2align 3
|
||||
sm_shellcore_bridge_install_armed:
|
||||
.byte 0
|
||||
sm_shellcore_bridge_install_title_id:
|
||||
.zero 32
|
||||
sm_shellcore_bridge_install_dir:
|
||||
.zero 1024
|
||||
sm_shellcore_bridge_blob_end:
|
||||
.section .note.GNU-stack,"",@progbits
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_scan.h"
|
||||
#include "sm_shellcore_flags.h"
|
||||
|
||||
#define SHELLCORE_FLAG_WAITMODE_OR 2u
|
||||
@@ -508,7 +509,11 @@ static void set_shellcore_flag_start_result(bool success) {
|
||||
}
|
||||
|
||||
static void enter_sleep_mode_and_cleanup(const char *reason) {
|
||||
(void)request_runtime_sleep_mode(true, reason);
|
||||
if (request_runtime_sleep_mode(true, reason)) {
|
||||
runtime_mount_state_lock();
|
||||
unmount_usb_sources_for_suspend();
|
||||
runtime_mount_state_unlock();
|
||||
}
|
||||
}
|
||||
|
||||
static void reset_shellcore_flag_state(shellcore_flag_monitor_t *flag) {
|
||||
|
||||
@@ -1,471 +0,0 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <signal.h>
|
||||
#include <sys/mman.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_shellcore_hooks.h"
|
||||
#include "sm_shellcore_remote.h"
|
||||
|
||||
#define SHELLCORE_BASE_HOOK_COUNT 1u
|
||||
#define SHELLCORE_MAX_HOOK_COUNT 2u
|
||||
#define MAX_HOOK_PROLOGUE_SIZE 32u
|
||||
#define ABSOLUTE_JUMP_SIZE 12u
|
||||
|
||||
/*
|
||||
* Private payload-SDK ABI from src/crt/kernel.h. The public
|
||||
* <ps5/kernel.h> intentionally omits dynlib_obj_t/kernel_dynlib_obj(), but
|
||||
* kernel_dynlib_obj() copies the complete 0x180-byte object. Keep the local
|
||||
* definition layout-checked because ShellCore bridge placement needs the
|
||||
* module bounds and its executable eh_frame area.
|
||||
*/
|
||||
typedef struct {
|
||||
uintptr_t next;
|
||||
uintptr_t path;
|
||||
uintptr_t unknown0[2];
|
||||
uint32_t refcount;
|
||||
uintptr_t handle;
|
||||
uintptr_t mapbase;
|
||||
uintptr_t mapsize;
|
||||
uintptr_t textsize;
|
||||
uintptr_t database;
|
||||
uintptr_t datasize;
|
||||
uintptr_t unknown1;
|
||||
uintptr_t unknown1size;
|
||||
uintptr_t entry;
|
||||
uintptr_t unknown2;
|
||||
uintptr_t vaddrbase;
|
||||
uint32_t tlsindex;
|
||||
uintptr_t tlsinit;
|
||||
uintptr_t tlsinitsize;
|
||||
uintptr_t tlssize;
|
||||
uintptr_t tlsoffset;
|
||||
uintptr_t tlsalign;
|
||||
uintptr_t pltgot;
|
||||
uintptr_t unknown3[6];
|
||||
uintptr_t init;
|
||||
uintptr_t fini;
|
||||
uintptr_t eh_frame_hdr;
|
||||
uintptr_t eh_frame_hdr_size;
|
||||
uintptr_t eh_frame;
|
||||
uintptr_t eh_frame_size;
|
||||
int32_t status;
|
||||
int32_t flags;
|
||||
uintptr_t unknown4[5];
|
||||
uintptr_t dynsec;
|
||||
uintptr_t unknown5[6];
|
||||
} shellcore_dynlib_obj_t;
|
||||
|
||||
_Static_assert(offsetof(shellcore_dynlib_obj_t, mapbase) == 0x30,
|
||||
"unexpected dynlib_obj mapbase offset");
|
||||
_Static_assert(offsetof(shellcore_dynlib_obj_t, eh_frame) == 0x108,
|
||||
"unexpected dynlib_obj eh_frame offset");
|
||||
_Static_assert(sizeof(shellcore_dynlib_obj_t) == 0x180,
|
||||
"unexpected dynlib_obj size");
|
||||
|
||||
int kernel_dynlib_obj(pid_t pid, uint32_t handle, shellcore_dynlib_obj_t *obj);
|
||||
|
||||
extern const uint8_t sm_shellcore_bridge_blob_start[];
|
||||
extern const uint8_t sm_shellcore_bridge_blob_end[];
|
||||
extern const uint8_t sm_shellcore_bridge_launch_hook[];
|
||||
extern const uint8_t sm_shellcore_bridge_install_all_hook[];
|
||||
extern const uint8_t sm_shellcore_bridge_launch_trampoline[];
|
||||
extern const uint8_t sm_shellcore_bridge_install_all_trampoline[];
|
||||
extern const uint8_t sm_shellcore_bridge_install_title_dir[];
|
||||
extern const uint8_t sm_shellcore_bridge_install_armed[];
|
||||
extern const uint8_t sm_shellcore_bridge_install_title_id[];
|
||||
extern const uint8_t sm_shellcore_bridge_install_dir[];
|
||||
extern const uint8_t sm_shellcore_bridge_socket[];
|
||||
extern const uint8_t sm_shellcore_bridge_connect[];
|
||||
extern const uint8_t sm_shellcore_bridge_read[];
|
||||
extern const uint8_t sm_shellcore_bridge_write[];
|
||||
extern const uint8_t sm_shellcore_bridge_close[];
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *slot;
|
||||
const char *name;
|
||||
} shellcore_import_t;
|
||||
|
||||
static const shellcore_import_t k_bridge_imports[] = {
|
||||
{sm_shellcore_bridge_socket, "socket"},
|
||||
{sm_shellcore_bridge_connect, "connect"},
|
||||
{sm_shellcore_bridge_read, "read"},
|
||||
{sm_shellcore_bridge_write, "write"},
|
||||
{sm_shellcore_bridge_close, "close"},
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
bool installed;
|
||||
sm_shellcore_remote_t remote;
|
||||
uintptr_t bridge_address;
|
||||
size_t hook_count;
|
||||
sm_shellcore_target_t target_ids[SHELLCORE_MAX_HOOK_COUNT];
|
||||
uint8_t original[SHELLCORE_MAX_HOOK_COUNT][MAX_HOOK_PROLOGUE_SIZE];
|
||||
uint8_t original_size[SHELLCORE_MAX_HOOK_COUNT];
|
||||
} shellcore_hooks_state_t;
|
||||
|
||||
static shellcore_hooks_state_t g_hooks;
|
||||
static pthread_mutex_t g_install_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static uintptr_t remote_bridge_symbol(const uint8_t *symbol) {
|
||||
return g_hooks.bridge_address +
|
||||
(uintptr_t)(symbol - sm_shellcore_bridge_blob_start);
|
||||
}
|
||||
|
||||
static void build_absolute_jump(uint8_t jump[ABSOLUTE_JUMP_SIZE],
|
||||
uintptr_t destination) {
|
||||
jump[0] = 0x48;
|
||||
jump[1] = 0xb8;
|
||||
memcpy(jump + 2, &destination, sizeof(destination));
|
||||
jump[10] = 0xff;
|
||||
jump[11] = 0xe0;
|
||||
}
|
||||
|
||||
static bool patch_remote_jump(pid_t pid, uintptr_t source,
|
||||
uintptr_t destination, size_t patch_size) {
|
||||
if (patch_size < ABSOLUTE_JUMP_SIZE || patch_size > MAX_HOOK_PROLOGUE_SIZE) {
|
||||
return false;
|
||||
}
|
||||
uint8_t patch[MAX_HOOK_PROLOGUE_SIZE];
|
||||
memset(patch, 0x90, patch_size);
|
||||
build_absolute_jump(patch, destination);
|
||||
if (!sm_remote_process_write(pid, source, patch, patch_size))
|
||||
return false;
|
||||
uint8_t verify[MAX_HOOK_PROLOGUE_SIZE];
|
||||
return sm_remote_process_read(pid, source, verify, patch_size) &&
|
||||
memcmp(verify, patch, patch_size) == 0;
|
||||
}
|
||||
|
||||
static bool verify_remote_bytes(pid_t pid, uintptr_t address,
|
||||
const void *expected, size_t size) {
|
||||
uint8_t buffer[256];
|
||||
const uint8_t *bytes = (const uint8_t *)expected;
|
||||
while (size != 0) {
|
||||
size_t chunk = size < sizeof(buffer) ? size : sizeof(buffer);
|
||||
if (!sm_remote_process_read(pid, address, buffer, chunk) ||
|
||||
memcmp(buffer, bytes, chunk) != 0) {
|
||||
return false;
|
||||
}
|
||||
address += chunk;
|
||||
bytes += chunk;
|
||||
size -= chunk;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool append_trampoline(uint8_t *bridge, size_t bridge_capacity,
|
||||
size_t *cursor, const uint8_t *original,
|
||||
size_t original_size, uintptr_t return_address,
|
||||
uintptr_t remote_base,
|
||||
uintptr_t *remote_trampoline_out) {
|
||||
size_t aligned = (*cursor + 15u) & ~(size_t)15u;
|
||||
size_t required = aligned + original_size + ABSOLUTE_JUMP_SIZE;
|
||||
if (required > bridge_capacity)
|
||||
return false;
|
||||
memcpy(bridge + aligned, original, original_size);
|
||||
build_absolute_jump(bridge + aligned + original_size, return_address);
|
||||
*remote_trampoline_out = remote_base + aligned;
|
||||
*cursor = required;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void set_bridge_pointer(uint8_t *bridge, const uint8_t *local_symbol,
|
||||
uintptr_t value) {
|
||||
size_t offset = (size_t)(local_symbol - sm_shellcore_bridge_blob_start);
|
||||
memcpy(bridge + offset, &value, sizeof(value));
|
||||
}
|
||||
|
||||
static bool resolve_bridge_imports(pid_t pid, uint8_t *bridge) {
|
||||
uint32_t handle = UINT32_MAX;
|
||||
if (kernel_dynlib_handle(pid, "libkernel_sys.sprx", &handle) != 0)
|
||||
return false;
|
||||
|
||||
for (size_t i = 0; i < sizeof(k_bridge_imports) / sizeof(k_bridge_imports[0]);
|
||||
++i) {
|
||||
uintptr_t address =
|
||||
(uintptr_t)kernel_dynlib_dlsym(pid, handle, k_bridge_imports[i].name);
|
||||
if (address == 0) {
|
||||
log_debug(" [SHELLCORE] failed to resolve libkernel %s",
|
||||
k_bridge_imports[i].name);
|
||||
return false;
|
||||
}
|
||||
set_bridge_pointer(bridge, k_bridge_imports[i].slot, address);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static pid_t find_shellcore_pid(void) {
|
||||
pid_t pid = find_pid_by_name("SceShellCore", true);
|
||||
if (pid <= 0)
|
||||
pid = find_pid_by_name("SceShellCore.elf", true);
|
||||
return pid;
|
||||
}
|
||||
|
||||
bool sm_shellcore_install_bridge_enabled(void) {
|
||||
uint16_t firmware =
|
||||
(uint16_t)((kernel_get_fw_version() >> 16) & 0xffffu);
|
||||
return firmware >= 0x1200u;
|
||||
}
|
||||
|
||||
static bool install_hooks_for_pid(pid_t pid) {
|
||||
const size_t blob_size =
|
||||
(size_t)(sm_shellcore_bridge_blob_end - sm_shellcore_bridge_blob_start);
|
||||
const size_t bridge_capacity =
|
||||
blob_size + SHELLCORE_MAX_HOOK_COUNT *
|
||||
(MAX_HOOK_PROLOGUE_SIZE + ABSOLUTE_JUMP_SIZE + 16u);
|
||||
uint8_t *bridge = calloc(1, bridge_capacity);
|
||||
if (!bridge)
|
||||
return false;
|
||||
memcpy(bridge, sm_shellcore_bridge_blob_start, blob_size);
|
||||
|
||||
if (kill(pid, SIGSTOP) != 0) {
|
||||
free(bridge);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
sm_shellcore_remote_t remote;
|
||||
shellcore_dynlib_obj_t object;
|
||||
if (!sm_shellcore_remote_resolve(pid, &remote) ||
|
||||
kernel_dynlib_obj(pid, 0, &object) != 0) {
|
||||
log_debug(" [SHELLCORE] stopped-process layout resolution failed");
|
||||
goto done;
|
||||
}
|
||||
if (object.mapbase != remote.image_base ||
|
||||
object.eh_frame < object.mapbase ||
|
||||
object.eh_frame_size < bridge_capacity ||
|
||||
object.eh_frame > UINTPTR_MAX - bridge_capacity ||
|
||||
object.eh_frame + bridge_capacity > object.mapbase + object.mapsize) {
|
||||
log_debug(" [SHELLCORE] invalid layout: base=0x%lx map=0x%lx+0x%lx "
|
||||
"eh=0x%lx+0x%lx bridge=0x%zx",
|
||||
(unsigned long)remote.image_base, (unsigned long)object.mapbase,
|
||||
(unsigned long)object.mapsize, (unsigned long)object.eh_frame,
|
||||
(unsigned long)object.eh_frame_size, bridge_capacity);
|
||||
goto done;
|
||||
}
|
||||
log_debug(" [SHELLCORE] layout validated: map=0x%lx+0x%lx eh=0x%lx+0x%lx",
|
||||
(unsigned long)object.mapbase, (unsigned long)object.mapsize,
|
||||
(unsigned long)object.eh_frame,
|
||||
(unsigned long)object.eh_frame_size);
|
||||
|
||||
const bool install_hook_enabled = sm_shellcore_install_bridge_enabled();
|
||||
const size_t hook_count = install_hook_enabled
|
||||
? SHELLCORE_MAX_HOOK_COUNT
|
||||
: SHELLCORE_BASE_HOOK_COUNT;
|
||||
uintptr_t targets[SHELLCORE_MAX_HOOK_COUNT] = {
|
||||
remote.targets[SM_SHELLCORE_TARGET_LAUNCH_APP],
|
||||
remote.targets[SM_SHELLCORE_TARGET_INSTALL_ALL],
|
||||
};
|
||||
sm_shellcore_target_t target_ids[SHELLCORE_MAX_HOOK_COUNT] = {
|
||||
SM_SHELLCORE_TARGET_LAUNCH_APP,
|
||||
SM_SHELLCORE_TARGET_INSTALL_ALL,
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < hook_count; ++i) {
|
||||
uint8_t patch_size = remote.offsets->targets[target_ids[i]].patch_size;
|
||||
if (patch_size < ABSOLUTE_JUMP_SIZE ||
|
||||
patch_size > MAX_HOOK_PROLOGUE_SIZE ||
|
||||
!sm_remote_process_read(pid, targets[i], g_hooks.original[i],
|
||||
patch_size)) {
|
||||
goto done;
|
||||
}
|
||||
static const uint8_t function_prologue[] = {0x55, 0x48, 0x89, 0xe5};
|
||||
if (memcmp(g_hooks.original[i], function_prologue,
|
||||
sizeof(function_prologue)) != 0) {
|
||||
log_debug(" [SHELLCORE] unexpected prologue: %s at 0x%lx",
|
||||
sm_shellcore_target_name(target_ids[i]),
|
||||
(unsigned long)targets[i]);
|
||||
goto done;
|
||||
}
|
||||
g_hooks.original_size[i] = patch_size;
|
||||
}
|
||||
|
||||
size_t cursor = blob_size;
|
||||
uintptr_t launch_trampoline = 0;
|
||||
uintptr_t install_all_trampoline = 0;
|
||||
if (!append_trampoline(bridge, bridge_capacity, &cursor, g_hooks.original[0],
|
||||
g_hooks.original_size[0],
|
||||
targets[0] + g_hooks.original_size[0], object.eh_frame,
|
||||
&launch_trampoline)) {
|
||||
goto done;
|
||||
}
|
||||
set_bridge_pointer(bridge, sm_shellcore_bridge_launch_trampoline,
|
||||
launch_trampoline);
|
||||
if (install_hook_enabled) {
|
||||
if (!append_trampoline(bridge, bridge_capacity, &cursor,
|
||||
g_hooks.original[1], g_hooks.original_size[1],
|
||||
targets[1] + g_hooks.original_size[1],
|
||||
object.eh_frame, &install_all_trampoline)) {
|
||||
goto done;
|
||||
}
|
||||
set_bridge_pointer(bridge, sm_shellcore_bridge_install_all_trampoline,
|
||||
install_all_trampoline);
|
||||
set_bridge_pointer(
|
||||
bridge, sm_shellcore_bridge_install_title_dir,
|
||||
remote.targets[SM_SHELLCORE_TARGET_INSTALL_TITLE_DIR]);
|
||||
}
|
||||
if (!resolve_bridge_imports(pid, bridge))
|
||||
goto done;
|
||||
|
||||
if (kernel_mprotect(pid, (intptr_t)object.mapbase, object.mapsize,
|
||||
PROT_READ | PROT_WRITE | PROT_EXEC) != 0 ||
|
||||
!sm_remote_process_write(pid, object.eh_frame, bridge, cursor) ||
|
||||
!verify_remote_bytes(pid, object.eh_frame, bridge, cursor)) {
|
||||
log_debug(" [SHELLCORE] bridge write verification failed");
|
||||
goto done;
|
||||
}
|
||||
|
||||
uintptr_t launch_hook =
|
||||
object.eh_frame + (uintptr_t)(sm_shellcore_bridge_launch_hook -
|
||||
sm_shellcore_bridge_blob_start);
|
||||
uintptr_t install_all_hook =
|
||||
object.eh_frame +
|
||||
(uintptr_t)(sm_shellcore_bridge_install_all_hook -
|
||||
sm_shellcore_bridge_blob_start);
|
||||
if (!patch_remote_jump(pid, targets[0], launch_hook,
|
||||
g_hooks.original_size[0]) ||
|
||||
(install_hook_enabled &&
|
||||
!patch_remote_jump(pid, targets[1], install_all_hook,
|
||||
g_hooks.original_size[1]))) {
|
||||
goto rollback;
|
||||
}
|
||||
|
||||
g_hooks.remote = remote;
|
||||
g_hooks.bridge_address = object.eh_frame;
|
||||
g_hooks.hook_count = hook_count;
|
||||
memcpy(g_hooks.target_ids, target_ids,
|
||||
hook_count * sizeof(g_hooks.target_ids[0]));
|
||||
g_hooks.installed = true;
|
||||
ok = true;
|
||||
log_debug(" [SHELLCORE] hooks installed: fw=%s pid=%ld lifecycle=1 "
|
||||
"install=%d",
|
||||
remote.offsets->name, (long)pid, install_hook_enabled ? 1 : 0);
|
||||
goto done;
|
||||
|
||||
rollback:
|
||||
for (size_t i = 0; i < hook_count; ++i) {
|
||||
(void)sm_remote_process_write(pid, targets[i], g_hooks.original[i],
|
||||
g_hooks.original_size[i]);
|
||||
}
|
||||
|
||||
done:
|
||||
free(bridge);
|
||||
(void)kill(pid, SIGCONT);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool sm_shellcore_hooks_start(void) {
|
||||
pthread_mutex_lock(&g_install_mutex);
|
||||
if (g_hooks.installed)
|
||||
goto installed;
|
||||
memset(&g_hooks, 0, sizeof(g_hooks));
|
||||
pid_t pid = find_shellcore_pid();
|
||||
if (pid <= 0) {
|
||||
log_debug(" [SHELLCORE] process not found");
|
||||
pthread_mutex_unlock(&g_install_mutex);
|
||||
return false;
|
||||
}
|
||||
bool ok = install_hooks_for_pid(pid);
|
||||
pthread_mutex_unlock(&g_install_mutex);
|
||||
return ok;
|
||||
|
||||
installed:
|
||||
pthread_mutex_unlock(&g_install_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
void sm_shellcore_hooks_stop(void) {
|
||||
pthread_mutex_lock(&g_install_mutex);
|
||||
if (!g_hooks.installed) {
|
||||
pthread_mutex_unlock(&g_install_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
pid_t pid = g_hooks.remote.pid;
|
||||
if (kill(pid, SIGSTOP) == 0) {
|
||||
for (size_t i = 0; i < g_hooks.hook_count; ++i) {
|
||||
sm_shellcore_target_t target = g_hooks.target_ids[i];
|
||||
(void)sm_remote_process_write(pid, g_hooks.remote.targets[target],
|
||||
g_hooks.original[i],
|
||||
g_hooks.original_size[i]);
|
||||
}
|
||||
(void)kill(pid, SIGCONT);
|
||||
}
|
||||
memset(&g_hooks, 0, sizeof(g_hooks));
|
||||
pthread_mutex_unlock(&g_install_mutex);
|
||||
}
|
||||
|
||||
bool sm_shellcore_install_title_dir(const char *title_id,
|
||||
const char *install_dir,
|
||||
int *result_out) {
|
||||
if (!title_id || !install_dir || !result_out)
|
||||
return false;
|
||||
|
||||
size_t title_len = strnlen(title_id, MAX_TITLE_ID);
|
||||
size_t dir_len = strnlen(install_dir, MAX_PATH);
|
||||
if (title_len == 0 || title_len >= MAX_TITLE_ID || dir_len == 0 ||
|
||||
dir_len >= MAX_PATH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_install_mutex);
|
||||
if (!g_hooks.installed ||
|
||||
g_hooks.hook_count < SHELLCORE_MAX_HOOK_COUNT) {
|
||||
pthread_mutex_unlock(&g_install_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
char remote_title[MAX_TITLE_ID] = {0};
|
||||
char remote_dir[MAX_PATH] = {0};
|
||||
memcpy(remote_title, title_id, title_len);
|
||||
memcpy(remote_dir, install_dir, dir_len);
|
||||
|
||||
pid_t pid = g_hooks.remote.pid;
|
||||
uint8_t armed = 1;
|
||||
bool written =
|
||||
sm_remote_process_write(pid,
|
||||
remote_bridge_symbol(
|
||||
sm_shellcore_bridge_install_title_id),
|
||||
remote_title, sizeof(remote_title)) &&
|
||||
sm_remote_process_write(
|
||||
pid, remote_bridge_symbol(sm_shellcore_bridge_install_dir),
|
||||
remote_dir, sizeof(remote_dir)) &&
|
||||
sm_remote_process_write(
|
||||
pid, remote_bridge_symbol(sm_shellcore_bridge_install_armed),
|
||||
&armed, sizeof(armed));
|
||||
if (!written) {
|
||||
armed = 0;
|
||||
(void)sm_remote_process_write(
|
||||
pid, remote_bridge_symbol(sm_shellcore_bridge_install_armed), &armed,
|
||||
sizeof(armed));
|
||||
pthread_mutex_unlock(&g_install_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
int result = sceAppInstUtilAppInstallAll(NULL);
|
||||
uint8_t remaining = 1;
|
||||
bool read_back = sm_remote_process_read(
|
||||
pid, remote_bridge_symbol(sm_shellcore_bridge_install_armed), &remaining,
|
||||
sizeof(remaining));
|
||||
bool consumed = read_back && remaining == 0;
|
||||
if (!consumed) {
|
||||
armed = 0;
|
||||
(void)sm_remote_process_write(
|
||||
pid, remote_bridge_symbol(sm_shellcore_bridge_install_armed), &armed,
|
||||
sizeof(armed));
|
||||
}
|
||||
pthread_mutex_unlock(&g_install_mutex);
|
||||
|
||||
if (!consumed) {
|
||||
log_debug(" [SHELLCORE] AppInstallAll trigger was not consumed: %s",
|
||||
title_id);
|
||||
return false;
|
||||
}
|
||||
log_debug(" [SHELLCORE] AppInstallAll bridge consumed: %s result=0x%08x",
|
||||
title_id, (uint32_t)result);
|
||||
*result_out = result;
|
||||
return true;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
#include "sm_shellcore_offsets.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
static const sm_shellcore_firmware_offsets_t g_shellcore_offsets[] = {
|
||||
#include "sm_shellcore_offsets.inc"
|
||||
};
|
||||
|
||||
const sm_shellcore_firmware_offsets_t *
|
||||
sm_shellcore_offsets_for_firmware(uint32_t raw_firmware) {
|
||||
uint16_t firmware = (uint16_t)((raw_firmware >> 16) & 0xffffu);
|
||||
for (size_t i = 0;
|
||||
i < sizeof(g_shellcore_offsets) / sizeof(g_shellcore_offsets[0]); ++i) {
|
||||
if (g_shellcore_offsets[i].firmware == firmware)
|
||||
return &g_shellcore_offsets[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *sm_shellcore_target_name(sm_shellcore_target_t target) {
|
||||
switch (target) {
|
||||
case SM_SHELLCORE_TARGET_LAUNCH_APP:
|
||||
return "launchApp";
|
||||
case SM_SHELLCORE_TARGET_UNMOUNT_WORKSPACE:
|
||||
return "unmountWorkspace";
|
||||
case SM_SHELLCORE_TARGET_INSTALL_TITLE_DIR:
|
||||
return "AppInstallTitleDirMain";
|
||||
case SM_SHELLCORE_TARGET_INSTALL_ALL:
|
||||
return "AppInstallAll";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
{
|
||||
.firmware = 0x0100u,
|
||||
.name = "1.00",
|
||||
.targets = {{.offset = 0x3465a0u, .patch_size = 12u}, {.offset = 0x3479b0u, .patch_size = 12u}, {.offset = 0x1adc50u, .patch_size = 12u}, {.offset = 0xa05c40u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1000u,
|
||||
.name = "10.00",
|
||||
.targets = {{.offset = 0x5ca1a0u, .patch_size = 12u}, {.offset = 0x605140u, .patch_size = 13u}, {.offset = 0x2b6700u, .patch_size = 12u}, {.offset = 0x17d1f00u, .patch_size = 16u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1001u,
|
||||
.name = "10.01",
|
||||
.targets = {{.offset = 0x5ca1a0u, .patch_size = 12u}, {.offset = 0x605140u, .patch_size = 13u}, {.offset = 0x2b6700u, .patch_size = 12u}, {.offset = 0x17d1f00u, .patch_size = 16u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0101u,
|
||||
.name = "1.01",
|
||||
.targets = {{.offset = 0x3465a0u, .patch_size = 12u}, {.offset = 0x3479b0u, .patch_size = 12u}, {.offset = 0x1adc50u, .patch_size = 12u}, {.offset = 0xa05c40u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0102u,
|
||||
.name = "1.02",
|
||||
.targets = {{.offset = 0x3465a0u, .patch_size = 12u}, {.offset = 0x3479b0u, .patch_size = 12u}, {.offset = 0x1adc50u, .patch_size = 12u}, {.offset = 0xa05c40u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1020u,
|
||||
.name = "10.20",
|
||||
.targets = {{.offset = 0x5ca1a0u, .patch_size = 12u}, {.offset = 0x605140u, .patch_size = 13u}, {.offset = 0x2b6700u, .patch_size = 12u}, {.offset = 0x17d61c0u, .patch_size = 16u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1040u,
|
||||
.name = "10.40",
|
||||
.targets = {{.offset = 0x5ca0f0u, .patch_size = 12u}, {.offset = 0x605090u, .patch_size = 13u}, {.offset = 0x2b6760u, .patch_size = 12u}, {.offset = 0x17d61e0u, .patch_size = 16u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1060u,
|
||||
.name = "10.60",
|
||||
.targets = {{.offset = 0x5cb980u, .patch_size = 12u}, {.offset = 0x606920u, .patch_size = 13u}, {.offset = 0x2b80c0u, .patch_size = 12u}, {.offset = 0x17d7a60u, .patch_size = 16u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1100u,
|
||||
.name = "11.00",
|
||||
.targets = {{.offset = 0x62a940u, .patch_size = 12u}, {.offset = 0x6668d0u, .patch_size = 15u}, {.offset = 0x2bd880u, .patch_size = 12u}, {.offset = 0x2c4e60u, .patch_size = 15u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0112u,
|
||||
.name = "1.12",
|
||||
.targets = {{.offset = 0x3469b0u, .patch_size = 12u}, {.offset = 0x347dc0u, .patch_size = 12u}, {.offset = 0x1adc30u, .patch_size = 12u}, {.offset = 0xa063a0u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1120u,
|
||||
.name = "11.20",
|
||||
.targets = {{.offset = 0x62aab0u, .patch_size = 12u}, {.offset = 0x666a40u, .patch_size = 15u}, {.offset = 0x2bd930u, .patch_size = 12u}, {.offset = 0x2c4f10u, .patch_size = 15u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0114u,
|
||||
.name = "1.14",
|
||||
.targets = {{.offset = 0x3469b0u, .patch_size = 12u}, {.offset = 0x347dc0u, .patch_size = 12u}, {.offset = 0x1adc30u, .patch_size = 12u}, {.offset = 0xa06660u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1140u,
|
||||
.name = "11.40",
|
||||
.targets = {{.offset = 0x62ba90u, .patch_size = 12u}, {.offset = 0x667a20u, .patch_size = 15u}, {.offset = 0x2be910u, .patch_size = 12u}, {.offset = 0x2c5ef0u, .patch_size = 15u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1160u,
|
||||
.name = "11.60",
|
||||
.targets = {{.offset = 0x633500u, .patch_size = 12u}, {.offset = 0x66f490u, .patch_size = 15u}, {.offset = 0x2c2c10u, .patch_size = 12u}, {.offset = 0x2ca1f0u, .patch_size = 15u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1200u,
|
||||
.name = "12.00",
|
||||
.targets = {{.offset = 0x647410u, .patch_size = 12u}, {.offset = 0x683720u, .patch_size = 15u}, {.offset = 0x2e0050u, .patch_size = 12u}, {.offset = 0x2dd100u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1202u,
|
||||
.name = "12.02",
|
||||
.targets = {{.offset = 0x647410u, .patch_size = 12u}, {.offset = 0x683720u, .patch_size = 15u}, {.offset = 0x2e0050u, .patch_size = 12u}, {.offset = 0x2dd100u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1220u,
|
||||
.name = "12.20",
|
||||
.targets = {{.offset = 0x647410u, .patch_size = 12u}, {.offset = 0x683720u, .patch_size = 15u}, {.offset = 0x2e0050u, .patch_size = 12u}, {.offset = 0x2dd100u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1240u,
|
||||
.name = "12.40",
|
||||
.targets = {{.offset = 0x647410u, .patch_size = 12u}, {.offset = 0x683720u, .patch_size = 15u}, {.offset = 0x2e0050u, .patch_size = 12u}, {.offset = 0x2dd100u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1260u,
|
||||
.name = "12.60",
|
||||
.targets = {{.offset = 0x648660u, .patch_size = 12u}, {.offset = 0x684970u, .patch_size = 15u}, {.offset = 0x2e0320u, .patch_size = 12u}, {.offset = 0x2dd3d0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1270u,
|
||||
.name = "12.70",
|
||||
.targets = {{.offset = 0x648660u, .patch_size = 12u}, {.offset = 0x684970u, .patch_size = 15u}, {.offset = 0x2e0320u, .patch_size = 12u}, {.offset = 0x2dd3d0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0200u,
|
||||
.name = "2.00",
|
||||
.targets = {{.offset = 0x3a4660u, .patch_size = 12u}, {.offset = 0x3a5aa0u, .patch_size = 12u}, {.offset = 0x1df8f0u, .patch_size = 12u}, {.offset = 0xaf3740u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0220u,
|
||||
.name = "2.20",
|
||||
.targets = {{.offset = 0x3a4930u, .patch_size = 12u}, {.offset = 0x3a5d70u, .patch_size = 12u}, {.offset = 0x1dfb80u, .patch_size = 12u}, {.offset = 0xaf3aa0u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0225u,
|
||||
.name = "2.25",
|
||||
.targets = {{.offset = 0x3a4e80u, .patch_size = 12u}, {.offset = 0x3a62c0u, .patch_size = 12u}, {.offset = 0x1dfb80u, .patch_size = 12u}, {.offset = 0xaf3fe0u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0226u,
|
||||
.name = "2.26",
|
||||
.targets = {{.offset = 0x3a65f0u, .patch_size = 12u}, {.offset = 0x3a7a30u, .patch_size = 12u}, {.offset = 0x1e0d50u, .patch_size = 12u}, {.offset = 0xaf57a0u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0230u,
|
||||
.name = "2.30",
|
||||
.targets = {{.offset = 0x3a6d90u, .patch_size = 12u}, {.offset = 0x3a81d0u, .patch_size = 12u}, {.offset = 0x1e0f00u, .patch_size = 12u}, {.offset = 0xaf6360u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0250u,
|
||||
.name = "2.50",
|
||||
.targets = {{.offset = 0x3a8140u, .patch_size = 12u}, {.offset = 0x3a9580u, .patch_size = 12u}, {.offset = 0x1e0ca0u, .patch_size = 12u}, {.offset = 0xaf7700u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0270u,
|
||||
.name = "2.70",
|
||||
.targets = {{.offset = 0x3a8140u, .patch_size = 12u}, {.offset = 0x3a9580u, .patch_size = 12u}, {.offset = 0x1e0ca0u, .patch_size = 12u}, {.offset = 0xaf7700u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0300u,
|
||||
.name = "3.00",
|
||||
.targets = {{.offset = 0x3f1960u, .patch_size = 12u}, {.offset = 0x3f2ef0u, .patch_size = 12u}, {.offset = 0x2138e0u, .patch_size = 12u}, {.offset = 0x212ab0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0310u,
|
||||
.name = "3.10",
|
||||
.targets = {{.offset = 0x3f19a0u, .patch_size = 12u}, {.offset = 0x3f2f30u, .patch_size = 12u}, {.offset = 0x213920u, .patch_size = 12u}, {.offset = 0x212af0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0320u,
|
||||
.name = "3.20",
|
||||
.targets = {{.offset = 0x3f1a50u, .patch_size = 12u}, {.offset = 0x3f2fe0u, .patch_size = 12u}, {.offset = 0x2139d0u, .patch_size = 12u}, {.offset = 0x212ba0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0321u,
|
||||
.name = "3.21",
|
||||
.targets = {{.offset = 0x3f1a50u, .patch_size = 12u}, {.offset = 0x3f2fe0u, .patch_size = 12u}, {.offset = 0x2139d0u, .patch_size = 12u}, {.offset = 0x212ba0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0400u,
|
||||
.name = "4.00",
|
||||
.targets = {{.offset = 0x432630u, .patch_size = 12u}, {.offset = 0x433d90u, .patch_size = 12u}, {.offset = 0x226c20u, .patch_size = 12u}, {.offset = 0x225e40u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0403u,
|
||||
.name = "4.03",
|
||||
.targets = {{.offset = 0x432630u, .patch_size = 12u}, {.offset = 0x433d90u, .patch_size = 12u}, {.offset = 0x226c20u, .patch_size = 12u}, {.offset = 0x225e40u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0450u,
|
||||
.name = "4.50",
|
||||
.targets = {{.offset = 0x432d80u, .patch_size = 12u}, {.offset = 0x4344e0u, .patch_size = 12u}, {.offset = 0x227350u, .patch_size = 12u}, {.offset = 0x226570u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0451u,
|
||||
.name = "4.51",
|
||||
.targets = {{.offset = 0x432d80u, .patch_size = 12u}, {.offset = 0x4344e0u, .patch_size = 12u}, {.offset = 0x227350u, .patch_size = 12u}, {.offset = 0x226570u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0500u,
|
||||
.name = "5.00",
|
||||
.targets = {{.offset = 0x498950u, .patch_size = 12u}, {.offset = 0x49a0a0u, .patch_size = 12u}, {.offset = 0x2556a0u, .patch_size = 12u}, {.offset = 0x254900u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0502u,
|
||||
.name = "5.02",
|
||||
.targets = {{.offset = 0x498940u, .patch_size = 12u}, {.offset = 0x49a090u, .patch_size = 12u}, {.offset = 0x2556a0u, .patch_size = 12u}, {.offset = 0x254900u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0510u,
|
||||
.name = "5.10",
|
||||
.targets = {{.offset = 0x4998f0u, .patch_size = 12u}, {.offset = 0x49b650u, .patch_size = 12u}, {.offset = 0x256570u, .patch_size = 12u}, {.offset = 0x2557d0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0550u,
|
||||
.name = "5.50",
|
||||
.targets = {{.offset = 0x4998f0u, .patch_size = 12u}, {.offset = 0x49b650u, .patch_size = 12u}, {.offset = 0x256570u, .patch_size = 12u}, {.offset = 0x2557d0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0600u,
|
||||
.name = "6.00",
|
||||
.targets = {{.offset = 0x4c7380u, .patch_size = 12u}, {.offset = 0x4fdf70u, .patch_size = 15u}, {.offset = 0x26b9b0u, .patch_size = 12u}, {.offset = 0x271bb0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0602u,
|
||||
.name = "6.02",
|
||||
.targets = {{.offset = 0x4c7380u, .patch_size = 12u}, {.offset = 0x4fdf70u, .patch_size = 15u}, {.offset = 0x26b9b0u, .patch_size = 12u}, {.offset = 0x271bb0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0650u,
|
||||
.name = "6.50",
|
||||
.targets = {{.offset = 0x4c73f0u, .patch_size = 12u}, {.offset = 0x4fdfe0u, .patch_size = 15u}, {.offset = 0x26ba20u, .patch_size = 12u}, {.offset = 0x271c20u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0700u,
|
||||
.name = "7.00",
|
||||
.targets = {{.offset = 0x568920u, .patch_size = 12u}, {.offset = 0x5a85d0u, .patch_size = 15u}, {.offset = 0x2852e0u, .patch_size = 12u}, {.offset = 0x28b690u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0701u,
|
||||
.name = "7.01",
|
||||
.targets = {{.offset = 0x568920u, .patch_size = 12u}, {.offset = 0x5a85d0u, .patch_size = 15u}, {.offset = 0x2852e0u, .patch_size = 12u}, {.offset = 0x28b690u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0720u,
|
||||
.name = "7.20",
|
||||
.targets = {{.offset = 0x568940u, .patch_size = 12u}, {.offset = 0x5a85f0u, .patch_size = 15u}, {.offset = 0x2852e0u, .patch_size = 12u}, {.offset = 0x28b690u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0740u,
|
||||
.name = "7.40",
|
||||
.targets = {{.offset = 0x56d430u, .patch_size = 12u}, {.offset = 0x5ad0e0u, .patch_size = 15u}, {.offset = 0x2896b0u, .patch_size = 12u}, {.offset = 0x28fa60u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0760u,
|
||||
.name = "7.60",
|
||||
.targets = {{.offset = 0x56d430u, .patch_size = 12u}, {.offset = 0x5ad0e0u, .patch_size = 15u}, {.offset = 0x2896b0u, .patch_size = 12u}, {.offset = 0x28fa60u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0761u,
|
||||
.name = "7.61",
|
||||
.targets = {{.offset = 0x56d430u, .patch_size = 12u}, {.offset = 0x5ad0e0u, .patch_size = 15u}, {.offset = 0x2896b0u, .patch_size = 12u}, {.offset = 0x28fa60u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0800u,
|
||||
.name = "8.00",
|
||||
.targets = {{.offset = 0x597eb0u, .patch_size = 12u}, {.offset = 0x5d3060u, .patch_size = 13u}, {.offset = 0x29a730u, .patch_size = 12u}, {.offset = 0x2a12f0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0820u,
|
||||
.name = "8.20",
|
||||
.targets = {{.offset = 0x598f80u, .patch_size = 12u}, {.offset = 0x5d4130u, .patch_size = 13u}, {.offset = 0x29a9a0u, .patch_size = 12u}, {.offset = 0x2a1560u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0840u,
|
||||
.name = "8.40",
|
||||
.targets = {{.offset = 0x598f80u, .patch_size = 12u}, {.offset = 0x5d4130u, .patch_size = 13u}, {.offset = 0x29a9a0u, .patch_size = 12u}, {.offset = 0x2a1560u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0860u,
|
||||
.name = "8.60",
|
||||
.targets = {{.offset = 0x598d60u, .patch_size = 12u}, {.offset = 0x5d3f10u, .patch_size = 13u}, {.offset = 0x29a750u, .patch_size = 12u}, {.offset = 0x2a1310u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0900u,
|
||||
.name = "9.00",
|
||||
.targets = {{.offset = 0x5cc0a0u, .patch_size = 12u}, {.offset = 0x6088e0u, .patch_size = 13u}, {.offset = 0x2b74d0u, .patch_size = 12u}, {.offset = 0x2be0e0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0905u,
|
||||
.name = "9.05",
|
||||
.targets = {{.offset = 0x5cc0a0u, .patch_size = 12u}, {.offset = 0x6088e0u, .patch_size = 13u}, {.offset = 0x2b74d0u, .patch_size = 12u}, {.offset = 0x2be0e0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0920u,
|
||||
.name = "9.20",
|
||||
.targets = {{.offset = 0x5cbdc0u, .patch_size = 12u}, {.offset = 0x608600u, .patch_size = 13u}, {.offset = 0x2b74d0u, .patch_size = 12u}, {.offset = 0x2be0e0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0940u,
|
||||
.name = "9.40",
|
||||
.targets = {{.offset = 0x5cc490u, .patch_size = 12u}, {.offset = 0x608cd0u, .patch_size = 13u}, {.offset = 0x2b7ba0u, .patch_size = 12u}, {.offset = 0x2be7b0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0960u,
|
||||
.name = "9.60",
|
||||
.targets = {{.offset = 0x5cc500u, .patch_size = 12u}, {.offset = 0x608d40u, .patch_size = 13u}, {.offset = 0x2b7c10u, .patch_size = 12u}, {.offset = 0x2be820u, .patch_size = 12u}},
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <ps5/mdbg.h>
|
||||
|
||||
#include "sm_log.h"
|
||||
#include "sm_shellcore_remote.h"
|
||||
|
||||
#define X86_PAGE_FRAME 0x000ffffffffff000ull
|
||||
#define X86_PAGE_VALID 0x001ull
|
||||
#define X86_PAGE_LARGE 0x080ull
|
||||
|
||||
static uintptr_t resolve_vmspace_pmap(uintptr_t vmspace) {
|
||||
uint32_t version = kernel_get_fw_version() >> 16;
|
||||
if (version >= 0x0100u && version <= 0x0102u)
|
||||
return vmspace + 0x2c0u;
|
||||
if (version >= 0x0105u && version <= 0x0550u)
|
||||
return vmspace + 0x2e0u;
|
||||
if (version >= 0x0600u && version <= 0x1270u)
|
||||
return vmspace + 0x2e8u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool resolve_process_paging(pid_t pid, uint64_t *cr3_out,
|
||||
uint64_t *dmap_out) {
|
||||
uintptr_t process = (uintptr_t)kernel_get_proc(pid);
|
||||
if (!process)
|
||||
return false;
|
||||
uintptr_t vmspace = (uintptr_t)kernel_getlong(
|
||||
process + (uintptr_t)KERNEL_OFFSET_PROC_P_VMSPACE);
|
||||
if (!vmspace)
|
||||
return false;
|
||||
uintptr_t pmap = resolve_vmspace_pmap(vmspace);
|
||||
if (!pmap)
|
||||
return false;
|
||||
|
||||
uint64_t paging[2] = {0};
|
||||
if (kernel_copyout((intptr_t)(pmap + 32u), paging, sizeof(paging)) != 0)
|
||||
return false;
|
||||
if (!paging[0] || !paging[1])
|
||||
return false;
|
||||
|
||||
*cr3_out = paging[1];
|
||||
*dmap_out = paging[0] - paging[1];
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint64_t virtual_to_physical(uint64_t address, uint64_t dmap,
|
||||
uint64_t page_map,
|
||||
uint64_t *physical_limit_out) {
|
||||
page_map &= X86_PAGE_FRAME;
|
||||
for (int shift = 39; shift >= 12; shift -= 9) {
|
||||
uint64_t index = (address >> shift) & 0x1ffu;
|
||||
page_map = kernel_getlong((intptr_t)(dmap + page_map + index * 8u));
|
||||
if ((page_map & X86_PAGE_VALID) == 0)
|
||||
return UINT64_MAX;
|
||||
if ((page_map & X86_PAGE_LARGE) != 0 || shift == 12) {
|
||||
page_map &= (1ull << 52) - (1ull << shift);
|
||||
page_map |= address & ((1ull << shift) - 1ull);
|
||||
if (physical_limit_out)
|
||||
*physical_limit_out = (page_map | ((1ull << shift) - 1ull)) + 1ull;
|
||||
return page_map;
|
||||
}
|
||||
page_map &= X86_PAGE_FRAME;
|
||||
}
|
||||
return UINT64_MAX;
|
||||
}
|
||||
|
||||
bool sm_remote_process_read(pid_t pid, uintptr_t address, void *buffer,
|
||||
size_t size) {
|
||||
if (!buffer || size == 0)
|
||||
return false;
|
||||
return mdbg_copyout(pid, (intptr_t)address, buffer, size) == 0;
|
||||
}
|
||||
|
||||
bool sm_remote_process_write(pid_t pid, uintptr_t address, const void *buffer,
|
||||
size_t size) {
|
||||
if (!buffer || size == 0)
|
||||
return false;
|
||||
if ((kernel_get_fw_version() >> 16) <= 0x0820u)
|
||||
return mdbg_copyin(pid, buffer, (intptr_t)address, size) == 0;
|
||||
|
||||
void *probe = malloc(size);
|
||||
if (!probe)
|
||||
return false;
|
||||
bool faulted = sm_remote_process_read(pid, address, probe, size);
|
||||
free(probe);
|
||||
if (!faulted)
|
||||
return false;
|
||||
|
||||
uint64_t cr3 = 0;
|
||||
uint64_t dmap = 0;
|
||||
if (!resolve_process_paging(pid, &cr3, &dmap))
|
||||
return false;
|
||||
|
||||
const uint8_t *source = (const uint8_t *)buffer;
|
||||
while (size != 0) {
|
||||
uint64_t physical_limit = 0;
|
||||
uint64_t physical =
|
||||
virtual_to_physical(address, dmap, cr3, &physical_limit);
|
||||
if (physical == UINT64_MAX || physical_limit <= physical)
|
||||
return false;
|
||||
size_t chunk = (size_t)(physical_limit - physical);
|
||||
if (chunk > size)
|
||||
chunk = size;
|
||||
if (kernel_copyin(source, (intptr_t)(dmap + physical), chunk) != 0)
|
||||
return false;
|
||||
address += chunk;
|
||||
source += chunk;
|
||||
size -= chunk;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sm_shellcore_remote_resolve(pid_t pid, sm_shellcore_remote_t *remote_out) {
|
||||
if (pid <= 0 || !remote_out)
|
||||
return false;
|
||||
memset(remote_out, 0, sizeof(*remote_out));
|
||||
|
||||
const sm_shellcore_firmware_offsets_t *firmware =
|
||||
sm_shellcore_offsets_for_firmware(kernel_get_fw_version());
|
||||
if (!firmware) {
|
||||
log_debug(" [SHELLCORE] unsupported firmware: 0x%08x",
|
||||
kernel_get_fw_version());
|
||||
return false;
|
||||
}
|
||||
|
||||
uintptr_t image_base = (uintptr_t)kernel_dynlib_mapbase_addr(pid, 0);
|
||||
if (!image_base) {
|
||||
log_debug(" [SHELLCORE] failed to resolve image base for pid=%ld",
|
||||
(long)pid);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int target = 0; target < SM_SHELLCORE_TARGET_COUNT; ++target) {
|
||||
remote_out->targets[target] = image_base + firmware->targets[target].offset;
|
||||
}
|
||||
|
||||
remote_out->pid = pid;
|
||||
remote_out->image_base = image_base;
|
||||
remote_out->offsets = firmware;
|
||||
log_debug(" [SHELLCORE] resolved fw=%s pid=%ld base=0x%lx", firmware->name,
|
||||
(long)pid, (unsigned long)image_base);
|
||||
return true;
|
||||
}
|
||||
@@ -1,395 +0,0 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_image.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_scan.h"
|
||||
#include "sm_scanner.h"
|
||||
#include "sm_shellcore_protocol.h"
|
||||
#include "sm_shellcore_service.h"
|
||||
|
||||
_Static_assert(sizeof(sm_shellcore_request_t) == 40,
|
||||
"unexpected ShellCore request size");
|
||||
_Static_assert(sizeof(sm_shellcore_response_t) == 4,
|
||||
"unexpected ShellCore response size");
|
||||
|
||||
typedef struct {
|
||||
pthread_t thread;
|
||||
pthread_mutex_t mutex;
|
||||
bool started;
|
||||
bool stop_requested;
|
||||
int listen_fd;
|
||||
// Owner of the prepared runtime mount, not the lifecycle active-game state.
|
||||
char prepared_title_id[MAX_TITLE_ID];
|
||||
} shellcore_service_state_t;
|
||||
|
||||
static shellcore_service_state_t g_service = {
|
||||
.mutex = PTHREAD_MUTEX_INITIALIZER,
|
||||
.listen_fd = -1,
|
||||
};
|
||||
|
||||
static void publish_prepared_title(const char *title_id) {
|
||||
bool changed = false;
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
if (strcmp(g_service.prepared_title_id, title_id) != 0) {
|
||||
(void)strlcpy(g_service.prepared_title_id, title_id,
|
||||
sizeof(g_service.prepared_title_id));
|
||||
changed = true;
|
||||
}
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
if (changed)
|
||||
sm_scanner_wake();
|
||||
}
|
||||
|
||||
static void clear_prepared_title(const char *title_id) {
|
||||
bool changed = false;
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
if (strcmp(g_service.prepared_title_id, title_id) == 0) {
|
||||
g_service.prepared_title_id[0] = '\0';
|
||||
changed = true;
|
||||
}
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
if (changed)
|
||||
sm_scanner_wake();
|
||||
}
|
||||
|
||||
static bool read_full(int fd, void *buffer, size_t size) {
|
||||
uint8_t *bytes = (uint8_t *)buffer;
|
||||
while (size != 0) {
|
||||
ssize_t count = recv(fd, bytes, size, 0);
|
||||
if (count == 0)
|
||||
return false;
|
||||
if (count < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
bytes += (size_t)count;
|
||||
size -= (size_t)count;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool write_full(int fd, const void *buffer, size_t size) {
|
||||
const uint8_t *bytes = (const uint8_t *)buffer;
|
||||
while (size != 0) {
|
||||
ssize_t count = send(fd, bytes, size, 0);
|
||||
if (count == 0)
|
||||
return false;
|
||||
if (count < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
bytes += (size_t)count;
|
||||
size -= (size_t)count;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool find_required_image_layer(const char *root,
|
||||
const char *runtime_source,
|
||||
unsigned int depth,
|
||||
char image_path[MAX_PATH]) {
|
||||
DIR *dir = opendir(root);
|
||||
if (!dir)
|
||||
return false;
|
||||
|
||||
bool found = false;
|
||||
struct dirent *entry;
|
||||
while (!found && (entry = readdir(dir)) != NULL) {
|
||||
if (entry->d_name[0] == '.')
|
||||
continue;
|
||||
|
||||
char path[MAX_PATH];
|
||||
int written = snprintf(path, sizeof(path), "%s/%s", root, entry->d_name);
|
||||
if (written < 0 || (size_t)written >= sizeof(path))
|
||||
continue;
|
||||
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0)
|
||||
continue;
|
||||
if (S_ISREG(st.st_mode) &&
|
||||
is_supported_image_file_path(path, entry->d_name)) {
|
||||
char mount_point[MAX_PATH];
|
||||
get_image_mount_point_for_source(path, mount_point);
|
||||
if (path_matches_root_or_child(runtime_source, mount_point)) {
|
||||
(void)strlcpy(image_path, path, MAX_PATH);
|
||||
found = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (depth > 0 && S_ISDIR(st.st_mode))
|
||||
found = find_required_image_layer(path, runtime_source, depth - 1u,
|
||||
image_path);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return found;
|
||||
}
|
||||
|
||||
static bool repair_image_chain_for_runtime_source(
|
||||
const char *title_id, const char *runtime_source, const char *eboot_path,
|
||||
char image_chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH], size_t *image_count) {
|
||||
while (*image_count < MAX_IMAGE_CHAIN_DEPTH) {
|
||||
char mounted_root[MAX_PATH];
|
||||
get_image_mount_point_for_source(image_chain[*image_count - 1u],
|
||||
mounted_root);
|
||||
char image_path[MAX_PATH];
|
||||
if (!find_required_image_layer(mounted_root, runtime_source,
|
||||
MAX_SCAN_DEPTH, image_path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *name = strrchr(image_path, '/');
|
||||
name = name ? name + 1 : image_path;
|
||||
bool unstable = false;
|
||||
if (!maybe_mount_image_file(image_path, name, &unstable))
|
||||
return false;
|
||||
|
||||
(void)strlcpy(image_chain[*image_count], image_path, MAX_PATH);
|
||||
(*image_count)++;
|
||||
if (!write_mount_image_chain(title_id, image_chain, *image_count)) {
|
||||
log_debug(" [SHELLCORE] failed to persist repaired image chain: %s",
|
||||
title_id);
|
||||
} else {
|
||||
log_debug(" [SHELLCORE] image chain repaired: title=%s layers=%zu",
|
||||
title_id, *image_count);
|
||||
}
|
||||
|
||||
if (path_exists(eboot_path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool prepare_image_source(const char *title_id,
|
||||
const char *runtime_source) {
|
||||
char eboot_path[MAX_PATH];
|
||||
int written = snprintf(eboot_path, sizeof(eboot_path), "%s/eboot.bin",
|
||||
runtime_source);
|
||||
if (written < 0 || (size_t)written >= sizeof(eboot_path))
|
||||
return false;
|
||||
if (path_exists(eboot_path))
|
||||
return true;
|
||||
if (!is_under_image_mount_base(runtime_source))
|
||||
return false;
|
||||
|
||||
char image_chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH];
|
||||
size_t image_count = 0;
|
||||
if (!read_mount_image_chain(title_id, image_chain, &image_count))
|
||||
return false;
|
||||
// The USB filesystem can become available shortly after WORKING. Avoid
|
||||
// repeatedly entering the image attach path until its outer layer exists.
|
||||
if (!path_exists(image_chain[0]))
|
||||
return false;
|
||||
for (size_t i = 0; i < image_count; ++i) {
|
||||
const char *name = strrchr(image_chain[i], '/');
|
||||
name = name ? name + 1 : image_chain[i];
|
||||
bool unstable = false;
|
||||
if (!maybe_mount_image_file(image_chain[i], name, &unstable)) {
|
||||
log_debug(" [SHELLCORE] image chain mount failed: title=%s layer=%zu "
|
||||
"path=%s",
|
||||
title_id, i, image_chain[i]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (path_exists(eboot_path))
|
||||
return true;
|
||||
if (repair_image_chain_for_runtime_source(title_id, runtime_source,
|
||||
eboot_path, image_chain,
|
||||
&image_count))
|
||||
return true;
|
||||
log_debug(" [SHELLCORE] image chain ready but eboot missing: %s",
|
||||
eboot_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool prepare_title_runtime(const char *title_id,
|
||||
const char *source_path) {
|
||||
if (!prepare_image_source(title_id, source_path))
|
||||
return false;
|
||||
runtime_mount_state_lock();
|
||||
bool ready = !runtime_sleep_mode_active() &&
|
||||
mount_title_nullfs(title_id, source_path);
|
||||
if (ready)
|
||||
ready = mount_backport_overlay_for_title(source_path, title_id, NULL, NULL);
|
||||
runtime_mount_state_unlock();
|
||||
return ready;
|
||||
}
|
||||
|
||||
bool sm_shellcore_ensure_title_runtime(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0' || runtime_sleep_mode_active())
|
||||
return false;
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
if (!read_mount_link(title_id, source_path, sizeof(source_path)))
|
||||
return true;
|
||||
|
||||
return prepare_title_runtime(title_id, source_path);
|
||||
}
|
||||
|
||||
static int handle_launch_request(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0' || runtime_sleep_mode_active())
|
||||
return EINVAL;
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
if (!read_mount_link(title_id, source_path, sizeof(source_path)))
|
||||
return ENOENT;
|
||||
|
||||
publish_prepared_title(title_id);
|
||||
|
||||
bool ready = prepare_title_runtime(title_id, source_path);
|
||||
if (!ready) {
|
||||
clear_prepared_title(title_id);
|
||||
return EIO;
|
||||
}
|
||||
log_debug(" [SHELLCORE] launch mount ready: %s", title_id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void release_backing_image(const char *title_id) {
|
||||
char image_chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH];
|
||||
size_t image_count = 0;
|
||||
if (!read_mount_image_chain(title_id, image_chain, &image_count))
|
||||
return;
|
||||
|
||||
for (size_t layer = image_count; layer > 0; --layer) {
|
||||
(void)release_runtime_image_mount(image_chain[layer - 1u]);
|
||||
}
|
||||
}
|
||||
|
||||
bool sm_shellcore_release_title_runtime(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return false;
|
||||
runtime_mount_state_lock();
|
||||
bool released = unmount_title_runtime_layers(title_id);
|
||||
if (released)
|
||||
release_backing_image(title_id);
|
||||
runtime_mount_state_unlock();
|
||||
if (released)
|
||||
clear_prepared_title(title_id);
|
||||
return released;
|
||||
}
|
||||
|
||||
bool sm_shellcore_service_has_prepared_mount(void) {
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
bool prepared = g_service.prepared_title_id[0] != '\0';
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
static int handle_launch_failed_request(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return 0;
|
||||
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
bool prepared = strcmp(g_service.prepared_title_id, title_id) == 0;
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
if (!prepared)
|
||||
return 0;
|
||||
return sm_shellcore_release_title_runtime(title_id) ? 0 : EBUSY;
|
||||
}
|
||||
|
||||
static void handle_client(int fd) {
|
||||
sm_shellcore_request_t request;
|
||||
sm_shellcore_response_t response;
|
||||
if (!read_full(fd, &request, sizeof(request)))
|
||||
return;
|
||||
if (request.magic != SM_SHELLCORE_PROTOCOL_MAGIC ||
|
||||
request.version != SM_SHELLCORE_PROTOCOL_VERSION ||
|
||||
strnlen(request.title_id, sizeof(request.title_id)) ==
|
||||
sizeof(request.title_id)) {
|
||||
response.status = EPROTO;
|
||||
} else if (request.operation == SM_SHELLCORE_REQUEST_LAUNCH) {
|
||||
response.status = handle_launch_request(request.title_id);
|
||||
} else if (request.operation == SM_SHELLCORE_REQUEST_LAUNCH_FAILED) {
|
||||
response.status = handle_launch_failed_request(request.title_id);
|
||||
} else {
|
||||
response.status = ENOTSUP;
|
||||
}
|
||||
(void)write_full(fd, &response, sizeof(response));
|
||||
}
|
||||
|
||||
static void *service_thread_main(void *arg) {
|
||||
(void)arg;
|
||||
while (true) {
|
||||
int client = accept(g_service.listen_fd, NULL, NULL);
|
||||
if (client < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
bool stopping = g_service.stop_requested;
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
if (!stopping)
|
||||
log_debug(" [SHELLCORE] accept failed: %s", strerror(errno));
|
||||
break;
|
||||
}
|
||||
handle_client(client);
|
||||
close(client);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool sm_shellcore_service_start(void) {
|
||||
if (g_service.started)
|
||||
return true;
|
||||
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
struct sockaddr_un address;
|
||||
memset(&address, 0, sizeof(address));
|
||||
address.sun_family = AF_UNIX;
|
||||
(void)strlcpy(address.sun_path, SM_SHELLCORE_SOCKET_PATH,
|
||||
sizeof(address.sun_path));
|
||||
(void)unlink(SM_SHELLCORE_SOCKET_PATH);
|
||||
if (bind(fd, (struct sockaddr *)&address, sizeof(address)) != 0 ||
|
||||
listen(fd, 4) != 0) {
|
||||
int saved_errno = errno;
|
||||
close(fd);
|
||||
(void)unlink(SM_SHELLCORE_SOCKET_PATH);
|
||||
errno = saved_errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
g_service.listen_fd = fd;
|
||||
g_service.stop_requested = false;
|
||||
g_service.prepared_title_id[0] = '\0';
|
||||
int rc = pthread_create(&g_service.thread, NULL, service_thread_main, NULL);
|
||||
if (rc != 0) {
|
||||
close(fd);
|
||||
g_service.listen_fd = -1;
|
||||
(void)unlink(SM_SHELLCORE_SOCKET_PATH);
|
||||
errno = rc;
|
||||
return false;
|
||||
}
|
||||
g_service.started = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void sm_shellcore_service_stop(void) {
|
||||
if (!g_service.started)
|
||||
return;
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
g_service.stop_requested = true;
|
||||
int fd = g_service.listen_fd;
|
||||
g_service.listen_fd = -1;
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
if (fd >= 0)
|
||||
(void)shutdown(fd, SHUT_RDWR);
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
pthread_join(g_service.thread, NULL);
|
||||
g_service.started = false;
|
||||
g_service.stop_requested = false;
|
||||
g_service.prepared_title_id[0] = '\0';
|
||||
(void)unlink(SM_SHELLCORE_SOCKET_PATH);
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate SceShellCore lifecycle offsets from unpacked firmware files.
|
||||
|
||||
The generator identifies functions through semantic log strings, finds the
|
||||
containing function prologue and emits a compact firmware offset table. It is
|
||||
intentionally independent from IDA databases so the checked-in table can be
|
||||
reproduced from unpacked firmware files.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import struct
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from capstone import CS_ARCH_X86, CS_MODE_64, Cs
|
||||
|
||||
PROLOGUE = b"\x55\x48\x89\xe5"
|
||||
TARGET_NAMES = (
|
||||
"launch_app",
|
||||
"unmount_workspace",
|
||||
"install_title_dir",
|
||||
"install_all",
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoadSegment:
|
||||
file_offset: int
|
||||
virtual_address: int
|
||||
file_size: int
|
||||
flags: int
|
||||
|
||||
|
||||
class ShellCore:
|
||||
def __init__(self, path: Path):
|
||||
self.path = path
|
||||
self.data = path.read_bytes()
|
||||
header = struct.unpack_from("<16sHHIQQQIHHHHHH", self.data, 0)
|
||||
phoff, phentsize, phnum = header[5], header[9], header[10]
|
||||
self.loads: list[LoadSegment] = []
|
||||
self.executable: LoadSegment | None = None
|
||||
for index in range(phnum):
|
||||
values = struct.unpack_from(
|
||||
"<IIQQQQQQ", self.data, phoff + index * phentsize
|
||||
)
|
||||
p_type, flags, offset, address, _, file_size, _, _ = values
|
||||
if p_type != 1:
|
||||
continue
|
||||
segment = LoadSegment(offset, address, file_size, flags)
|
||||
self.loads.append(segment)
|
||||
if flags & 1:
|
||||
self.executable = segment
|
||||
if self.executable is None:
|
||||
raise ValueError(f"{path}: executable PT_LOAD not found")
|
||||
|
||||
def file_to_virtual(self, offset: int) -> int:
|
||||
for segment in self.loads:
|
||||
relative = offset - segment.file_offset
|
||||
if 0 <= relative < segment.file_size:
|
||||
return segment.virtual_address + relative
|
||||
raise ValueError(f"file offset 0x{offset:x} is not mapped")
|
||||
|
||||
def virtual_to_file(self, address: int) -> int:
|
||||
for segment in self.loads:
|
||||
relative = address - segment.virtual_address
|
||||
if 0 <= relative < segment.file_size:
|
||||
return segment.file_offset + relative
|
||||
raise ValueError(f"virtual address 0x{address:x} is not mapped")
|
||||
|
||||
def string_addresses(self, needle: bytes, use_string_start: bool) -> list[int]:
|
||||
result: list[int] = []
|
||||
offset = 0
|
||||
while True:
|
||||
offset = self.data.find(needle, offset)
|
||||
if offset < 0:
|
||||
return result
|
||||
string_offset = offset
|
||||
if use_string_start:
|
||||
string_offset = self.data.rfind(
|
||||
b"\0", max(0, offset - 256), offset
|
||||
) + 1
|
||||
result.append(self.file_to_virtual(string_offset))
|
||||
offset += 1
|
||||
|
||||
def rip_xrefs(self, targets: list[int], slop: int = 0) -> list[int]:
|
||||
segment = self.executable
|
||||
assert segment is not None
|
||||
code = self.data[
|
||||
segment.file_offset : segment.file_offset + segment.file_size
|
||||
]
|
||||
result: list[int] = []
|
||||
for index in range(len(code) - 7):
|
||||
if code[index] not in (0x48, 0x4C):
|
||||
continue
|
||||
if code[index + 1] not in (0x8D, 0x8B):
|
||||
continue
|
||||
if code[index + 2] & 0xC7 != 0x05:
|
||||
continue
|
||||
displacement = struct.unpack_from("<i", code, index + 3)[0]
|
||||
instruction_address = segment.virtual_address + index
|
||||
destination = instruction_address + 7 + displacement
|
||||
if any(target - slop <= destination <= target + slop for target in targets):
|
||||
result.append(instruction_address)
|
||||
return result
|
||||
|
||||
def preceding_prologue(self, address: int, window: int = 0x40000) -> int:
|
||||
end = self.virtual_to_file(address)
|
||||
start = max(self.executable.file_offset, end - window) # type: ignore[union-attr]
|
||||
found = self.data.rfind(PROLOGUE, start, end)
|
||||
if found < 0:
|
||||
raise ValueError(f"no prologue before 0x{address:x}")
|
||||
return self.file_to_virtual(found)
|
||||
|
||||
def locate_targets(self) -> dict[str, int]:
|
||||
launch_strings = self.string_addresses(b"launchApp(%s)", True)
|
||||
launch_xrefs = self.rip_xrefs(launch_strings)
|
||||
if len(launch_xrefs) != 1:
|
||||
raise ValueError(f"launchApp xrefs: {launch_xrefs!r}")
|
||||
launch = self.preceding_prologue(launch_xrefs[0])
|
||||
|
||||
unmount_strings = self.string_addresses(
|
||||
b"m_workspace.unmountWorkspace()", False
|
||||
)
|
||||
unmount_groups = {
|
||||
self.preceding_prologue(xref)
|
||||
for xref in self.rip_xrefs(unmount_strings, slop=8)
|
||||
}
|
||||
unmount_groups.discard(launch)
|
||||
if len(unmount_groups) != 1:
|
||||
raise ValueError(f"unmountWorkspace candidates: {unmount_groups!r}")
|
||||
unmount = next(iter(unmount_groups))
|
||||
|
||||
install_strings = self.string_addresses(b"AppInstallTitleDirMain", False)
|
||||
install_groups: dict[int, int] = {}
|
||||
for xref in self.rip_xrefs(install_strings, slop=8):
|
||||
prologue = self.preceding_prologue(xref)
|
||||
install_groups[prologue] = install_groups.get(prologue, 0) + 1
|
||||
if not install_groups:
|
||||
raise ValueError("AppInstallTitleDirMain xrefs not found")
|
||||
best_count = max(install_groups.values())
|
||||
best = [address for address, count in install_groups.items() if count == best_count]
|
||||
if len(best) != 1:
|
||||
raise ValueError(f"ambiguous AppInstallTitleDirMain: {install_groups!r}")
|
||||
|
||||
app_install_all_strings = self.string_addresses(b"AppInstallAll()", True)
|
||||
app_install_all_xrefs = self.rip_xrefs(app_install_all_strings)
|
||||
call_targets: dict[int, int] = {}
|
||||
for xref in app_install_all_xrefs:
|
||||
xref_file = self.virtual_to_file(xref)
|
||||
for call_file in range(xref_file - 32, xref_file):
|
||||
if self.data[call_file] != 0xE8:
|
||||
continue
|
||||
call_address = self.file_to_virtual(call_file)
|
||||
displacement = struct.unpack_from("<i", self.data, call_file + 1)[0]
|
||||
target = call_address + 5 + displacement
|
||||
call_targets[target] = call_targets.get(target, 0) + 1
|
||||
if not call_targets:
|
||||
raise ValueError("AppInstallAll call target not found")
|
||||
app_install_all = max(call_targets, key=call_targets.get)
|
||||
|
||||
return {
|
||||
"launch_app": launch,
|
||||
"unmount_workspace": unmount,
|
||||
"install_title_dir": best[0],
|
||||
"install_all": app_install_all,
|
||||
}
|
||||
|
||||
def patch_size(self, address: int, minimum: int = 12) -> int:
|
||||
offset = self.virtual_to_file(address)
|
||||
decoder = Cs(CS_ARCH_X86, CS_MODE_64)
|
||||
size = 0
|
||||
for instruction in decoder.disasm(self.data[offset : offset + 64], address):
|
||||
size += instruction.size
|
||||
if size >= minimum:
|
||||
return size
|
||||
raise ValueError(f"cannot determine patch size at 0x{address:x}")
|
||||
|
||||
def firmware_key(name: str) -> int:
|
||||
major, minor = (int(part) for part in name.split(".", 1))
|
||||
if major > 99 or minor > 99:
|
||||
raise ValueError(f"firmware version is not BCD-compatible: {name}")
|
||||
major_bcd = ((major // 10) << 4) | (major % 10)
|
||||
minor_bcd = ((minor // 10) << 4) | (minor % 10)
|
||||
return (major_bcd << 8) | minor_bcd
|
||||
|
||||
|
||||
def generate(root: Path) -> str:
|
||||
firmware_dirs = sorted(
|
||||
(path for path in root.iterdir() if (path / "system/vsh/SceShellCore.elf").is_file()),
|
||||
key=lambda path: tuple(int(part) for part in path.name.split(".")),
|
||||
)
|
||||
records: list[str] = []
|
||||
for firmware_dir in firmware_dirs:
|
||||
shellcore = ShellCore(firmware_dir / "system/vsh/SceShellCore.elf")
|
||||
targets = shellcore.locate_targets()
|
||||
records.append(
|
||||
" {\n"
|
||||
f" .firmware = 0x{firmware_key(firmware_dir.name):04x}u,\n"
|
||||
f" .name = \"{firmware_dir.name}\",\n"
|
||||
" .targets = {"
|
||||
+ ", ".join(
|
||||
"{.offset = "
|
||||
f"0x{targets[name]:x}u, .patch_size = "
|
||||
f"{shellcore.patch_size(targets[name])}u}}"
|
||||
for name in TARGET_NAMES
|
||||
)
|
||||
+ "},\n }"
|
||||
)
|
||||
return "/* Generated by tools/generate_shellcore_offsets.py. */\n" + ",\n".join(records) + "\n"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("root", type=Path)
|
||||
parser.add_argument("--output", type=Path)
|
||||
args = parser.parse_args()
|
||||
output = generate(args.root)
|
||||
if args.output:
|
||||
with args.output.open("w", encoding="utf-8", newline="\n") as file:
|
||||
file.write(output)
|
||||
else:
|
||||
print(output, end="")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate SceShellCore lifecycle offsets from a flat ELF directory.
|
||||
|
||||
Input files must be named ``SceShellCoreXXX.elf`` or ``SceShellCoreXXXX.elf``.
|
||||
The last two digits are the minor firmware version; the preceding digits are
|
||||
the major version. For example, ``SceShellCore1140.elf`` is firmware 11.40.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from generate_shellcore_offsets import ShellCore, TARGET_NAMES, firmware_key
|
||||
|
||||
|
||||
SHELLCORE_NAME = re.compile(r"^SceShellCore(\d{3,4})\.elf$")
|
||||
|
||||
|
||||
def firmware_files(root: Path) -> list[tuple[int, int, Path]]:
|
||||
result: list[tuple[int, int, Path]] = []
|
||||
for path in root.iterdir():
|
||||
if not path.is_file():
|
||||
continue
|
||||
match = SHELLCORE_NAME.fullmatch(path.name)
|
||||
if match is None:
|
||||
continue
|
||||
version = match.group(1)
|
||||
result.append((int(version[:-2]), int(version[-2:]), path))
|
||||
return sorted(result, key=lambda item: (item[0], item[1]))
|
||||
|
||||
|
||||
def generate(root: Path) -> str:
|
||||
records: list[str] = []
|
||||
for major, minor, path in firmware_files(root):
|
||||
firmware_name = f"{major}.{minor:02d}"
|
||||
shellcore = ShellCore(path)
|
||||
targets = shellcore.locate_targets()
|
||||
records.append(
|
||||
" {\n"
|
||||
f" .firmware = 0x{firmware_key(firmware_name):04x}u,\n"
|
||||
f" .name = \"{firmware_name}\",\n"
|
||||
" .targets = {"
|
||||
+ ", ".join(
|
||||
"{.offset = "
|
||||
f"0x{targets[name]:x}u, .patch_size = "
|
||||
f"{shellcore.patch_size(targets[name])}u}}"
|
||||
for name in TARGET_NAMES
|
||||
)
|
||||
+ "},\n }"
|
||||
)
|
||||
return (
|
||||
"/* Generated by tools/generate_shellcore_offsets_from_files.py. */\n"
|
||||
+ ",\n".join(records)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate offsets from SceShellCoreXXXX.elf files"
|
||||
)
|
||||
parser.add_argument("root", type=Path, help="directory containing the ELF files")
|
||||
parser.add_argument("--output", type=Path)
|
||||
args = parser.parse_args()
|
||||
output = generate(args.root)
|
||||
if args.output:
|
||||
with args.output.open("w", encoding="utf-8", newline="\n") as file:
|
||||
file.write(output)
|
||||
else:
|
||||
print(output, end="")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify that generated ShellCore hook prologues are safe to copy."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from capstone import CS_ARCH_X86, CS_GRP_CALL, CS_GRP_JUMP, CS_MODE_64, Cs
|
||||
from capstone.x86 import X86_OP_MEM, X86_REG_RIP
|
||||
|
||||
from generate_shellcore_offsets import ShellCore
|
||||
|
||||
|
||||
def verify_firmware(path: Path) -> None:
|
||||
shellcore = ShellCore(path)
|
||||
targets = shellcore.locate_targets()
|
||||
decoder = Cs(CS_ARCH_X86, CS_MODE_64)
|
||||
decoder.detail = True
|
||||
|
||||
names = ["launch_app"]
|
||||
# AppInstallAll is hooked after the public TitleDir RPC disappeared.
|
||||
try:
|
||||
firmware = int(path.parents[2].name.replace(".", ""), 16)
|
||||
except ValueError:
|
||||
firmware = 0
|
||||
if firmware >= 0x1200:
|
||||
names.append("install_all")
|
||||
|
||||
for name in names:
|
||||
address = targets[name]
|
||||
patch_size = shellcore.patch_size(address)
|
||||
offset = shellcore.virtual_to_file(address)
|
||||
code = shellcore.data[offset : offset + patch_size]
|
||||
instructions = list(decoder.disasm(code, address))
|
||||
if sum(instruction.size for instruction in instructions) != patch_size:
|
||||
raise ValueError(f"{path}: {name}: split instruction")
|
||||
for instruction in instructions:
|
||||
if instruction.group(CS_GRP_CALL) or instruction.group(CS_GRP_JUMP):
|
||||
raise ValueError(
|
||||
f"{path}: {name}: control flow in copied prologue at "
|
||||
f"0x{instruction.address:x}"
|
||||
)
|
||||
for operand in instruction.operands:
|
||||
if operand.type == X86_OP_MEM and operand.mem.base == X86_REG_RIP:
|
||||
raise ValueError(
|
||||
f"{path}: {name}: RIP-relative operand at "
|
||||
f"0x{instruction.address:x}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("root", type=Path)
|
||||
args = parser.parse_args()
|
||||
firmware_dirs = sorted(
|
||||
path
|
||||
for path in args.root.iterdir()
|
||||
if (path / "system/vsh/SceShellCore.elf").is_file()
|
||||
)
|
||||
for firmware_dir in firmware_dirs:
|
||||
verify_firmware(firmware_dir / "system/vsh/SceShellCore.elf")
|
||||
print(f"verified {len(firmware_dirs)} firmware trampoline layouts")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user