Fix ShellCore runtime release after app exit

This commit is contained in:
Roman Tarasov committed 2026-07-16 01:57:28 +03:00
1 parent eb3c611ebf
commit 4085fcd8ca
14 files changed
+333 -216

No files matched your search

+2
View File
@@ -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.
+2 -2
View File
@@ -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);
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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;
+2 -3
View File
@@ -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.
+31 -8
View File
@@ -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);
+3 -3
View File
@@ -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);
}
}
+9 -10
View File
@@ -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
+13 -13
View File
@@ -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;
+2 -2
View File
@@ -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:
+118 -117
View File
@@ -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}},
}
+61 -18
View File
@@ -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) {
+68 -28
View File
@@ -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("<i", code, index + 3)[0]
instruction_address = segment.virtual_address + index
references.append(
(instruction_address, instruction_address + 7 + displacement)
)
self._rip_references = references
if slop == 0:
exact_targets = set(targets)
return [
address
for address, destination in self._rip_references
if destination in exact_targets
]
return [
address
for address, destination in self._rip_references
if any(
target - slop <= destination <= target + slop
for target in targets
)
]
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)
@@ -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,
}
+19 -9
View File
@@ -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