diff --git a/include/sm_filesystem.h b/include/sm_filesystem.h index f065ee5..744a5de 100644 --- a/include/sm_filesystem.h +++ b/include/sm_filesystem.h @@ -42,6 +42,8 @@ bool mount_title_nullfs(const char *title_id, const char *src_path); 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); +// Same operation without expected EBUSY diagnostics during a bounded retry. +bool unmount_title_runtime_layers_quiet(const char *title_id); // Release every managed title layer while preserving link metadata. bool unmount_all_title_runtime_layers(void); // Reconcile the title mount stack against the expected source/backport state. diff --git a/include/sm_shellcore_hooks.h b/include/sm_shellcore_hooks.h index 69bcc44..ad31d6b 100644 --- a/include/sm_shellcore_hooks.h +++ b/include/sm_shellcore_hooks.h @@ -6,8 +6,8 @@ // Install/remove the fail-open SceShellCore lifecycle bridge. bool sm_shellcore_hooks_start(void); void sm_shellcore_hooks_stop(void); -// Return whether the post-unmountWorkspace lifecycle hook is installed. -bool sm_shellcore_workspace_hook_active(void); +// Return whether the post-onAppExit lifecycle hook is installed. +bool sm_shellcore_app_exit_hook_active(void); // Return whether this firmware uses the internal TitleDir bridge. bool sm_shellcore_install_bridge_enabled(void); diff --git a/include/sm_shellcore_offsets.h b/include/sm_shellcore_offsets.h index 77952e6..9691d25 100644 --- a/include/sm_shellcore_offsets.h +++ b/include/sm_shellcore_offsets.h @@ -5,7 +5,7 @@ typedef enum { SM_SHELLCORE_TARGET_LAUNCH_APP = 0, - SM_SHELLCORE_TARGET_UNMOUNT_WORKSPACE, + SM_SHELLCORE_TARGET_APP_EXIT, SM_SHELLCORE_TARGET_INSTALL_TITLE_DIR, SM_SHELLCORE_TARGET_INSTALL_ALL, SM_SHELLCORE_TARGET_COUNT diff --git a/include/sm_shellcore_protocol.h b/include/sm_shellcore_protocol.h index 80bf404..3377a80 100644 --- a/include/sm_shellcore_protocol.h +++ b/include/sm_shellcore_protocol.h @@ -12,7 +12,7 @@ typedef enum { SM_SHELLCORE_REQUEST_LAUNCH = 1, SM_SHELLCORE_REQUEST_LAUNCH_FAILED = 2, - SM_SHELLCORE_REQUEST_WORKSPACE_UNMOUNTED = 3 + SM_SHELLCORE_REQUEST_APP_EXITED = 3 } sm_shellcore_request_op_t; typedef struct { @@ -24,7 +24,7 @@ typedef struct { struct { uint32_t app_id; uint8_t reserved[MAX_TITLE_ID - sizeof(uint32_t)]; - } workspace; + } app_exit; } payload; } sm_shellcore_request_t; diff --git a/include/sm_shellcore_service.h b/include/sm_shellcore_service.h index 0043798..4cace4f 100644 --- a/include/sm_shellcore_service.h +++ b/include/sm_shellcore_service.h @@ -8,8 +8,7 @@ bool sm_shellcore_service_start(void); void sm_shellcore_service_stop(void); -// Release transient title/image mounts after launch failure or ShellCore -// workspace teardown. +// Release transient title/image mounts after launch failure or app exit. bool sm_shellcore_release_title_runtime(const char *title_id); // Public API entry points. Return 0 or a positive errno value. int sm_shellcore_mount_title_runtime(const char *title_id); @@ -17,7 +16,7 @@ int sm_shellcore_unmount_title_runtime(const char *title_id); // Bind the prepared managed title to the app id stored in LncApplication. void sm_shellcore_service_bind_prepared_app(const char *title_id, uint32_t app_id); -// Publish process exit without tearing down the still-live ShellCore workspace. +// Publish process exit while ShellCore finishes its own app-exit cleanup. // Returns true when a managed prepared title transitioned to exit-pending. bool sm_shellcore_service_note_game_exit(const char *title_id); // Ensure that a managed title has its image/nullfs/backport runtime stack. diff --git a/src/sm_filesystem.c b/src/sm_filesystem.c index 43116ec..936557c 100644 --- a/src/sm_filesystem.c +++ b/src/sm_filesystem.c @@ -11,6 +11,8 @@ #include "sm_paths.h" static bool unmount_controlled_mount_stack(const char *path); +static bool unmount_controlled_mount_stack_impl(const char *path, + bool diagnose_busy); // --- FILESYSTEM --- bool is_installed(const char *title_id) { @@ -593,7 +595,8 @@ static bool inspect_title_stack(const char *title_id, const char *source_path, return true; } -static bool unmount_top_controlled_layer(const char *path) { +static bool unmount_top_controlled_layer_impl(const char *path, + bool diagnose_busy) { struct statfs mount_st; if (!get_top_mount(path, &mount_st)) return true; @@ -611,15 +614,21 @@ static bool unmount_top_controlled_layer(const char *path) { if (unmount(path, 0) == 0 || errno == ENOENT || errno == EINVAL) return true; int unmount_errno = errno; - if (unmount_errno == EBUSY) + if (unmount_errno == EBUSY && diagnose_busy) sm_mount_diag_log_busy(path); - log_debug(" [LINK] unmount deferred for %s: %s", path, - strerror(unmount_errno)); + if (unmount_errno != EBUSY || diagnose_busy) { + log_debug(" [LINK] unmount deferred for %s: %s", path, + strerror(unmount_errno)); + } errno = unmount_errno; return false; } +static bool unmount_top_controlled_layer(const char *path) { + return unmount_top_controlled_layer_impl(path, true); +} + static bool title_stack_top_is_managed(const title_mount_state_t *state) { return state->top_is_our_nullfs || state->top_kind == TITLE_STACK_TOP_BACKPORT; } @@ -638,7 +647,8 @@ bool rollback_title_nullfs_mount(const char *title_id, const char *src_path) { return true; } -bool unmount_title_runtime_layers(const char *title_id) { +static bool unmount_title_runtime_layers_impl(const char *title_id, + bool diagnose_busy) { if (!title_id || title_id[0] == '\0') return false; @@ -647,12 +657,20 @@ bool unmount_title_runtime_layers(const char *title_id) { if (written < 0 || (size_t)written >= sizeof(path)) return false; - if (!unmount_controlled_mount_stack(path)) + if (!unmount_controlled_mount_stack_impl(path, diagnose_busy)) return false; log_debug(" [LINK] runtime layers released: %s", title_id); return true; } +bool unmount_title_runtime_layers(const char *title_id) { + return unmount_title_runtime_layers_impl(title_id, true); +} + +bool unmount_title_runtime_layers_quiet(const char *title_id) { + return unmount_title_runtime_layers_impl(title_id, false); +} + static bool unmount_managed_title_entry( const char *title_id, const title_link_paths_t *paths, void *ctx) { bool *all_released = (bool *)ctx; @@ -763,12 +781,13 @@ bool mount_backport_overlay(const char *mount_point, return false; } -static bool unmount_controlled_mount_stack(const char *path) { +static bool unmount_controlled_mount_stack_impl(const char *path, + bool diagnose_busy) { for (int i = 0; i < MAX_LAYERED_UNMOUNT_ATTEMPTS * 4; i++) { struct statfs mount_st; if (!get_top_mount(path, &mount_st)) return true; - if (!unmount_top_controlled_layer(path)) + if (!unmount_top_controlled_layer_impl(path, diagnose_busy)) return false; } @@ -776,6 +795,10 @@ static bool unmount_controlled_mount_stack(const char *path) { return !get_top_mount(path, &mount_st); } +static bool unmount_controlled_mount_stack(const char *path) { + return unmount_controlled_mount_stack_impl(path, true); +} + static void unmount_mount_point_for_recovery(const char *path) { if (!unmount_controlled_mount_stack(path)) { log_debug(" [LINK] failed to reset mount stack for recovery: %s", path); diff --git a/src/sm_game_lifecycle.c b/src/sm_game_lifecycle.c index fb9ac00..6cafcc3 100644 --- a/src/sm_game_lifecycle.c +++ b/src/sm_game_lifecycle.c @@ -476,11 +476,11 @@ static void handle_game_exit(pid_t pid) { sm_kstuff_game_on_exit(pid); if (had_active_title) { if (sm_shellcore_service_note_game_exit(title_id)) { - if (sm_shellcore_workspace_hook_active()) { - log_debug(" [SHELLCORE] runtime release awaiting unmountWorkspace: %s", + if (sm_shellcore_app_exit_hook_active()) { + log_debug(" [SHELLCORE] runtime release awaiting onAppExit: %s", title_id); } else if (!sm_shellcore_release_title_runtime(title_id)) { - log_debug(" [SHELLCORE] no workspace hook; runtime release deferred: %s", + log_debug(" [SHELLCORE] no app-exit hook; runtime release deferred: %s", title_id); } } diff --git a/src/sm_shellcore_bridge.S b/src/sm_shellcore_bridge.S index 77feb5e..45ce2dd 100644 --- a/src/sm_shellcore_bridge.S +++ b/src/sm_shellcore_bridge.S @@ -4,10 +4,10 @@ .global sm_shellcore_bridge_blob_start .global sm_shellcore_bridge_blob_end .global sm_shellcore_bridge_launch_hook - .global sm_shellcore_bridge_unmount_hook + .global sm_shellcore_bridge_app_exit_hook .global sm_shellcore_bridge_install_all_hook .global sm_shellcore_bridge_launch_trampoline - .global sm_shellcore_bridge_unmount_trampoline + .global sm_shellcore_bridge_app_exit_trampoline .global sm_shellcore_bridge_install_all_trampoline .global sm_shellcore_bridge_install_title_dir .global sm_shellcore_bridge_install_armed @@ -70,15 +70,14 @@ sm_shellcore_bridge_launch_hook: .p2align 4 /* - * LncApplication::unmountWorkspace() first removes FsSandbox mappings and - * tears down m_workspace. Notify ShadowMount only after the original method - * returns. The first LncApplication dword is m_appId (also passed by callers - * to endAppMount), which lets the service reject unrelated application exits. + * LncManager::onAppExit(uint32_t app_id) owns the SysCore application-exit + * event. Notify ShadowMount only after the original method returns, after its + * firmware-specific exit cleanup. The app id is the second argument. */ -sm_shellcore_bridge_unmount_hook: +sm_shellcore_bridge_app_exit_hook: pushq %rbx - movl (%rdi), %ebx - call *sm_shellcore_bridge_unmount_trampoline(%rip) + movl %esi, %ebx + call *sm_shellcore_bridge_app_exit_trampoline(%rip) pushq %rax subq $8, %rsp movl $3, %edi @@ -196,7 +195,7 @@ sm_shellcore_bridge_exchange: .p2align 3 sm_shellcore_bridge_launch_trampoline: .quad 0 -sm_shellcore_bridge_unmount_trampoline: +sm_shellcore_bridge_app_exit_trampoline: .quad 0 sm_shellcore_bridge_install_all_trampoline: .quad 0 diff --git a/src/sm_shellcore_hooks.c b/src/sm_shellcore_hooks.c index 72c52c0..f386e53 100644 --- a/src/sm_shellcore_hooks.c +++ b/src/sm_shellcore_hooks.c @@ -71,10 +71,10 @@ 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_unmount_hook[]; +extern const uint8_t sm_shellcore_bridge_app_exit_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_unmount_trampoline[]; +extern const uint8_t sm_shellcore_bridge_app_exit_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[]; @@ -260,12 +260,12 @@ static bool install_hooks_for_pid(pid_t pid) { : SHELLCORE_BASE_HOOK_COUNT; uintptr_t targets[SHELLCORE_MAX_HOOK_COUNT] = { remote.targets[SM_SHELLCORE_TARGET_LAUNCH_APP], - remote.targets[SM_SHELLCORE_TARGET_UNMOUNT_WORKSPACE], + remote.targets[SM_SHELLCORE_TARGET_APP_EXIT], 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_UNMOUNT_WORKSPACE, + SM_SHELLCORE_TARGET_APP_EXIT, SM_SHELLCORE_TARGET_INSTALL_ALL, }; @@ -290,7 +290,7 @@ static bool install_hooks_for_pid(pid_t pid) { size_t cursor = blob_size; uintptr_t launch_trampoline = 0; - uintptr_t unmount_trampoline = 0; + uintptr_t app_exit_trampoline = 0; uintptr_t install_all_trampoline = 0; if (!append_trampoline(bridge, bridge_capacity, &cursor, g_hooks.original[0], g_hooks.original_size[0], @@ -303,11 +303,11 @@ static bool install_hooks_for_pid(pid_t pid) { 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, - &unmount_trampoline)) { + &app_exit_trampoline)) { goto done; } - set_bridge_pointer(bridge, sm_shellcore_bridge_unmount_trampoline, - unmount_trampoline); + set_bridge_pointer(bridge, sm_shellcore_bridge_app_exit_trampoline, + app_exit_trampoline); if (install_hook_enabled) { if (!append_trampoline(bridge, bridge_capacity, &cursor, g_hooks.original[2], g_hooks.original_size[2], @@ -335,8 +335,8 @@ static bool install_hooks_for_pid(pid_t pid) { uintptr_t launch_hook = object.eh_frame + (uintptr_t)(sm_shellcore_bridge_launch_hook - sm_shellcore_bridge_blob_start); - uintptr_t unmount_hook = - object.eh_frame + (uintptr_t)(sm_shellcore_bridge_unmount_hook - + uintptr_t app_exit_hook = + object.eh_frame + (uintptr_t)(sm_shellcore_bridge_app_exit_hook - sm_shellcore_bridge_blob_start); uintptr_t install_all_hook = object.eh_frame + @@ -344,7 +344,7 @@ static bool install_hooks_for_pid(pid_t pid) { sm_shellcore_bridge_blob_start); if (!patch_remote_jump(pid, targets[0], launch_hook, g_hooks.original_size[0]) || - !patch_remote_jump(pid, targets[1], unmount_hook, + !patch_remote_jump(pid, targets[1], app_exit_hook, g_hooks.original_size[1]) || (install_hook_enabled && !patch_remote_jump(pid, targets[2], install_all_hook, @@ -360,7 +360,7 @@ static bool install_hooks_for_pid(pid_t pid) { g_hooks.installed = true; ok = true; log_debug(" [SHELLCORE] hooks installed: fw=%s pid=%ld lifecycle=1 " - "workspace=1 install=%d", + "app_exit=1 install=%d", remote.offsets->name, (long)pid, install_hook_enabled ? 1 : 0); goto done; @@ -417,7 +417,7 @@ void sm_shellcore_hooks_stop(void) { pthread_mutex_unlock(&g_install_mutex); } -bool sm_shellcore_workspace_hook_active(void) { +bool sm_shellcore_app_exit_hook_active(void) { int saved_errno = errno; pthread_mutex_lock(&g_install_mutex); bool active = g_hooks.installed && g_hooks.remote.pid > 0; diff --git a/src/sm_shellcore_offsets.c b/src/sm_shellcore_offsets.c index e0fe4c1..2ac887b 100644 --- a/src/sm_shellcore_offsets.c +++ b/src/sm_shellcore_offsets.c @@ -21,8 +21,8 @@ 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_APP_EXIT: + return "onAppExit"; case SM_SHELLCORE_TARGET_INSTALL_TITLE_DIR: return "AppInstallTitleDirMain"; case SM_SHELLCORE_TARGET_INSTALL_ALL: diff --git a/src/sm_shellcore_offsets.inc b/src/sm_shellcore_offsets.inc index aecf979..4e264e2 100644 --- a/src/sm_shellcore_offsets.inc +++ b/src/sm_shellcore_offsets.inc @@ -1,285 +1,286 @@ +/* Generated by tools/generate_shellcore_offsets_from_files.py. */ { .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}}, + .targets = {{.offset = 0x3465a0u, .patch_size = 12u}, {.offset = 0x3498e0u, .patch_size = 12u}, {.offset = 0x1adc50u, .patch_size = 12u}, {.offset = 0xa05c40u, .patch_size = 14u}}, }, { .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}}, + .targets = {{.offset = 0x3465a0u, .patch_size = 12u}, {.offset = 0x3498e0u, .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}}, + .targets = {{.offset = 0x3465a0u, .patch_size = 12u}, {.offset = 0x3498e0u, .patch_size = 12u}, {.offset = 0x1adc50u, .patch_size = 12u}, {.offset = 0xa05c40u, .patch_size = 14u}}, }, { .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}}, + .targets = {{.offset = 0x3469b0u, .patch_size = 12u}, {.offset = 0x349cf0u, .patch_size = 12u}, {.offset = 0x1adc30u, .patch_size = 12u}, {.offset = 0xa063a0u, .patch_size = 14u}}, }, { .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}}, + .targets = {{.offset = 0x3469b0u, .patch_size = 12u}, {.offset = 0x349cf0u, .patch_size = 12u}, {.offset = 0x1adc30u, .patch_size = 12u}, {.offset = 0xa06660u, .patch_size = 14u}}, }, { .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}}, + .targets = {{.offset = 0x3a4660u, .patch_size = 12u}, {.offset = 0x3a7cf0u, .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}}, + .targets = {{.offset = 0x3a4930u, .patch_size = 12u}, {.offset = 0x3a7fc0u, .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}}, + .targets = {{.offset = 0x3a4e80u, .patch_size = 12u}, {.offset = 0x3a8510u, .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}}, + .targets = {{.offset = 0x3a65f0u, .patch_size = 12u}, {.offset = 0x3a9c80u, .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}}, + .targets = {{.offset = 0x3a6d90u, .patch_size = 12u}, {.offset = 0x3aa420u, .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}}, + .targets = {{.offset = 0x3a8140u, .patch_size = 12u}, {.offset = 0x3ab7d0u, .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}}, + .targets = {{.offset = 0x3a8140u, .patch_size = 12u}, {.offset = 0x3ab7d0u, .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}}, + .targets = {{.offset = 0x3f1960u, .patch_size = 12u}, {.offset = 0x3f51e0u, .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}}, + .targets = {{.offset = 0x3f19a0u, .patch_size = 12u}, {.offset = 0x3f5220u, .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}}, + .targets = {{.offset = 0x3f1a50u, .patch_size = 12u}, {.offset = 0x3f52d0u, .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}}, + .targets = {{.offset = 0x3f1a50u, .patch_size = 12u}, {.offset = 0x3f52d0u, .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}}, + .targets = {{.offset = 0x432630u, .patch_size = 12u}, {.offset = 0x436460u, .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}}, + .targets = {{.offset = 0x432630u, .patch_size = 12u}, {.offset = 0x436460u, .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}}, + .targets = {{.offset = 0x432d80u, .patch_size = 12u}, {.offset = 0x436bb0u, .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}}, + .targets = {{.offset = 0x432d80u, .patch_size = 12u}, {.offset = 0x436bb0u, .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}}, + .targets = {{.offset = 0x498950u, .patch_size = 12u}, {.offset = 0x49c640u, .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}}, + .targets = {{.offset = 0x498940u, .patch_size = 12u}, {.offset = 0x49c630u, .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}}, + .targets = {{.offset = 0x4998f0u, .patch_size = 12u}, {.offset = 0x49dfc0u, .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}}, + .targets = {{.offset = 0x4998f0u, .patch_size = 12u}, {.offset = 0x49dfc0u, .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}}, + .targets = {{.offset = 0x4c7380u, .patch_size = 12u}, {.offset = 0x4cb8c0u, .patch_size = 12u}, {.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}}, + .targets = {{.offset = 0x4c7380u, .patch_size = 12u}, {.offset = 0x4cb8c0u, .patch_size = 12u}, {.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}}, + .targets = {{.offset = 0x4c73f0u, .patch_size = 12u}, {.offset = 0x4cb930u, .patch_size = 12u}, {.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}}, + .targets = {{.offset = 0x568920u, .patch_size = 12u}, {.offset = 0x56e9b0u, .patch_size = 12u}, {.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}}, + .targets = {{.offset = 0x568920u, .patch_size = 12u}, {.offset = 0x56e9b0u, .patch_size = 12u}, {.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}}, + .targets = {{.offset = 0x568940u, .patch_size = 12u}, {.offset = 0x56e9d0u, .patch_size = 12u}, {.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}}, + .targets = {{.offset = 0x56d430u, .patch_size = 12u}, {.offset = 0x5734c0u, .patch_size = 12u}, {.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}}, + .targets = {{.offset = 0x56d430u, .patch_size = 12u}, {.offset = 0x5734c0u, .patch_size = 12u}, {.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}}, + .targets = {{.offset = 0x56d430u, .patch_size = 12u}, {.offset = 0x5734c0u, .patch_size = 12u}, {.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}}, + .targets = {{.offset = 0x597eb0u, .patch_size = 12u}, {.offset = 0x59c640u, .patch_size = 12u}, {.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}}, + .targets = {{.offset = 0x598f80u, .patch_size = 12u}, {.offset = 0x59d710u, .patch_size = 12u}, {.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}}, + .targets = {{.offset = 0x598f80u, .patch_size = 12u}, {.offset = 0x59d710u, .patch_size = 12u}, {.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}}, + .targets = {{.offset = 0x598d60u, .patch_size = 12u}, {.offset = 0x59d4f0u, .patch_size = 12u}, {.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}}, + .targets = {{.offset = 0x5cc0a0u, .patch_size = 12u}, {.offset = 0x5d0a80u, .patch_size = 12u}, {.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}}, + .targets = {{.offset = 0x5cc0a0u, .patch_size = 12u}, {.offset = 0x5d0a80u, .patch_size = 12u}, {.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}}, + .targets = {{.offset = 0x5cbdc0u, .patch_size = 12u}, {.offset = 0x5d07a0u, .patch_size = 12u}, {.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}}, + .targets = {{.offset = 0x5cc490u, .patch_size = 12u}, {.offset = 0x5d0e70u, .patch_size = 12u}, {.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}}, + .targets = {{.offset = 0x5cc500u, .patch_size = 12u}, {.offset = 0x5d0ee0u, .patch_size = 12u}, {.offset = 0x2b7c10u, .patch_size = 12u}, {.offset = 0x2be820u, .patch_size = 12u}}, + }, + { + .firmware = 0x1000u, + .name = "10.00", + .targets = {{.offset = 0x5ca1a0u, .patch_size = 12u}, {.offset = 0x5ce820u, .patch_size = 12u}, {.offset = 0x2b6700u, .patch_size = 12u}, {.offset = 0x17d1f00u, .patch_size = 16u}}, + }, + { + .firmware = 0x1001u, + .name = "10.01", + .targets = {{.offset = 0x5ca1a0u, .patch_size = 12u}, {.offset = 0x5ce820u, .patch_size = 12u}, {.offset = 0x2b6700u, .patch_size = 12u}, {.offset = 0x17d1f00u, .patch_size = 16u}}, + }, + { + .firmware = 0x1020u, + .name = "10.20", + .targets = {{.offset = 0x5ca1a0u, .patch_size = 12u}, {.offset = 0x5ce820u, .patch_size = 12u}, {.offset = 0x2b6700u, .patch_size = 12u}, {.offset = 0x17d61c0u, .patch_size = 16u}}, + }, + { + .firmware = 0x1040u, + .name = "10.40", + .targets = {{.offset = 0x5ca0f0u, .patch_size = 12u}, {.offset = 0x5ce770u, .patch_size = 12u}, {.offset = 0x2b6760u, .patch_size = 12u}, {.offset = 0x17d61e0u, .patch_size = 16u}}, + }, + { + .firmware = 0x1060u, + .name = "10.60", + .targets = {{.offset = 0x5cb980u, .patch_size = 12u}, {.offset = 0x5d0000u, .patch_size = 12u}, {.offset = 0x2b80c0u, .patch_size = 12u}, {.offset = 0x17d7a60u, .patch_size = 16u}}, + }, + { + .firmware = 0x1100u, + .name = "11.00", + .targets = {{.offset = 0x62a940u, .patch_size = 12u}, {.offset = 0x62efe0u, .patch_size = 12u}, {.offset = 0x2bd880u, .patch_size = 12u}, {.offset = 0x2c4e60u, .patch_size = 15u}}, + }, + { + .firmware = 0x1120u, + .name = "11.20", + .targets = {{.offset = 0x62aab0u, .patch_size = 12u}, {.offset = 0x62f150u, .patch_size = 12u}, {.offset = 0x2bd930u, .patch_size = 12u}, {.offset = 0x2c4f10u, .patch_size = 15u}}, + }, + { + .firmware = 0x1140u, + .name = "11.40", + .targets = {{.offset = 0x62ba90u, .patch_size = 12u}, {.offset = 0x630130u, .patch_size = 12u}, {.offset = 0x2be910u, .patch_size = 12u}, {.offset = 0x2c5ef0u, .patch_size = 15u}}, + }, + { + .firmware = 0x1160u, + .name = "11.60", + .targets = {{.offset = 0x633500u, .patch_size = 12u}, {.offset = 0x637ba0u, .patch_size = 12u}, {.offset = 0x2c2c10u, .patch_size = 12u}, {.offset = 0x2ca1f0u, .patch_size = 15u}}, + }, + { + .firmware = 0x1200u, + .name = "12.00", + .targets = {{.offset = 0x647410u, .patch_size = 12u}, {.offset = 0x64bac0u, .patch_size = 12u}, {.offset = 0x2e0050u, .patch_size = 12u}, {.offset = 0x2dd100u, .patch_size = 12u}}, + }, + { + .firmware = 0x1202u, + .name = "12.02", + .targets = {{.offset = 0x647410u, .patch_size = 12u}, {.offset = 0x64bac0u, .patch_size = 12u}, {.offset = 0x2e0050u, .patch_size = 12u}, {.offset = 0x2dd100u, .patch_size = 12u}}, + }, + { + .firmware = 0x1220u, + .name = "12.20", + .targets = {{.offset = 0x647410u, .patch_size = 12u}, {.offset = 0x64bac0u, .patch_size = 12u}, {.offset = 0x2e0050u, .patch_size = 12u}, {.offset = 0x2dd100u, .patch_size = 12u}}, + }, + { + .firmware = 0x1240u, + .name = "12.40", + .targets = {{.offset = 0x647410u, .patch_size = 12u}, {.offset = 0x64bac0u, .patch_size = 12u}, {.offset = 0x2e0050u, .patch_size = 12u}, {.offset = 0x2dd100u, .patch_size = 12u}}, + }, + { + .firmware = 0x1260u, + .name = "12.60", + .targets = {{.offset = 0x648660u, .patch_size = 12u}, {.offset = 0x64cd10u, .patch_size = 12u}, {.offset = 0x2e0320u, .patch_size = 12u}, {.offset = 0x2dd3d0u, .patch_size = 12u}}, + }, + { + .firmware = 0x1270u, + .name = "12.70", + .targets = {{.offset = 0x648660u, .patch_size = 12u}, {.offset = 0x64cd10u, .patch_size = 12u}, {.offset = 0x2e0320u, .patch_size = 12u}, {.offset = 0x2dd3d0u, .patch_size = 12u}}, } diff --git a/src/sm_shellcore_service.c b/src/sm_shellcore_service.c index a3b871d..ddd5e13 100644 --- a/src/sm_shellcore_service.c +++ b/src/sm_shellcore_service.c @@ -22,8 +22,8 @@ _Static_assert(sizeof(sm_shellcore_request_t) == 40, "unexpected ShellCore request size"); _Static_assert(offsetof(sm_shellcore_request_t, payload) == 8, "unexpected ShellCore request payload offset"); -_Static_assert(offsetof(sm_shellcore_request_t, payload.workspace.app_id) == 8, - "unexpected ShellCore workspace app id offset"); +_Static_assert(offsetof(sm_shellcore_request_t, payload.app_exit.app_id) == 8, + "unexpected ShellCore app-exit app id offset"); _Static_assert(sizeof(sm_shellcore_response_t) == 4, "unexpected ShellCore response size"); @@ -49,6 +49,9 @@ static shellcore_service_state_t g_service = { .client_fd = -1, }; +#define SHELLCORE_RELEASE_MAX_ATTEMPTS 5u +#define SHELLCORE_RELEASE_RETRY_DELAY_US 150000u + static int claim_prepared_title(const char *title_id, bool launch_request, bool *claimed_out) { bool changed = false; @@ -134,6 +137,13 @@ static void end_runtime_release(void) { pthread_mutex_unlock(&g_service.mutex); } +static bool shellcore_service_stopping(void) { + pthread_mutex_lock(&g_service.mutex); + bool stopping = g_service.stop_requested; + pthread_mutex_unlock(&g_service.mutex); + return stopping; +} + static bool find_required_image_layer(const char *root, const char *runtime_source, unsigned int depth, @@ -361,18 +371,47 @@ static bool release_title_runtime(const char *title_id, errno = begin_status; return false; } - runtime_mount_state_lock(); log_debug(" [SHELLCORE] runtime release start: %s", title_id); - bool released = unmount_title_runtime_layers(title_id); - if (released) - released = release_backing_image(title_id); - runtime_mount_state_unlock(); + bool released = false; + int release_errno = 0; + unsigned int attempts = public_request ? 1u : SHELLCORE_RELEASE_MAX_ATTEMPTS; + unsigned int attempt = 0; + for (; attempt < attempts; ++attempt) { + bool diagnose_busy = attempt + 1u == attempts; + runtime_mount_state_lock(); + released = diagnose_busy + ? unmount_title_runtime_layers(title_id) + : unmount_title_runtime_layers_quiet(title_id); + if (released) + released = release_backing_image(title_id); + release_errno = released ? 0 : errno; + runtime_mount_state_unlock(); + + if (released || release_errno != EBUSY || attempt + 1u == attempts || + shellcore_service_stopping()) { + break; + } + if (attempt == 0) { + log_debug(" [SHELLCORE] runtime release busy; fast retry armed: %s " + "attempts=%u delay_ms=%u", + title_id, attempts - 1u, + SHELLCORE_RELEASE_RETRY_DELAY_US / 1000u); + } + (void)sceKernelUsleep(SHELLCORE_RELEASE_RETRY_DELAY_US); + } + if (released && attempt > 0) { + log_debug(" [SHELLCORE] runtime release completed after retry: %s " + "attempt=%u", + title_id, attempt + 1u); + } // The lifecycle owner is gone even when a short-lived ShellCore sandbox // nullfs still pins the title stack. Leaving the title marked as prepared // would block scanner cleanup forever. The intact stack remains discoverable // through mount.lnk and is retried by the normal full-scan cleanup cycle. clear_prepared_title(title_id); end_runtime_release(); + if (!released && release_errno != 0) + errno = release_errno; return released; } @@ -413,7 +452,7 @@ bool sm_shellcore_service_note_game_exit(const char *title_id) { bool sm_shellcore_service_has_prepared_mount(void) { pthread_mutex_lock(&g_service.mutex); // Keep scanner cleanup blocked after process exit as well. Only the - // post-unmountWorkspace event (or the explicit no-hook fallback) owns the + // post-onAppExit event (or the explicit no-hook fallback) owns the // transition that clears this state and releases the runtime stack. bool prepared = g_service.prepared_title_id[0] != '\0'; pthread_mutex_unlock(&g_service.mutex); @@ -442,28 +481,29 @@ static int handle_launch_failed_request(const char *title_id) { return sm_shellcore_release_title_runtime(title_id) ? 0 : EBUSY; } -static int handle_workspace_unmounted(uint32_t app_id) { +static bool resolve_app_exit_title(uint32_t app_id, + char title_id[MAX_TITLE_ID]) { if (app_id == 0) - return 0; + return false; - char title_id[MAX_TITLE_ID] = {0}; pthread_mutex_lock(&g_service.mutex); if (g_service.prepared_app_id == app_id && g_service.prepared_title_id[0] != '\0') { - (void)strlcpy(title_id, g_service.prepared_title_id, sizeof(title_id)); + (void)strlcpy(title_id, g_service.prepared_title_id, MAX_TITLE_ID); } pthread_mutex_unlock(&g_service.mutex); if (title_id[0] == '\0') - return 0; + return false; - log_debug(" [SHELLCORE] unmountWorkspace complete: %s app_id=0x%08X", + log_debug(" [SHELLCORE] onAppExit cleanup complete: %s app_id=0x%08X", title_id, app_id); - return sm_shellcore_release_title_runtime(title_id) ? 0 : EBUSY; + return true; } static void handle_client(int fd) { sm_shellcore_request_t request; sm_shellcore_response_t response; + char release_title_id[MAX_TITLE_ID] = {0}; if (!sm_socket_read_full(fd, &request, sizeof(request))) return; if (request.magic != SM_SHELLCORE_PROTOCOL_MAGIC || @@ -477,13 +517,16 @@ static void handle_client(int fd) { response.status = handle_launch_request(request.payload.title_id); } else if (request.operation == SM_SHELLCORE_REQUEST_LAUNCH_FAILED) { response.status = handle_launch_failed_request(request.payload.title_id); - } else if (request.operation == SM_SHELLCORE_REQUEST_WORKSPACE_UNMOUNTED) { - response.status = - handle_workspace_unmounted(request.payload.workspace.app_id); + } else if (request.operation == SM_SHELLCORE_REQUEST_APP_EXITED) { + (void)resolve_app_exit_title(request.payload.app_exit.app_id, + release_title_id); + response.status = 0; } else { response.status = ENOTSUP; } (void)sm_socket_write_full(fd, &response, sizeof(response)); + if (release_title_id[0] != '\0') + (void)sm_shellcore_release_title_runtime(release_title_id); } static void *service_thread_main(void *arg) { diff --git a/tools/generate_shellcore_offsets.py b/tools/generate_shellcore_offsets.py index 68a2b13..22c23e1 100644 --- a/tools/generate_shellcore_offsets.py +++ b/tools/generate_shellcore_offsets.py @@ -5,6 +5,13 @@ 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. + +The runtime-release hook targets ``LncManager::onAppExit`` rather than an +application/workspace destructor. ShellCore can destroy a launch-time +``LncApplication`` object with the live app id, so destructor hooks produce a +false exit event while the game is still running. ``onAppExit`` is the +SysCore exit-event owner and returns after the firmware-specific exit cleanup +path has run. """ from __future__ import annotations @@ -19,7 +26,7 @@ from capstone import CS_ARCH_X86, CS_MODE_64, Cs PROLOGUE = b"\x55\x48\x89\xe5" TARGET_NAMES = ( "launch_app", - "unmount_workspace", + "app_exit", "install_title_dir", "install_all", ) @@ -54,6 +61,7 @@ class ShellCore: self.executable = segment if self.executable is None: raise ValueError(f"{path}: executable PT_LOAD not found") + self._rip_references: list[tuple[int, int]] | None = None def file_to_virtual(self, offset: int) -> int: for segment in self.loads: @@ -85,25 +93,42 @@ class ShellCore: 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 + if self._rip_references is None: + segment = self.executable + assert segment is not None + code = self.data[ + segment.file_offset : segment.file_offset + segment.file_size + ] + references: list[tuple[int, 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(" int: end = self.virtual_to_file(address) @@ -120,17 +145,32 @@ class ShellCore: 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 + app_exit_strings = self.string_addresses( + b"[SceLncService] onAppExit() appId={0x%08x}", False ) - unmount_groups = { + app_exit_groups = { self.preceding_prologue(xref) - for xref in self.rip_xrefs(unmount_strings, slop=8) + for xref in self.rip_xrefs(app_exit_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)) + if len(app_exit_groups) != 1: + raise ValueError(f"onAppExit candidates: {app_exit_groups!r}") + app_exit = next(iter(app_exit_groups)) + + # Validate the candidate with an independent source-expression log. + # This prevents a changed format string from silently selecting an + # unrelated function that also logs an app id. + app_remove_strings = self.string_addresses( + b"m_app_list.remove(app);", False + ) + app_remove_groups = { + self.preceding_prologue(xref) + for xref in self.rip_xrefs(app_remove_strings, slop=8) + } + if app_exit not in app_remove_groups: + raise ValueError( + "onAppExit candidate does not remove the application: " + f"0x{app_exit:x}, remove candidates {app_remove_groups!r}" + ) install_strings = self.string_addresses(b"AppInstallTitleDirMain", False) install_groups: dict[int, int] = {} @@ -162,7 +202,7 @@ class ShellCore: return { "launch_app": launch, - "unmount_workspace": unmount, + "app_exit": app_exit, "install_title_dir": best[0], "install_all": app_install_all, } diff --git a/tools/verify_shellcore_trampolines.py b/tools/verify_shellcore_trampolines.py index b8a382e..922a29c 100644 --- a/tools/verify_shellcore_trampolines.py +++ b/tools/verify_shellcore_trampolines.py @@ -9,21 +9,18 @@ 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 +from generate_shellcore_offsets import ShellCore, firmware_key +from generate_shellcore_offsets_from_files import firmware_files -def verify_firmware(path: Path) -> None: +def verify_firmware(path: Path, firmware: int) -> None: shellcore = ShellCore(path) targets = shellcore.locate_targets() decoder = Cs(CS_ARCH_X86, CS_MODE_64) decoder.detail = True - names = ["launch_app", "unmount_workspace"] + names = ["launch_app", "app_exit"] # 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") @@ -53,14 +50,27 @@ def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("root", type=Path) args = parser.parse_args() + firmware_inputs: list[tuple[int, Path]] = [] 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") + firmware_inputs.append( + ( + firmware_key(firmware_dir.name), + firmware_dir / "system/vsh/SceShellCore.elf", + ) + ) + for major, minor, path in firmware_files(args.root): + firmware_inputs.append((firmware_key(f"{major}.{minor:02d}"), path)) + if not firmware_inputs: + raise ValueError(f"no SceShellCore firmware files under {args.root}") + + for firmware, path in firmware_inputs: + verify_firmware(path, firmware) + print(f"verified {len(firmware_inputs)} firmware trampoline layouts") return 0