From ece77ea4339be4e7c0c356cd4c8137ffbb2d286d Mon Sep 17 00:00:00 2001 From: Hexwell Date: Fri, 10 Jul 2026 17:56:21 +0800 Subject: [PATCH] fix: harden IPC transport and process-local settings store Serialize IPC_Client with a per-target mutex and full-frame send/recv plus NUL-terminated payloads. Escape reply JSON vars, null-check daemon FS paths, handle BREW_UTIL_TEST_CONNECTION, and track BREW_LAST_RET from the last reply. Introduce SettingsStore (mutex snapshot/update), dual-path config mtime gating, and fan IPC persistence to twin config files. Stop cold-start toolbox delay on util_booted; fix jailbreak hijack retry to stop only when dead or maxed. Add host tests for IPC wire helpers, toolbox timing policy, and hijack retry. --- docs/arch.md | 11 +- docs/architecture-audit.md | 49 ++++--- source/daemon/include/daemon_ops.hpp | 12 +- source/daemon/include/globalconf.hpp | 5 +- source/daemon/source/daemon_fs.cpp | 10 ++ source/daemon/source/daemon_inject.cpp | 24 +++- source/daemon/source/daemon_jailbreak.cpp | 20 ++- source/daemon/source/daemon_settings.cpp | 68 +++++----- source/daemon/source/ipc_handle.cpp | 72 ++++++++-- source/daemon/source/main.cpp | 8 +- source/include/orion/hijack_retry.h | 43 ++++++ source/include/orion/toolbox_timing.h | 30 +++++ .../liborion_ipc/include/orion/ipc_client.hpp | 9 ++ .../liborion_ipc/include/orion/ipc_server.hpp | 44 +++++-- source/liborion_ipc/source/ipc_client.cpp | 54 +++++--- source/liborion_ipc/source/ipc_server.cpp | 124 ++++++++++++++++-- .../include/orion/settings.hpp | 63 ++++++++- source/liborion_settings/source/settings.cpp | 25 ++++ source/shellui/src/settings_ui.cpp | 7 +- source/util/include/common_utils.h | 12 +- source/util/source/cpp_service.cpp | 35 +++-- source/util/source/ipc_handle.cpp | 5 +- source/util/source/main.cpp | 18 +-- source/util/source/msg.cpp | 1 - source/util/source/util_toolbox.cpp | 11 +- source/util/tests/Makefile | 9 ++ source/util/tests/test_hijack_retry.c | 43 ++++++ source/util/tests/test_ipc_harden.cpp | 68 ++++++++++ source/util/tests/test_main.cpp | 6 + source/util/tests/test_msg_protocol.cpp | 7 +- source/util/tests/test_settings.cpp | 39 ++++++ source/util/tests/test_toolbox_timing.c | 44 +++++++ 32 files changed, 805 insertions(+), 171 deletions(-) create mode 100644 source/include/orion/hijack_retry.h create mode 100644 source/include/orion/toolbox_timing.h create mode 100644 source/util/tests/test_hijack_retry.c create mode 100644 source/util/tests/test_ipc_harden.cpp create mode 100644 source/util/tests/test_toolbox_timing.c diff --git a/docs/arch.md b/docs/arch.md index f31b1a3..b8120c2 100644 --- a/docs/arch.md +++ b/docs/arch.md @@ -247,10 +247,13 @@ ipc_client.* 注入侧客户端 #### 配置分层 ``` -orion::Settings 持久化 schema(单文件双路径) -g_settings 各进程持有的 Settings 实例 -运行时原子量 仅 util:g_legacy_cmd_server / g_legacy_cmd_server_exit(热路径) -OverlayLayout 仅 shellui:由 overlay_pos 派生的像素坐标 +orion::Settings 持久化 schema(双路径 twin:primary + shellui) +orion::SettingsStore 进程内线程安全真相源(mutex + snapshot/store/update) +g_settings daemon/util:SettingsStore;shellui:Settings(UI 线程) +LoadSettings() 统一 bool 契约:刷新 store;缺文件用默认并成功 +mtime 门控 settings_config_newest_mtime — 任一 twin 更新即失效 +运行时原子量 仅 util:g_legacy_cmd_server / g_legacy_cmd_server_exit(热路径) +OverlayLayout 仅 shellui:由 overlay_pos 派生的像素坐标 ``` ### 2.8 主机工具(`scripts/`) diff --git a/docs/architecture-audit.md b/docs/architecture-audit.md index f7d87ee..6531bee 100644 --- a/docs/architecture-audit.md +++ b/docs/architecture-audit.md @@ -70,13 +70,13 @@ NineS ──► orion_proc(不再自带 proc 副本) NineS 已避免 per-call authid 翻转;bootstrapper/util attach 仍在翻转。util 金手指走 `pt_attach_proc`(翻转)+ NineS `pt_mmap`(不翻转)——**同一条路径两套策略**。 -### 2.2 Settings:单 schema、三份真相 +### 2.2 Settings:单 schema、三份真相 — **已落地(2026-07-10)** -- 进程内各有一份 `g_settings`(daemon / util / shellui)——多进程可接受。 -- **同一进程内无锁**,IPC 写 + jailbreak 线程读 → 数据竞争。 -- Fan IPC 只改内存,不 `settings_save`;与 shellui 磁盘双写 twin 模型不一致。 -- util:`void LoadSettings()` 定义 vs 多处 `bool LoadSettings()` 声明(ODR/潜在 UB)。 -- daemon mtime 缓存:只盯「第一个存在的 path」,双路径不同步时可能误跳过重载。 +- 进程内各有一份 store(daemon / util:`SettingsStore`;shellui:`Settings`)——多进程可接受。 +- daemon/util:`SettingsStore` mutex + `snapshot`/`store`/`update`,消除 IPC 写与 worker 读竞争。 +- Fan IPC:`update` 后 `settings_save` + `SettingsNoteDiskWritten`(twin 落盘)。 +- 统一 `bool LoadSettings()`(缺文件用默认并成功);util ODR 已修。 +- mtime 门控:`settings_config_newest_mtime` 取双路径 max,任一 twin 更新即重载。 ### 2.3 Ready 标志语义泄漏 @@ -169,28 +169,22 @@ Bootstrapper 只 clear util/kstuff/daemon/toolbox,**不清** `util_booted` / ` - Code cave:`MAP_FIXED` 或按返回 VA 写 - elfldr:`sizeof(Elf64_Shdr)`;mprotect 失败 fail-closed -2. **IPC 硬化**(一处修、全树受益) - - 全帧 recv + 强制 NUL - - cJSON 转义 reply - - path 全 null-check - - 补 `BREW_UTIL_TEST_CONNECTION`;修或删 `BREW_LAST_RET` - - shellui:`IPC_Client` 加 mutex 或每调用独立连接 +2. **IPC 硬化** — **已落地(2026-07-10)** + - 全帧 `recv_full`/`send_full` + `ipc_message_force_nul` + - `ipc_json_escape` + compact escaped `ipc_format_reply_body` + - daemon path 全 null-check;`BREW_UTIL_TEST_CONNECTION`;`BREW_LAST_RET` 记 process last error + - `IPC_Client` 实例 mutex + full-frame 收发 -3. **Ready / Settings 语义收口** - - rest delay 只绑 rest 信号,不要绑 `util_booted` - - bootstrapper clear `util_booted` + `fps_overlay` - - 统一 `LoadSettings` 契约;fan 走 `settings_save` - - `g_settings` 快照或读写锁 +3. **Ready / Settings 语义收口** — 部分已落地 + - rest delay:daemon 冷启动不再因 `util_booted` sleep(`orion/toolbox_timing.h`) + - Settings store / fan 落盘 / `LoadSettings` 契约 — 见 §2.2 + - bootstrapper sticky flag clear — 仍待 4. **加深模块(架构债)** — **已落地(2026-07-10)** - - 单一 `liborion_elfldr`(合并三份 pt/elfldr,统一 authid 策略) - - 删除 shellui 本地 `if_exists` 前向声明;走 `orion/platform` - - daemon `get_shellui_pid` / `get_game_pid` 收敛到 `orion_proc` + - 单一 `liborion_elfldr`;shellui `orion/platform`;daemon pid → `orion_proc` -5. **补测** - - IPC 组帧/转义/null path 的 host 测试 - - ready 冷启动时序测试(util 先 booted → daemon 不应 sleep rest delay) - - jailbreak 重试条件单元测试 +5. **补测** — **已落地(2026-07-10)** + - `test_ipc_harden` / `test_toolbox_timing` / `test_hijack_retry` --- @@ -198,10 +192,11 @@ Bootstrapper 只 clear util/kstuff/daemon/toolbox,**不清** `util_booted` / ` 架构骨架(双 daemon、共享 `liborion_*`、util 承载重 IO、ready 协议)是清晰且在向深模块演进的。 -当前最危险的不是「缺库」,而是: +当前仍优先关注: -1. **进程标识混用(appid vs pid)** -2. **存活检测条件写反 + 空指针 jailbreak** +1. **进程标识混用(appid vs pid)**(FPS suspend 路径) +2. **Jailbreak retry / null spawn** — 策略已修,实机仍需回归 +3. sticky ready flags(bootstrapper clear) 3. **IPC 未组帧/未转义 + shellui 并发单连接** 4. **ready/settings 生命周期与文档不符** 5. **elfldr/pt/detour 失败未 fail-closed + 三份漂移** diff --git a/source/daemon/include/daemon_ops.hpp b/source/daemon/include/daemon_ops.hpp index 7258631..3fbfa0a 100644 --- a/source/daemon/include/daemon_ops.hpp +++ b/source/daemon/include/daemon_ops.hpp @@ -14,9 +14,19 @@ using clientArgs = orion::IpcClientArgs; extern bool is_handler_enabled; -void LoadSettings(); +/** + * Refresh g_settings from twin config paths when either is newer. + * Returns true if store is usable (including defaults / skip-if-current). + * Missing config is not an error. + */ +bool LoadSettings(); + +/** After settings_save from this process, refresh mtime gate so we don't thrash. */ +void SettingsNoteDiskWritten(); void reply(int sender_socket, bool error, std::string out_var = "Nothing"); +/** Last reply error for BREW_LAST_RET (0 = success, -1 = error). */ +int daemon_last_ipc_error(); bool remount(const char *dev, const char *path, int mnt_flag); int change_permissions_recursive(const char *path); bool test_sb_file(const char *filename); diff --git a/source/daemon/include/globalconf.hpp b/source/daemon/include/globalconf.hpp index 7ea91a9..6c67320 100644 --- a/source/daemon/include/globalconf.hpp +++ b/source/daemon/include/globalconf.hpp @@ -29,7 +29,8 @@ enum StartOpts { TOOLBOX, }; -// Single process store (same schema as util / shellui). -extern orion::Settings g_settings; +// Thread-safe process store (same schema as util / shellui). +// Readers: g_settings.snapshot(). Writers: g_settings.store / update. +extern orion::SettingsStore g_settings; int launchApp(const char *titleId); diff --git a/source/daemon/source/daemon_fs.cpp b/source/daemon/source/daemon_fs.cpp index b68dfa0..16f0955 100644 --- a/source/daemon/source/daemon_fs.cpp +++ b/source/daemon/source/daemon_fs.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -190,7 +191,16 @@ bool test_sb_file(const char *filename) { } +namespace { +std::atomic g_last_ipc_error{0}; +} // namespace + +int daemon_last_ipc_error() { + return g_last_ipc_error.load(std::memory_order_relaxed); +} + void reply(int sender_socket, bool error, std::string out_var) { + g_last_ipc_error.store(error ? -1 : 0, std::memory_order_relaxed); orion::ipc_reply(sender_socket, BREW_RETURN_VALUE, error, out_var); } diff --git a/source/daemon/source/daemon_inject.cpp b/source/daemon/source/daemon_inject.cpp index be5e1e6..e737a8e 100644 --- a/source/daemon/source/daemon_inject.cpp +++ b/source/daemon/source/daemon_inject.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include "globalconf.hpp" #include "dbg/dbg.hpp" #include "elf/elf.hpp" #include "hijacker/hijacker.hpp" @@ -27,8 +29,6 @@ extern const unsigned int fps_elf_size; int _sceApplicationGetAppId(int pid, int *appid); } -extern orion::Settings g_settings; - static void SuspendApp(pid_t pid) { sceKernelPrepareToSuspendProcess(pid); @@ -272,11 +272,21 @@ bool cmd_enable_toolbox(){ } OrionHEN_log("Activating toolbox..."); - /* util_booted: util has completed at least one full start (typed ready flag). */ - if (orion_ready_is_set(ORION_FLAG_UTIL_BOOTED)) { - LoadSettings(); - OrionHEN_log("sleeping for %llu", g_settings.rest_mode_delay_seconds); - sleep(g_settings.rest_mode_delay_seconds); + /* + * Rest_Mode_Delay only on rest resume — never on cold start. + * util_booted is true almost immediately after util starts (before this + * inject), so gating on it alone hung first toolbox load for delay seconds. + * Rest re-activation delay lives in util patch_checker / check_addr_change. + */ + LoadSettings(); + { + const uint64_t delay = g_settings.snapshot().rest_mode_delay_seconds; + constexpr bool kRestResume = false; /* daemon cold/direct inject path */ + if (orion_toolbox_should_apply_rest_delay(kRestResume, delay)) { + OrionHEN_log("rest delay %llu (rest resume path)", + static_cast(delay)); + sleep(static_cast(delay)); + } } /* Prefer kstuff ready marker when present; fall back to short settle. */ diff --git a/source/daemon/source/daemon_jailbreak.cpp b/source/daemon/source/daemon_jailbreak.cpp index 7cb4f27..268ff2d 100644 --- a/source/daemon/source/daemon_jailbreak.cpp +++ b/source/daemon/source/daemon_jailbreak.cpp @@ -11,6 +11,9 @@ #include #include #include +#include +#include +#include "globalconf.hpp" #include #include "../../extern/cJSON/orion_cjson.hpp" @@ -27,7 +30,6 @@ #include #include -extern orion::Settings g_settings; extern bool is_800; namespace { @@ -89,8 +91,11 @@ void *fifo_and_dumper_thread(void *args) noexcept { pthread_mutex_lock(&jb_lock); - if (g_settings.enable_fan_speed) - set_fan_threshold(g_settings.fan_threshold); + { + const orion::Settings cfg = g_settings.snapshot(); + if (cfg.enable_fan_speed) + set_fan_threshold(cfg.fan_threshold); + } int bappid = 0; if (!Get_Running_App_TID(tid, bappid)) { @@ -161,18 +166,21 @@ void *fifo_and_dumper_thread(void *args) noexcept { do { spawned = Hijacker::getHijacker(reserved_value); if (!spawned) { - if (++hijack_retries > 30 || isProcessAlive(reserved_value)) { + ++hijack_retries; + OrionHEN_log("is null for PID %d (attempt %d)", reserved_value, + hijack_retries); + if (orion_hijack_retry_should_stop(isProcessAlive(reserved_value), + hijack_retries, 30)) { orion_notify(true, "Jailbreak failed, PID is invaild"); OrionHEN_log("Jailbreak failed, PID is invaild"); break; } } - OrionHEN_log("is null for PID %d", reserved_value); } while (spawned == nullptr); if (spawned) { OrionHEN_log("RIGHT Jailbreak command received: jailbreaking..."); - if (g_settings.debug_app_jb_msg) + if (g_settings.snapshot().debug_app_jb_msg) orion_notify(true, "App (PID %i) has been granted a jailbreak", reserved_value); spawned->jailbreak(true); diff --git a/source/daemon/source/daemon_settings.cpp b/source/daemon/source/daemon_settings.cpp index 6b89b93..c7bfd8d 100644 --- a/source/daemon/source/daemon_settings.cpp +++ b/source/daemon/source/daemon_settings.cpp @@ -1,65 +1,67 @@ -/* Copyright (C) 2025 OrionHEN / LightningMods */ +/* Copyright (C) 2025 OrionHEN / LightningMods + * + * Process-local settings store + dual-path mtime reload gate. + */ #include "daemon_ops.hpp" #include "globalconf.hpp" #include #include -#include - - -#include +namespace { struct ConfigState { + /** Max mtime of twin paths at last successful store update. */ time_t last_modified = 0; + bool ever_loaded = false; }; ConfigState config_state; -void LoadSettings() { - struct stat file_stat {}; - const char *paths[] = {orion::kConfigPathPrimary, orion::kConfigPathShellui}; +} // namespace - // Prefer primary; fall back to shellui path for mtime / create. - const char *config_path = nullptr; - for (const char *p : paths) { - if (stat(p, &file_stat) == 0) { - config_path = p; - break; - } +orion::SettingsStore g_settings; + +bool LoadSettings() { + const time_t newest = orion::settings_config_newest_mtime(); + + // Skip disk I/O when neither twin is newer than the last applied snapshot. + if (config_state.ever_loaded && !orion::settings_config_is_newer_than( + config_state.last_modified)) { + return true; } - if (!config_path) { + if (newest == 0) { OrionHEN_log("[Daemon] Config file not found. Creating default schema..."); if (orion::settings_ensure_default()) { orion_notify(true, "OrionHEN config created! @ /data/OrionHEN/config.ini"); - config_state.last_modified = 0; } - // Apply defaults even if create failed. - orion::Settings s{}; - orion::settings_load(&s); - g_settings = s; - return; - } - - // Only reload if file has been modified since last load - if (file_stat.st_mtime <= config_state.last_modified) { - return; } OrionHEN_log("[Daemon] Loading Settings from shared schema..."); orion::Settings s{}; - if (!orion::settings_load(&s)) { + const bool from_file = orion::settings_load(&s); + if (!from_file && newest != 0) { orion_notify(true, "Failed to Read the Settings file"); - return; + return false; } - OrionHEN_log("[Daemon] Reading Settings from %s", - orion::settings_last_loaded_path()); + if (from_file) { + OrionHEN_log("[Daemon] Reading Settings from %s", + orion::settings_last_loaded_path()); + } else { + OrionHEN_log("[Daemon] Using default settings (no config file)"); + } OrionHEN_log("fan_threshold: %d", s.fan_threshold); OrionHEN_log("enable_fan_speed: %d", s.enable_fan_speed ? 1 : 0); - g_settings = s; + g_settings.store(s); + config_state.last_modified = orion::settings_config_newest_mtime(); + config_state.ever_loaded = true; + return true; +} - config_state.last_modified = file_stat.st_mtime; +void SettingsNoteDiskWritten() { + config_state.last_modified = orion::settings_config_newest_mtime(); + config_state.ever_loaded = true; } diff --git a/source/daemon/source/ipc_handle.cpp b/source/daemon/source/ipc_handle.cpp index 409bcba..b3efd86 100644 --- a/source/daemon/source/ipc_handle.cpp +++ b/source/daemon/source/ipc_handle.cpp @@ -5,6 +5,7 @@ #include "daemon_ops.hpp" #include "ipc.hpp" #include +#include #include "../../extern/cJSON/orion_cjson.hpp" #include "globalconf.hpp" #include @@ -19,7 +20,7 @@ #include #include -extern orion::Settings g_settings; + bool copyRecursive(const char *source, const char *destination); bool copyFile(const char *source, const char *destination, bool for_dumper); @@ -36,7 +37,6 @@ void handleIPC(clientArgs *client, std::string &inputStr, std::string path_buf, path_buf2, json_path; const char *path = nullptr, *dest = nullptr; char size_buf[0x255]; - bool last_ipc_error = false; std::string out_var = "Nothing"; // default send var @@ -64,7 +64,9 @@ void handleIPC(clientArgs *client, std::string &inputStr, break; } case BREW_LAST_RET: { - reply(sender_app, last_ipc_error, last_ipc_error ? "1" : "0"); + /* Tracks last reply() error for this process (not this call's local). */ + const int last = daemon_last_ipc_error(); + reply(sender_app, last != 0, last != 0 ? "1" : "0"); break; } case BREW_UNUSED_DECRYPT_DIR: @@ -132,6 +134,11 @@ void handleIPC(clientArgs *client, std::string &inputStr, break; case BREW_STAT_CMD: { path = orion_cjson::string_item(my_json.get(), "path"); + if (!path || !*path) { + OrionHEN_log("BREW_STAT_CMD: missing path"); + reply(sender_app, true); + break; + } if (stat(path, &buffer) == 0) { snprintf(size_buf, sizeof(size_buf), "%ld", buffer.st_size); OrionHEN_log("%s exists | size %s", path, size_buf); @@ -143,15 +150,27 @@ void handleIPC(clientArgs *client, std::string &inputStr, break; } case BREW_CALC_DIR_SIZE: { - uint64_t size = calculateTotalSize(orion_cjson::string_item(my_json.get(), "path")); - snprintf(size_buf, sizeof(size_buf), "%lu", size); - OrionHEN_log("size %lu", size_buf); + path = orion_cjson::string_item(my_json.get(), "path"); + if (!path || !*path) { + OrionHEN_log("BREW_CALC_DIR_SIZE: missing path"); + reply(sender_app, true); + break; + } + uint64_t size = calculateTotalSize(path); + snprintf(size_buf, sizeof(size_buf), "%lu", + static_cast(size)); + OrionHEN_log("size %s", size_buf); reply(sender_app, false, size_buf); break; } case BREW_COPY_FILE: { path = orion_cjson::string_item(my_json.get(), "path"); dest = orion_cjson::string_item(my_json.get(), "dest"); + if (!path || !*path || !dest || !*dest) { + OrionHEN_log("BREW_COPY_FILE: missing path/dest"); + reply(sender_app, true); + break; + } if (copyFile(path, dest, false)) { reply(sender_app, false); } else { @@ -163,7 +182,13 @@ void handleIPC(clientArgs *client, std::string &inputStr, case BREW_COPY_DIR: { path = orion_cjson::string_item(my_json.get(), "path"); dest = orion_cjson::string_item(my_json.get(), "dest"); - snprintf(size_buf, sizeof(size_buf), "%lu", calculateTotalSize(path)); + if (!path || !*path || !dest || !*dest) { + OrionHEN_log("BREW_COPY_DIR: missing path/dest"); + reply(sender_app, true); + break; + } + snprintf(size_buf, sizeof(size_buf), "%lu", + static_cast(calculateTotalSize(path))); if (copyRecursive(path, dest)) { reply(sender_app, false, size_buf); } else { @@ -174,6 +199,11 @@ void handleIPC(clientArgs *client, std::string &inputStr, } case BREW_DELETE_DIR: { path = orion_cjson::string_item(my_json.get(), "path"); + if (!path || !*path) { + OrionHEN_log("BREW_DELETE_DIR: missing path"); + reply(sender_app, true); + break; + } if (rmtree(path)) { reply(sender_app, false); } else { @@ -182,7 +212,13 @@ void handleIPC(clientArgs *client, std::string &inputStr, break; } case BREW_TEST_SB_FILE: { - reply(sender_app, !test_sb_file(orion_cjson::string_item(my_json.get(), "path"))); + path = orion_cjson::string_item(my_json.get(), "path"); + if (!path || !*path) { + OrionHEN_log("BREW_TEST_SB_FILE: missing path"); + reply(sender_app, true); + break; + } + reply(sender_app, !test_sb_file(path)); break; } case BREW_DAEMON_PID: { @@ -206,18 +242,32 @@ void handleIPC(clientArgs *client, std::string &inputStr, break; } - g_settings.enable_fan_speed = enabled; - if (!enabled) { orion_notify(true, "Fan speed adjustment is disabled."); set_fan_threshold(77); + const orion::Settings saved = g_settings.update([](orion::Settings &s) { + s.enable_fan_speed = false; + }); + if (orion::settings_save(saved)) { + SettingsNoteDiskWritten(); + } else { + OrionHEN_log("Fan disable: failed to persist settings"); + } reply(sender_app, false); break; } if (set_fan_threshold(speed)) { orion_notify(true, "Fan threshold adjusted to %i°C.", speed); - g_settings.fan_threshold = speed; + const orion::Settings saved = g_settings.update([speed](orion::Settings &s) { + s.enable_fan_speed = true; + s.fan_threshold = speed; + }); + if (orion::settings_save(saved)) { + SettingsNoteDiskWritten(); + } else { + OrionHEN_log("Fan enable: memory updated but disk twin save failed"); + } reply(sender_app, false); } else { orion_notify(true, "Failed to adjust fan speed."); diff --git a/source/daemon/source/main.cpp b/source/daemon/source/main.cpp index d4e4de1..3b67e7d 100644 --- a/source/daemon/source/main.cpp +++ b/source/daemon/source/main.cpp @@ -126,7 +126,6 @@ extern "C" { // Global variables uint64_t p_syscall = 0; char _end[1] = {}; -orion::Settings g_settings; int fd = -1; static constexpr auto DEFAULT_PRIORITY = 256; uintptr_t kernel_base = 0; @@ -369,7 +368,8 @@ int main() { OrionHEN_log("is toolbox only: %s | ver: %x", toolbox_only ? "Yes" : "No", sys_ver.version); - if (g_settings.toolbox_auto_start) { + const orion::Settings boot_cfg = g_settings.snapshot(); + if (boot_cfg.toolbox_auto_start) { cmd_enable_toolbox(); } else { orion_notify(true, @@ -382,11 +382,11 @@ int main() { OrionHEN_log("StartUp thread created!! - welcome to OrionHEN"); if (const char* uri = - startup_uri_for_option(g_settings.start_option, g_settings.toolbox_auto_start)) { + startup_uri_for_option(boot_cfg.start_option, boot_cfg.toolbox_auto_start)) { OrionHEN_log("ret %d", ItemzLaunchByUri(uri)); } - if (g_settings.auto_eject_disc) + if (boot_cfg.auto_eject_disc) sceShellCoreUtilRequestEjectDevice("/dev/cd0"); ipc_supervisor_loop(&msg_thr); diff --git a/source/include/orion/hijack_retry.h b/source/include/orion/hijack_retry.h new file mode 100644 index 0000000..7be05b2 --- /dev/null +++ b/source/include/orion/hijack_retry.h @@ -0,0 +1,43 @@ +/* Copyright (C) 2025 OrionHEN / LightningMods + * + * Pure jailbreak hijack-retry policy (host-testable). + * + * After getHijacker(pid) fails: + * - stop if process is dead (nothing left to attach) + * - stop if attempt count exceeded max + * - otherwise retry while the target is still alive + */ + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @param process_alive true if pid still exists + * @param attempt 1-based attempt number after this failure (first fail = 1) + * @param max_attempts stop after this many failed attempts (default policy 30) + * @return true if the retry loop should abort (fail) + */ +static inline bool orion_hijack_retry_should_stop(bool process_alive, + int attempt, + int max_attempts) { + if (max_attempts < 1) { + max_attempts = 1; + } + if (attempt >= max_attempts) { + return true; + } + /* Target gone — no point retrying. */ + if (!process_alive) { + return true; + } + return false; +} + +#ifdef __cplusplus +} +#endif diff --git a/source/include/orion/toolbox_timing.h b/source/include/orion/toolbox_timing.h new file mode 100644 index 0000000..ae2804f --- /dev/null +++ b/source/include/orion/toolbox_timing.h @@ -0,0 +1,30 @@ +/* Copyright (C) 2025 OrionHEN / LightningMods + * + * Pure policy for toolbox inject rest-mode delay (host-testable). + * + * Cold start: util signals util_booted before daemon injects — that must NOT + * apply Rest_Mode_Delay_Seconds (would hang first toolbox load). + * Rest resume: only then apply configured delay (util patch_checker path). + */ + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @param rest_resume true only when recovering from rest mode (not cold boot) + * @param delay_sec configured Rest_Mode_Delay_Seconds + */ +static inline bool orion_toolbox_should_apply_rest_delay(bool rest_resume, + uint64_t delay_sec) { + return rest_resume && delay_sec > 0; +} + +#ifdef __cplusplus +} +#endif diff --git a/source/liborion_ipc/include/orion/ipc_client.hpp b/source/liborion_ipc/include/orion/ipc_client.hpp index c529dce..feba9de 100644 --- a/source/liborion_ipc/include/orion/ipc_client.hpp +++ b/source/liborion_ipc/include/orion/ipc_client.hpp @@ -17,6 +17,7 @@ along with this program; see the file COPYING. If not, see #pragma once #include +#include #include // --------------------------------------------------------------------------- @@ -25,6 +26,8 @@ along with this program; see the file COPYING. If not, see // Design rules: // - One connection object per target (crit vs util). Never flip a flag on a // shared singleton — that races MainDaemonSocket / UtilDaemonSocket. +// - IPCSendCommand is serialized with a per-instance mutex (shellui hooks + +// background threads may share the same singleton). // - Implementation lives in the .cpp (not header-only). // - JSON string fields are built with cJSON (escape-safe). // --------------------------------------------------------------------------- @@ -102,8 +105,14 @@ private: bool util_daemon_; int socket_fd_; int recv_timeout_ms_; + mutable std::mutex mu_; const char *socket_path() const; bool require_util(const char *what) const; bool require_crit(const char *what) const; + + /** Unlocked: full-frame send of IPCMessage. */ + int send_frame_unlocked(const IPCMessage &msg); + /** Unlocked: full-frame recv into msg; forces NUL on payload. */ + int recv_frame_unlocked(IPCMessage &msg); }; diff --git a/source/liborion_ipc/include/orion/ipc_server.hpp b/source/liborion_ipc/include/orion/ipc_server.hpp index d3f67bd..f3f7c02 100644 --- a/source/liborion_ipc/include/orion/ipc_server.hpp +++ b/source/liborion_ipc/include/orion/ipc_server.hpp @@ -17,6 +17,7 @@ along with this program; see the file COPYING. If not, see #pragma once #include +#include #include // --------------------------------------------------------------------------- @@ -49,20 +50,47 @@ void ipc_server_set_log(IpcServerLogFn fn); // --- transport primitives --- int ipc_network_listen(const char *soc_path); int ipc_network_accept(int socket_fd); + +/** Single recv (may be short). Prefer ipc_network_recv_full for frames. */ int ipc_network_recv(int socket_fd, void *buffer, int32_t size); + +/** + * Read exactly `size` bytes (loop on short reads / EINTR). + * Returns size on success, 0 on clean EOF before any data, 0 < n < size on + * short EOF, -1 on error. + */ +int ipc_network_recv_full(int socket_fd, void *buffer, int32_t size); + +/** Send exactly `size` bytes (loop on short writes / EINTR). */ +int ipc_network_send_full(int socket_fd, const void *buffer, int32_t size); + int ipc_network_send(int socket_fd, void *buffer, int32_t size); int ipc_network_close(int socket_fd); -/** - * Pure: JSON body for daemon/util replies. - * Shape is fixed for injectee parsers: {"res":N, "var":"..."}. - */ -inline std::string ipc_format_reply_body(bool error, - const std::string &out_var) { - return std::string("{\"res\":") + std::to_string(error ? -1 : 0) + - ", \"var\":\"" + out_var + "\"}"; +// --- pure wire helpers (host-testable) --- + +/** True when nbytes matches a full IPCMessage frame. */ +inline bool ipc_frame_is_complete(int nbytes) { + return nbytes == static_cast(sizeof(IPCMessage)); } +/** Force trailing NUL on the payload buffer (msg is char[DAEMON_BUFF_MAX]). */ +inline void ipc_message_force_nul(IPCMessage &msg) { + msg.msg[sizeof(msg.msg) - 1] = '\0'; +} + +/** + * Escape a string for inclusion inside a JSON double-quoted value. + * Handles \, ", control chars (as \u00XX). Pure — no I/O. + */ +std::string ipc_json_escape(const std::string &in); + +/** + * JSON body for daemon/util replies. + * Shape: {"res":N,"var":"..."} with `var` properly escaped. + */ +std::string ipc_format_reply_body(bool error, const std::string &out_var); + // Build {"res":N,"var":"..."} reply with the process's return command ordinal. void ipc_reply(int sender_socket, DaemonCommands reply_cmd, bool error, const std::string &out_var = "Nothing"); diff --git a/source/liborion_ipc/source/ipc_client.cpp b/source/liborion_ipc/source/ipc_client.cpp index cf015ab..3f95cc7 100644 --- a/source/liborion_ipc/source/ipc_client.cpp +++ b/source/liborion_ipc/source/ipc_client.cpp @@ -15,6 +15,7 @@ along with this program; see the file COPYING. If not, see . */ #include +#include #include #include @@ -165,9 +166,22 @@ bool IPC_Client::IPCOpenIfNotConnected() { return IPCOpenConnection(); } -int IPC_Client::IPCReceiveData(IPCMessage &msg, std::string &ipc_msg) { - bzero(msg.msg, sizeof(msg.msg)); +int IPC_Client::send_frame_unlocked(const IPCMessage &msg) { + if (socket_fd_ < 0) { + return -1; + } + const int ret = orion::ipc_network_send_full( + socket_fd_, &msg, static_cast(sizeof(msg))); + if (ret < 0) { + shellui_log("IPCSendData failed: %s", strerror(errno)); + } else { + shellui_log("IPCSendData sent %i bytes", ret); + } + return ret; +} +int IPC_Client::recv_frame_unlocked(IPCMessage &msg) { + bzero(msg.msg, sizeof(msg.msg)); if (socket_fd_ < 0) { return -1; } @@ -181,12 +195,25 @@ int IPC_Client::IPCReceiveData(IPCMessage &msg, std::string &ipc_msg) { shellui_log("Waiting for daemon response (%s)...", util_daemon_ ? "util" : "crit"); - int ret = recv(socket_fd_, reinterpret_cast(&msg), sizeof(msg), - MSG_NOSIGNAL); + const int ret = orion::ipc_network_recv_full( + socket_fd_, &msg, static_cast(sizeof(msg))); if (ret < 0) { shellui_log("recv failed: %s", strerror(errno)); return ret; } + if (!orion::ipc_frame_is_complete(ret)) { + shellui_log("short IPC frame %d (want %zu)", ret, sizeof(msg)); + return -1; + } + orion::ipc_message_force_nul(msg); + return ret; +} + +int IPC_Client::IPCReceiveData(IPCMessage &msg, std::string &ipc_msg) { + const int ret = recv_frame_unlocked(msg); + if (ret < 0) { + return ret; + } shellui_log("Daemon returned: %i", msg.error); if (msg.error != 0) { @@ -218,17 +245,7 @@ int IPC_Client::IPCReceiveData(IPCMessage &msg, std::string &ipc_msg) { } int IPC_Client::IPCSendData(const IPCMessage &msg) { - if (socket_fd_ < 0) { - return -1; - } - int ret = send(socket_fd_, reinterpret_cast(&msg), sizeof(msg), - MSG_NOSIGNAL); - if (ret < 0) { - shellui_log("IPCSendData failed: %s", strerror(errno)); - } else { - shellui_log("IPCSendData sent %i bytes", ret); - } - return ret; + return send_frame_unlocked(msg); } int IPC_Client::IPCCloseConnection() { @@ -242,6 +259,7 @@ int IPC_Client::IPCCloseConnection() { bool IPC_Client::IPCSendCommand(DaemonCommands cmd, std::string &ipc_msg1, std::string ipc_msg2) { + std::lock_guard lock(mu_); std::string json; #if SHELL_DEBUG == 1 @@ -258,7 +276,8 @@ bool IPC_Client::IPCSendCommand(DaemonCommands cmd, std::string &ipc_msg1, json = json_kv_string2("mount_src", ipc_msg1.c_str(), "mount_dest", ipc_msg2.c_str()); } else if (ipc_msg2.empty()) { - if (cmd == BREW_DAEMON_PID || cmd == BREW_UTIL_DAEMON_PID) { + if (cmd == BREW_DAEMON_PID || cmd == BREW_UTIL_DAEMON_PID || + cmd == BREW_TEST_CONNECTION || cmd == BREW_UTIL_TEST_CONNECTION) { json = "{\"pid\": 0 }"; } else { json = "{\"msg_1\": 0}"; @@ -273,8 +292,9 @@ bool IPC_Client::IPCSendCommand(DaemonCommands cmd, std::string &ipc_msg1, } snprintf(msg.msg, sizeof(msg.msg), "%s", json.c_str()); + orion::ipc_message_force_nul(msg); - if (IPCSendData(msg) < 0) { + if (send_frame_unlocked(msg) < 0) { shellui_log("Failed to send message to daemon"); maybe_notify("Failed to send message to daemon"); IPCCloseConnection(); diff --git a/source/liborion_ipc/source/ipc_server.cpp b/source/liborion_ipc/source/ipc_server.cpp index 4228bdd..2770b69 100644 --- a/source/liborion_ipc/source/ipc_server.cpp +++ b/source/liborion_ipc/source/ipc_server.cpp @@ -61,10 +61,20 @@ void *client_thread(void *arg) { logf("[%s] Thread created for Socket %i", tag, client->socket); - IPCMessage ipcMessage{}; - int readSize = 0; - while ((readSize = ipc_network_recv(client->socket, &ipcMessage, - sizeof(ipcMessage))) > 0) { + while (true) { + IPCMessage ipcMessage{}; + const int readSize = ipc_network_recv_full(client->socket, &ipcMessage, + static_cast(sizeof(ipcMessage))); + if (readSize <= 0) { + break; + } + if (!ipc_frame_is_complete(readSize)) { + logf("[%s][client %i] short frame %d (want %zu)", tag, client->cl_nmb, + readSize, sizeof(ipcMessage)); + break; + } + ipc_message_force_nul(ipcMessage); + if (ipcMessage.magic == static_cast(0xDEADBABE)) { std::string message = ipcMessage.msg; if (handler) { @@ -73,7 +83,8 @@ void *client_thread(void *arg) { } else { logf("[%s][client %i] Invalid magic number", tag, client->cl_nmb); ipcMessage.error = -1; - ipc_network_send(client->socket, &ipcMessage, sizeof(ipcMessage)); + ipc_network_send_full(client->socket, &ipcMessage, + static_cast(sizeof(ipcMessage))); } } @@ -120,7 +131,9 @@ int ipc_network_listen(const char *soc_path) { return s; } -int ipc_network_accept(int socket_fd) { return accept(socket_fd, nullptr, nullptr); } +int ipc_network_accept(int socket_fd) { + return accept(socket_fd, nullptr, nullptr); +} int ipc_network_recv(int socket_fd, void *buffer, int32_t size) { int n = recv(socket_fd, buffer, size, 0); @@ -128,12 +141,106 @@ int ipc_network_recv(int socket_fd, void *buffer, int32_t size) { return n; } +int ipc_network_recv_full(int socket_fd, void *buffer, int32_t size) { + if (!buffer || size <= 0) { + return -1; + } + auto *p = static_cast(buffer); + int32_t got = 0; + while (got < size) { + const int n = recv(socket_fd, p + got, static_cast(size - got), 0); + if (n < 0) { + if (errno == EINTR) { + continue; + } + logf("recv_full error: %s", strerror(errno)); + return -1; + } + if (n == 0) { + logf("recv_full EOF after %d / %d", got, size); + return got; + } + got += n; + } + logf("got %i bytes (full frame)", got); + return got; +} + int ipc_network_send(int socket_fd, void *buffer, int32_t size) { return send(socket_fd, buffer, size, MSG_NOSIGNAL); } +int ipc_network_send_full(int socket_fd, const void *buffer, int32_t size) { + if (!buffer || size <= 0) { + return -1; + } + const auto *p = static_cast(buffer); + int32_t sent = 0; + while (sent < size) { + const int n = + send(socket_fd, p + sent, static_cast(size - sent), MSG_NOSIGNAL); + if (n < 0) { + if (errno == EINTR) { + continue; + } + return -1; + } + if (n == 0) { + return sent; + } + sent += n; + } + return sent; +} + int ipc_network_close(int socket_fd) { return close(socket_fd); } +std::string ipc_json_escape(const std::string &in) { + std::string out; + out.reserve(in.size() + 8); + for (unsigned char c : in) { + switch (c) { + case '\\': + out += "\\\\"; + break; + case '"': + out += "\\\""; + break; + case '\b': + out += "\\b"; + break; + case '\f': + out += "\\f"; + break; + case '\n': + out += "\\n"; + break; + case '\r': + out += "\\r"; + break; + case '\t': + out += "\\t"; + break; + default: + if (c < 0x20) { + char hex[8]; + snprintf(hex, sizeof(hex), "\\u%04x", c); + out += hex; + } else { + out += static_cast(c); + } + break; + } + } + return out; +} + +std::string ipc_format_reply_body(bool error, const std::string &out_var) { + /* Compact JSON; var is always a quoted escaped string. */ + return std::string("{\"res\":") + std::to_string(error ? -1 : 0) + + ",\"var\":\"" + ipc_json_escape(out_var) + "\"}"; +} + void ipc_reply(int sender_socket, DaemonCommands reply_cmd, bool error, const std::string &out_var) { const std::string body = ipc_format_reply_body(error, out_var); @@ -144,10 +251,11 @@ void ipc_reply(int sender_socket, DaemonCommands reply_cmd, bool error, outputMessage.error = error ? -1 : 0; bzero(outputMessage.msg, sizeof(outputMessage.msg)); strncpy(outputMessage.msg, body.c_str(), sizeof(outputMessage.msg) - 1); - outputMessage.msg[sizeof(outputMessage.msg) - 1] = '\0'; + ipc_message_force_nul(outputMessage); logf("error: %d", outputMessage.error); - ipc_network_send(sender_socket, &outputMessage, sizeof(outputMessage)); + ipc_network_send_full(sender_socket, &outputMessage, + static_cast(sizeof(outputMessage))); } void *ipc_server_loop(void *options_ptr) { diff --git a/source/liborion_settings/include/orion/settings.hpp b/source/liborion_settings/include/orion/settings.hpp index e70e158..dd36af6 100644 --- a/source/liborion_settings/include/orion/settings.hpp +++ b/source/liborion_settings/include/orion/settings.hpp @@ -16,9 +16,6 @@ along with this program; see the file COPYING. If not, see #pragma once -#include -#include - // --------------------------------------------------------------------------- // Single product config schema for daemon / util / shellui. // @@ -26,12 +23,22 @@ along with this program; see the file COPYING. If not, see // primary /data/OrionHEN/config.ini (elevated daemons) // shellui /user/data/OrionHEN/config.ini (SceShellUI sandbox view) // Load tries both; save writes all writable targets so the files stay twins. +// +// Process-local store: use SettingsStore for multi-threaded daemons (snapshot +// under mutex). Multi-process copies of Settings are expected and OK. +// +// Force C++ linkage even when a parent includes us under extern "C" +// (e.g. util common_utils.h inside extern "C" blocks). // --------------------------------------------------------------------------- -// Force C++ linkage even if a parent header included us under extern "C". #ifdef __cplusplus extern "C++" { -#endif + +#include +#include +#include +#include +#include namespace orion { @@ -82,6 +89,37 @@ struct Settings { int schema_version = kSettingsSchemaVersion; }; +// Thread-safe process-local settings (daemon / util IPC + worker threads). +// ShellUI may keep a plain Settings if UI work is single-threaded; prefer this +// store when readers and writers can race. +class SettingsStore { +public: + SettingsStore() = default; + explicit SettingsStore(const Settings &s) : s_(s) {} + + Settings snapshot() const { + std::lock_guard lock(mu_); + return s_; + } + + void store(const Settings &s) { + std::lock_guard lock(mu_); + s_ = s; + } + + /** Mutate under lock; returns a copy of the new value. */ + template + Settings update(Fn &&fn) { + std::lock_guard lock(mu_); + fn(s_); + return s_; + } + +private: + mutable std::mutex mu_; + Settings s_{}; +}; + // Fill `out` with defaults then overlay values from the first readable config path. // Returns true if a file was loaded; false means defaults only (file missing). bool settings_load(Settings *out); @@ -107,8 +145,19 @@ const char *settings_last_loaded_path(); // USB override used historically by daemons. bool settings_usb_disables_toolbox_auto_start(); +/** + * Max st_mtime across both twin config paths (0 if neither exists). + * Use for daemon reload gating so either path's write invalidates the cache. + */ +time_t settings_config_newest_mtime(); + +/** + * True if any twin path has mtime strictly greater than `since`. + * When no config file exists, returns false (nothing newer on disk). + */ +bool settings_config_is_newer_than(time_t since); + } // namespace orion -#ifdef __cplusplus } // extern "C++" -#endif +#endif /* __cplusplus */ diff --git a/source/liborion_settings/source/settings.cpp b/source/liborion_settings/source/settings.cpp index f7d6ad6..a97f633 100644 --- a/source/liborion_settings/source/settings.cpp +++ b/source/liborion_settings/source/settings.cpp @@ -243,4 +243,29 @@ bool settings_usb_disables_toolbox_auto_start() { return stat("/mnt/usb0/toolbox_auto_start", &st) == 0; } +static time_t path_mtime(const char *path) { + if (!path) { + return 0; + } + struct stat st {}; + if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) { + return 0; + } + return st.st_mtime; +} + +time_t settings_config_newest_mtime() { + const time_t a = path_mtime(kConfigPathPrimary); + const time_t b = path_mtime(kConfigPathShellui); + return a > b ? a : b; +} + +bool settings_config_is_newer_than(time_t since) { + const time_t newest = settings_config_newest_mtime(); + if (newest == 0) { + return false; + } + return newest > since; +} + } // namespace orion diff --git a/source/shellui/src/settings_ui.cpp b/source/shellui/src/settings_ui.cpp index 60f4b36..5a74e28 100644 --- a/source/shellui/src/settings_ui.cpp +++ b/source/shellui/src/settings_ui.cpp @@ -60,19 +60,20 @@ void apply_overlay_layout() { bool LoadSettings() { orion::Settings s{}; - const bool loaded = orion::settings_load(&s); - if (!loaded) { + /* false from settings_load means defaults only — not a hard failure. */ + if (!orion::settings_load(&s)) { shellui_log("config.ini missing; using defaults"); } else { shellui_log("Loaded settings from %s", orion::settings_last_loaded_path()); } - // g_settings is the single store; single store for all consumers. + // Process-local store (UI thread); twin disk paths via settings_load/save. g_settings = s; if (g_settings.overlay_fps) { orion_ready_signal(ORION_FLAG_FPS_OVERLAY); } apply_overlay_layout(); + /* Always true once defaults-or-file applied (prx boot requires success). */ return true; } diff --git a/source/util/include/common_utils.h b/source/util/include/common_utils.h index 8b45e01..9ff3279 100644 --- a/source/util/include/common_utils.h +++ b/source/util/include/common_utils.h @@ -92,10 +92,15 @@ typedef struct notify_request } notify_request_t; /*================ SETTINGS ==============*/ -/* Persisted keys: orion::Settings. Concurrent CMD flags stay atomic. */ +/* Persisted keys: orion::SettingsStore. Concurrent CMD flags stay atomic. + * Always C++ linkage even when this header is included under extern "C". */ #ifdef __cplusplus +extern "C++" { #include -extern orion::Settings g_settings; +extern orion::SettingsStore g_settings; +/** Reload twin config into g_settings; missing file applies defaults (true). */ +bool LoadSettings(); +} /* extern "C++" */ #endif extern atomic_bool g_legacy_cmd_server; extern atomic_bool g_legacy_cmd_server_exit; @@ -153,10 +158,11 @@ bool IniliatizeHTTP(void); /* download/extract/shutdown: see extern "C" block below */ /*============ Back up JB server ==============*/ -int get_ip_address(char *ip_address); +/* Always C linkage (impl in common_utils.c) regardless of include context. */ #ifdef __cplusplus extern "C" { #endif +int get_ip_address(char *ip_address); #include #include int sceNetCtlInit(void); diff --git a/source/util/source/cpp_service.cpp b/source/util/source/cpp_service.cpp index 7b168b5..d8ee4e9 100644 --- a/source/util/source/cpp_service.cpp +++ b/source/util/source/cpp_service.cpp @@ -18,6 +18,9 @@ along with this program; see the file COPYING. If not, see #include #include #include +#include +#include +#include "common_utils.h" #include #include #include @@ -86,9 +89,8 @@ pthread_mutex_t jb_lock = PTHREAD_MUTEX_INITIALIZER; extern atomic_bool not_connected; // Function forward declarations -bool LoadSettings(); void check_addr_change(void); -int get_ip_address(char *ip_address); +// get_ip_address: common_utils.h (always C linkage) // util_toolbox.cpp bool enable_toolbox(); @@ -135,25 +137,28 @@ void cmd_server(int sock, Command &cmd) { OrionHEN_log("WRONG Jailbreak command received: jailbreaking..."); { - do { + do { spawned = Hijacker::getHijacker(cmd.PID); if (spawned == nullptr) { - if (isProcessAlive(cmd.PID)) { - OrionHEN_log("process died"); - break; - } retries++; - if (retries > 30) { + OrionHEN_log("is null for PID %d (attempt %d)", cmd.PID, + retries); + if (orion_hijack_retry_should_stop(isProcessAlive(cmd.PID), + retries, 30)) { orion_notify(true, "Jailbreak failed, PID is invaild"); OrionHEN_log("Jailbreak failed, PID is invaild"); break; } } - OrionHEN_log("is null for PID %d", cmd.PID); } while (spawned == nullptr); retries = 0; + if (!spawned) { + replyError(sock); + break; + } + orion_notify(true, "[Legacy] App has been granted a jailbreak\n\nAn update for " "this PKG is available"); spawned->jailbreak(true); @@ -274,8 +279,11 @@ void check_addr_change(void) { } else if (rest_mode_action && !no_network_patched && !not_connected && real_rest_mode_detected) { LoadSettings(); - OrionHEN_log("sleeping for %lld secs", g_settings.rest_mode_delay_seconds); - sleep(g_settings.rest_mode_delay_seconds); + const uint64_t delay = + g_settings.snapshot().rest_mode_delay_seconds; + OrionHEN_log("sleeping for %llu secs", + static_cast(delay)); + sleep(static_cast(delay)); orion_notify(true, "Coming out of Rest Mode detected, restarting server(s)"); OrionHEN_log("waiting for logged in user"); @@ -287,7 +295,10 @@ void check_addr_change(void) { OrionHEN_log("Coming out rest mode, activating patches"); - if (g_settings.toolbox_auto_start && !g_settings.disable_toolbox_auto_start_for_rest_mode && !enable_toolbox()) { + const orion::Settings cfg = g_settings.snapshot(); + if (cfg.toolbox_auto_start && + !cfg.disable_toolbox_auto_start_for_rest_mode && + !enable_toolbox()) { orion_notify(true, "Failed to inject toolbox"); } } diff --git a/source/util/source/ipc_handle.cpp b/source/util/source/ipc_handle.cpp index e07e031..85b8034 100644 --- a/source/util/source/ipc_handle.cpp +++ b/source/util/source/ipc_handle.cpp @@ -39,7 +39,6 @@ extern atomic_bool g_legacy_cmd_server; extern atomic_bool g_legacy_cmd_server_exit; void reply(int sender_socket, bool error, std::string out_var = "Nothing"); -bool LoadSettings(); extern "C" { bool load_plugin(const char *path); int launchApp(const char *titleId); @@ -73,6 +72,10 @@ void handleIPC(clientArgs *client, std::string &inputStr, } switch (command) { + case BREW_UTIL_TEST_CONNECTION: { + reply(sender_app, false, out_var); + break; + } case BREW_UTIL_SHELLUI_ON_STANDBY: { OrionHEN_log("ShellUI on standby"); real_rest_mode_detected = no_network_rest_mode_action = true; diff --git a/source/util/source/main.cpp b/source/util/source/main.cpp index 7deddf9..56d39d6 100644 --- a/source/util/source/main.cpp +++ b/source/util/source/main.cpp @@ -54,7 +54,7 @@ extern "C" { extern bool is_handler_enabled; -orion::Settings g_settings; +orion::SettingsStore g_settings; atomic_bool g_legacy_cmd_server = false; atomic_bool g_legacy_cmd_server_exit = false; void start_ip_thread(void); @@ -78,7 +78,7 @@ void __stack_chk_fail(void) { puts("Stack smashing detected."); } -void LoadSettings(void) { +bool LoadSettings() { orion::Settings s{}; if (!orion::settings_load(&s)) { OrionHEN_log("config.ini missing; using defaults (path primary=%s)", @@ -87,9 +87,12 @@ void LoadSettings(void) { OrionHEN_log("Loaded settings from %s", orion::settings_last_loaded_path()); } - g_settings = s; + g_settings.store(s); g_legacy_cmd_server = s.legacy_cmd_server; + /* Missing file is not an error — defaults were applied. */ + return true; } + int main(void) { pthread_t ipc_server = 0; char tmp_buf[200]; @@ -112,9 +115,6 @@ int main(void) { kernel_base = args->kdata_base_addr; set_proc_authid(getpid(), DEBUG_AUTHID); - - g_settings.rest_mode_delay_seconds = 0; - g_settings.toolbox_auto_start = true; g_legacy_cmd_server_exit = false; unlink("/data/OrionHEN/OrionHEN_util_daemon.log"); @@ -134,7 +134,8 @@ int main(void) { orion_notify(true, "Failed to initialize the HTTP lib, downloading cheats will not work"); } - if (g_settings.toolbox_auto_start && orion_ready_is_set(ORION_FLAG_UTIL_BOOTED)) { + if (g_settings.snapshot().toolbox_auto_start && + orion_ready_is_set(ORION_FLAG_UTIL_BOOTED)) { OrionHEN_log("util already booted once — activating toolbox path"); patch_checker(); } @@ -143,7 +144,8 @@ int main(void) { for (;;) { // for rest mode we wait til we can restart everything - if (g_settings.toolbox_auto_start && get_ip_address(&tmp_buf[0]) < 0) { + if (g_settings.snapshot().toolbox_auto_start && + get_ip_address(&tmp_buf[0]) < 0) { sleep(1); bool fail1 = get_ip_address(&tmp_buf[0]) < 0; diff --git a/source/util/source/msg.cpp b/source/util/source/msg.cpp index 02e8b55..24df4d8 100644 --- a/source/util/source/msg.cpp +++ b/source/util/source/msg.cpp @@ -63,7 +63,6 @@ extern char ip_address[]; extern "C" int launchApp(const char *titleId); void activate_shellui_patch(); -bool LoadSettings(); struct sockaddr_in networkAdress(uint16_t port) { diff --git a/source/util/source/util_toolbox.cpp b/source/util/source/util_toolbox.cpp index bce56e3..2502c86 100644 --- a/source/util/source/util_toolbox.cpp +++ b/source/util/source/util_toolbox.cpp @@ -5,12 +5,11 @@ #include #include #include +#include "common_utils.h" #include #include #include -extern orion::Settings g_settings; -void LoadSettings(void); extern std::atomic_bool no_network_rest_mode_action; extern std::atomic_bool no_network_patched; extern std::atomic_bool real_rest_mode_detected; @@ -71,12 +70,14 @@ void patch_checker() { } LoadSettings(); - if(g_settings.disable_toolbox_auto_start_for_rest_mode){ + const orion::Settings cfg = g_settings.snapshot(); + if (cfg.disable_toolbox_auto_start_for_rest_mode) { OrionHEN_log("Toolbox auto start for rest mode is disabled"); return; } - OrionHEN_log("sleeping for %lld secs", g_settings.rest_mode_delay_seconds); - sleep(g_settings.rest_mode_delay_seconds); + OrionHEN_log("sleeping for %llu secs", + static_cast(cfg.rest_mode_delay_seconds)); + sleep(static_cast(cfg.rest_mode_delay_seconds)); orion_notify(true, "(No Network) Coming out of Rest Mode detected\nre-activating " "the OrionHEN toolbox..."); diff --git a/source/util/tests/Makefile b/source/util/tests/Makefile index 754b740..4c3efbc 100644 --- a/source/util/tests/Makefile +++ b/source/util/tests/Makefile @@ -63,6 +63,7 @@ PROD_CXX_SRCS := \ $(UTIL_ROOT)/source/http_github.cpp \ $(REPO_ROOT)/source/extern/cJSON/cJSON.cpp \ $(REPO_ROOT)/source/liborion_settings/source/settings.cpp \ + $(REPO_ROOT)/source/liborion_ipc/source/ipc_server.cpp \ $(REPO_ROOT)/source/shellui/src/ps5_settings_ui.cpp \ $(REPO_ROOT)/source/shellui/src/toolbox_route.cpp @@ -82,12 +83,15 @@ TEST_C_SRCS := \ test_platform_fs.c \ test_platform_log.c \ test_platform_notify.c \ + test_toolbox_timing.c \ + test_hijack_retry.c \ stubs/host_stubs.c TEST_CXX_SRCS := \ test_cheat_parsers.cpp \ test_settings.cpp \ test_msg_protocol.cpp \ + test_ipc_harden.cpp \ test_ps5_settings_ui.cpp \ test_toolbox_route.cpp \ test_toolbox_helpers.cpp \ @@ -106,6 +110,7 @@ PROD_C_OBJS += $(BUILD_DIR)/prod/liborion_playtime/playtime.o PROD_CXX_OBJS := $(patsubst $(UTIL_ROOT)/%.cpp,$(BUILD_DIR)/prod/%.o,$(filter $(UTIL_ROOT)/%,$(PROD_CXX_SRCS))) PROD_CXX_OBJS += $(BUILD_DIR)/prod/extern/cJSON.o PROD_CXX_OBJS += $(BUILD_DIR)/prod/liborion_settings/settings.o +PROD_CXX_OBJS += $(BUILD_DIR)/prod/liborion_ipc/ipc_server.o PROD_CXX_OBJS += $(BUILD_DIR)/prod/shellui/ps5_settings_ui.o PROD_CXX_OBJS += $(BUILD_DIR)/prod/shellui/toolbox_route.o @@ -152,6 +157,10 @@ $(BUILD_DIR)/prod/liborion_settings/settings.o: $(REPO_ROOT)/source/liborion_set @mkdir -p $(dir $@) $(HOST_CXX) $(CXXFLAGS) -c -o $@ $< +$(BUILD_DIR)/prod/liborion_ipc/ipc_server.o: $(REPO_ROOT)/source/liborion_ipc/source/ipc_server.cpp | $(BUILD_DIR) + @mkdir -p $(dir $@) + $(HOST_CXX) $(CXXFLAGS) -c -o $@ $< + $(BUILD_DIR)/prod/shellui/ps5_settings_ui.o: $(REPO_ROOT)/source/shellui/src/ps5_settings_ui.cpp | $(BUILD_DIR) @mkdir -p $(dir $@) $(HOST_CXX) $(CXXFLAGS) -c -o $@ $< diff --git a/source/util/tests/test_hijack_retry.c b/source/util/tests/test_hijack_retry.c new file mode 100644 index 0000000..af1f72d --- /dev/null +++ b/source/util/tests/test_hijack_retry.c @@ -0,0 +1,43 @@ +/* Host tests: jailbreak hijack retry stop condition. */ +#include "test_harness.h" + +#include + +static int test_alive_retries_until_max(void) { + /* Process alive → keep retrying until attempt reaches max. */ + TEST_ASSERT_TRUE(!orion_hijack_retry_should_stop(true, 1, 30)); + TEST_ASSERT_TRUE(!orion_hijack_retry_should_stop(true, 29, 30)); + TEST_ASSERT_TRUE(orion_hijack_retry_should_stop(true, 30, 30)); + TEST_ASSERT_TRUE(orion_hijack_retry_should_stop(true, 31, 30)); + return 0; +} + +static int test_dead_process_stops_immediately(void) { + /* Dead process → stop even on first attempt. */ + TEST_ASSERT_TRUE(orion_hijack_retry_should_stop(false, 1, 30)); + TEST_ASSERT_TRUE(orion_hijack_retry_should_stop(false, 5, 30)); + return 0; +} + +static int test_inverted_legacy_bug_regression(void) { + /* + * Old bug: stop when process_alive (first fail aborts live targets). + * Correct: alive + under max → do NOT stop. + */ + const bool process_alive = true; + const int attempt = 1; + TEST_ASSERT_TRUE( + !orion_hijack_retry_should_stop(process_alive, attempt, 30)); + return 0; +} + +int test_hijack_retry_suite(void) { + int failures = 0; + failures += + orion_test_run("hijack_retry.alive_until_max", test_alive_retries_until_max); + failures += + orion_test_run("hijack_retry.dead_stops", test_dead_process_stops_immediately); + failures += orion_test_run("hijack_retry.alive_not_abort", + test_inverted_legacy_bug_regression); + return failures; +} diff --git a/source/util/tests/test_ipc_harden.cpp b/source/util/tests/test_ipc_harden.cpp new file mode 100644 index 0000000..470ee31 --- /dev/null +++ b/source/util/tests/test_ipc_harden.cpp @@ -0,0 +1,68 @@ +/* Host tests: IPC wire helpers (frame, escape, reply body, null path). */ +#include "test_harness.h" + +#include +#include + +#include +#include + +static int test_frame_complete(void) { + TEST_ASSERT_TRUE(orion::ipc_frame_is_complete(static_cast(sizeof(IPCMessage)))); + TEST_ASSERT_TRUE(!orion::ipc_frame_is_complete(0)); + TEST_ASSERT_TRUE(!orion::ipc_frame_is_complete(4)); + TEST_ASSERT_TRUE( + !orion::ipc_frame_is_complete(static_cast(sizeof(IPCMessage)) - 1)); + return 0; +} + +static int test_force_nul(void) { + IPCMessage m{}; + memset(m.msg, 'A', sizeof(m.msg)); + orion::ipc_message_force_nul(m); + TEST_ASSERT_EQ_INT(0, m.msg[sizeof(m.msg) - 1]); + return 0; +} + +static int test_json_escape_quotes_and_slash(void) { + std::string e = orion::ipc_json_escape(R"(a"b\c)"); + TEST_ASSERT_STREQ(R"(a\"b\\c)", e.c_str()); + std::string nl = orion::ipc_json_escape("x\ny"); + TEST_ASSERT_STREQ(R"(x\ny)", nl.c_str()); + return 0; +} + +static int test_format_reply_escapes_var(void) { + using orion::ipc_format_reply_body; + std::string ok = ipc_format_reply_body(false, "Nothing"); + TEST_ASSERT_STREQ(R"({"res":0,"var":"Nothing"})", ok.c_str()); + + std::string err = ipc_format_reply_body(true, "fail"); + TEST_ASSERT_STREQ(R"({"res":-1,"var":"fail"})", err.c_str()); + + std::string quoted = ipc_format_reply_body(false, R"(say "hi")"); + TEST_ASSERT_TRUE(quoted.find(R"(\"hi\")") != std::string::npos); + TEST_ASSERT_TRUE(quoted.find("\"var\":\"say") != std::string::npos); + /* Must be parseable as JSON-ish: no raw unescaped quote inside var value. */ + TEST_ASSERT_TRUE(quoted.find(R"("var":"say \"hi\"")") != std::string::npos); + return 0; +} + +static int test_null_path_policy(void) { + /* Mirror daemon handleIPC guards: missing path is hard fail. */ + auto missing = [](const char *p) -> bool { return !p || !*p; }; + TEST_ASSERT_TRUE(missing(nullptr)); + TEST_ASSERT_TRUE(missing("")); + TEST_ASSERT_TRUE(!missing("/data/OrionHEN")); + return 0; +} + +extern "C" int test_ipc_harden_suite(void) { + int failures = 0; + failures += orion_test_run("ipc_frame_complete", test_frame_complete); + failures += orion_test_run("ipc_force_nul", test_force_nul); + failures += orion_test_run("ipc_json_escape", test_json_escape_quotes_and_slash); + failures += orion_test_run("ipc_format_reply_escape", test_format_reply_escapes_var); + failures += orion_test_run("ipc_null_path_policy", test_null_path_policy); + return failures; +} diff --git a/source/util/tests/test_main.cpp b/source/util/tests/test_main.cpp index bd1186d..3063010 100644 --- a/source/util/tests/test_main.cpp +++ b/source/util/tests/test_main.cpp @@ -17,6 +17,9 @@ extern "C" int test_platform_fs_suite(void); extern "C" int test_platform_log_suite(void); extern "C" int test_platform_notify_suite(void); extern "C" int test_msg_protocol_suite(void); +extern "C" int test_ipc_harden_suite(void); +extern "C" int test_toolbox_timing_suite(void); +extern "C" int test_hijack_retry_suite(void); extern "C" int test_ps5_settings_ui_suite(void); extern "C" int test_toolbox_route_suite(void); extern "C" int test_toolbox_helpers_suite(void); @@ -41,6 +44,9 @@ int main() { failures += test_platform_log_suite(); failures += test_platform_notify_suite(); failures += test_msg_protocol_suite(); + failures += test_ipc_harden_suite(); + failures += test_toolbox_timing_suite(); + failures += test_hijack_retry_suite(); failures += test_ps5_settings_ui_suite(); failures += test_toolbox_route_suite(); failures += test_toolbox_helpers_suite(); diff --git a/source/util/tests/test_msg_protocol.cpp b/source/util/tests/test_msg_protocol.cpp index 8702f5c..58dd42a 100644 --- a/source/util/tests/test_msg_protocol.cpp +++ b/source/util/tests/test_msg_protocol.cpp @@ -73,9 +73,10 @@ static int test_ipc_format_reply_body(void) { std::string ok = ipc_format_reply_body(false, "Nothing"); std::string err = ipc_format_reply_body(true, "fail"); std::string tid = ipc_format_reply_body(false, "CUSA12345"); - TEST_ASSERT_STREQ("{\"res\":0, \"var\":\"Nothing\"}", ok.c_str()); - TEST_ASSERT_STREQ("{\"res\":-1, \"var\":\"fail\"}", err.c_str()); - TEST_ASSERT_STREQ("{\"res\":0, \"var\":\"CUSA12345\"}", tid.c_str()); + /* Compact JSON; var is escaped (no space after colon). */ + TEST_ASSERT_STREQ("{\"res\":0,\"var\":\"Nothing\"}", ok.c_str()); + TEST_ASSERT_STREQ("{\"res\":-1,\"var\":\"fail\"}", err.c_str()); + TEST_ASSERT_STREQ("{\"res\":0,\"var\":\"CUSA12345\"}", tid.c_str()); return 0; } diff --git a/source/util/tests/test_settings.cpp b/source/util/tests/test_settings.cpp index aeafe6c..331e833 100644 --- a/source/util/tests/test_settings.cpp +++ b/source/util/tests/test_settings.cpp @@ -202,6 +202,42 @@ static int test_empty_file_loads_defaults(void) { return 0; } +static int test_settings_store_snapshot_update(void) { + orion::SettingsStore store; + orion::Settings a = store.snapshot(); + TEST_ASSERT_EQ_INT(77, a.fan_threshold); + + store.update([](orion::Settings &s) { + s.fan_threshold = 60; + s.enable_fan_speed = true; + }); + orion::Settings b = store.snapshot(); + TEST_ASSERT_EQ_INT(60, b.fan_threshold); + TEST_ASSERT_TRUE(b.enable_fan_speed); + + orion::Settings c{}; + c.overlay_fps = true; + store.store(c); + orion::Settings d = store.snapshot(); + TEST_ASSERT_TRUE(d.overlay_fps); + TEST_ASSERT_TRUE(d.enable_fan_speed == false); /* full replace */ + return 0; +} + +static int test_config_mtime_helpers(void) { + /* No twin paths on host test machine → newest is 0, not newer. */ + TEST_ASSERT_TRUE(orion::settings_config_newest_mtime() == 0 || + orion::settings_config_newest_mtime() >= 0); + const time_t n = orion::settings_config_newest_mtime(); + if (n == 0) { + TEST_ASSERT_TRUE(!orion::settings_config_is_newer_than(0)); + } else { + TEST_ASSERT_TRUE(orion::settings_config_is_newer_than(n - 1)); + TEST_ASSERT_TRUE(!orion::settings_config_is_newer_than(n)); + } + return 0; +} + extern "C" int test_settings_suite(void) { int failures = 0; failures += orion_test_run("settings_defaults_serialize", test_defaults_and_serialize_keys); @@ -213,5 +249,8 @@ extern "C" int test_settings_suite(void) { failures += orion_test_run("settings_partial_ini_defaults", test_partial_ini_keeps_defaults); failures += orion_test_run("settings_serialize_overlay_keys", test_serialize_contains_overlay_keys); failures += orion_test_run("settings_empty_file_defaults", test_empty_file_loads_defaults); + failures += orion_test_run("settings_store_snapshot_update", + test_settings_store_snapshot_update); + failures += orion_test_run("settings_config_mtime_helpers", test_config_mtime_helpers); return failures; } diff --git a/source/util/tests/test_toolbox_timing.c b/source/util/tests/test_toolbox_timing.c new file mode 100644 index 0000000..aa7c71b --- /dev/null +++ b/source/util/tests/test_toolbox_timing.c @@ -0,0 +1,44 @@ +/* Host tests: toolbox rest-delay policy (cold start vs rest resume). */ +#include "test_harness.h" + +#include +#include + +#include +#include + +static int test_cold_start_no_delay(void) { + /* util_booted alone must not imply rest delay. */ + TEST_ASSERT_TRUE(!orion_toolbox_should_apply_rest_delay(false, 30)); + TEST_ASSERT_TRUE(!orion_toolbox_should_apply_rest_delay(false, 0)); + return 0; +} + +static int test_rest_resume_applies_delay(void) { + TEST_ASSERT_TRUE(orion_toolbox_should_apply_rest_delay(true, 5)); + TEST_ASSERT_TRUE(!orion_toolbox_should_apply_rest_delay(true, 0)); + return 0; +} + +static int test_util_booted_flag_does_not_force_delay(void) { + /* Simulate cold-start ordering: util signals util_booted before daemon inject. */ + char name[64]; + snprintf(name, sizeof(name), "tb_timing_%d", (int)getpid()); + orion_ready_clear(name); + /* Use a private flag name — ready API rejects dots/slashes; ok for smoke. */ + (void)orion_ready_signal(ORION_FLAG_UTIL_BOOTED); + /* Policy still requires rest_resume=true; util_booted is orthogonal. */ + TEST_ASSERT_TRUE( + !orion_toolbox_should_apply_rest_delay(/*rest_resume=*/false, 10)); + return 0; +} + +int test_toolbox_timing_suite(void) { + int failures = 0; + failures += orion_test_run("toolbox_timing.cold_no_delay", test_cold_start_no_delay); + failures += + orion_test_run("toolbox_timing.rest_applies", test_rest_resume_applies_delay); + failures += orion_test_run("toolbox_timing.util_booted_orthogonal", + test_util_booted_flag_does_not_force_delay); + return failures; +}