From bddaf7f61d3c5b2f38835863cf71a4aba7bb9ca5 Mon Sep 17 00:00:00 2001 From: maj0r Date: Thu, 18 Jun 2026 17:19:03 -0300 Subject: [PATCH] v0.14 update --- src/cr_addr_cache.c | 6 +- src/cr_api.c | 209 +++++++++++++----------------- src/cr_api.h | 3 +- src/cr_api_config.c | 19 +-- src/cr_api_dashboard.c | 26 +++- src/cr_api_dev.c | 2 +- src/cr_api_internal.h | 11 +- src/cr_api_launch.c | 2 +- src/cr_api_patches.c | 21 +-- src/cr_appdb.c | 7 +- src/cr_appdb.h | 3 + src/cr_browser.h | 7 +- src/cr_cheat_formats.c | 5 +- src/cr_cheats.c | 283 +++++++++++++---------------------------- src/cr_cheats.h | 6 +- src/cr_config.c | 32 ++--- src/cr_config.h | 3 - src/cr_conflict.c | 22 ++-- src/cr_fan.c | 4 +- src/cr_fan.h | 11 +- src/cr_game_monitor.c | 37 +----- src/cr_http.c | 73 ++++++----- src/cr_launch.c | 13 +- src/cr_mdbg.c | 137 ++++++++++++++++++++ src/cr_mdbg.h | 15 +++ src/cr_memory.c | 206 ++++-------------------------- src/cr_memory.h | 13 +- src/cr_notifications.c | 28 +++- src/cr_patch_parser.c | 73 ++++------- src/cr_patch_parser.h | 27 +--- src/cr_profile.c | 10 +- src/cr_profile.h | 15 +-- src/cr_shutdown.c | 26 ++-- src/cr_store_lookup.h | 5 +- src/cr_tile_pkg.c | 9 +- src/cr_tile_pkg.h | 5 +- src/cr_titles.c | 5 +- src/dashboard_css.inc | 88 ++++++++++--- src/dashboard_html.inc | 10 +- src/dashboard_js.inc | 74 +++++++++-- src/main.c | 25 +--- src/pt.c | 24 ++-- src/pt.h | 16 +-- 43 files changed, 731 insertions(+), 885 deletions(-) create mode 100644 src/cr_mdbg.c create mode 100644 src/cr_mdbg.h diff --git a/src/cr_addr_cache.c b/src/cr_addr_cache.c index 58076ef..9cb2758 100644 --- a/src/cr_addr_cache.c +++ b/src/cr_addr_cache.c @@ -101,11 +101,7 @@ addr_cache_load(void) { /* Internal: must be called with g_addr_cache_lock held. */ static void addr_cache_save_locked(void) { - /* Worst-case bytes per entry: escaped key (key[384] can double to ~766 when - * every char needs a backslash) + orig hex (128 bytes → 256 chars) + the - * fixed JSON scaffolding/addr/ts (~80). Under-sizing here makes the bounded - * snprintf truncate mid-entry, producing invalid JSON that fails to parse on - * the next load and silently drops the whole learned-address cache. */ + /* Generous per-entry cap: under-sizing truncates the snprintf mid-entry, producing invalid JSON that silently drops the whole cache on next load. */ size_t cap = (size_t)g_addr_cache_n * 1200 + 64; if (cap < 4) cap = 4; char *buf = malloc(cap); diff --git a/src/cr_api.c b/src/cr_api.c index acbfc48..1ba058b 100644 --- a/src/cr_api.c +++ b/src/cr_api.c @@ -60,10 +60,10 @@ #endif #ifndef CHEATRUNNER_VERSION -#define CHEATRUNNER_VERSION "0.1" +#define CHEATRUNNER_VERSION "0.14" #endif -#define MAX_GAMES 1024 +#define MAX_GAMES CR_APPDB_MAX_GAMES const char g_dashboard_html[] = #include "dashboard_html.inc" @@ -204,11 +204,39 @@ http_send_response(int fd, int status, const char *content_type, const uint8_t * } } +void +http_send_response_cached(int fd, int status, const char *content_type, const uint8_t *body, size_t body_len) { + char header[512]; + int n = snprintf(header, sizeof(header), + "HTTP/1.1 %d %s\r\n" + "Content-Type: %s\r\n" + "Content-Length: %u\r\n" + "Connection: close\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Cache-Control: public, max-age=3600\r\n" + "\r\n", + status, status_text_for(status), content_type ? content_type : "application/octet-stream", + (unsigned int)body_len); + if (n > 0) { + if (socket_send_all(fd, header, (size_t)n) != 0) { + return; + } + if (body_len > 0) { + (void)socket_send_all(fd, body, body_len); + } + } +} + void http_send_json(int fd, int status, const char *body) { http_send_response(fd, status, "application/json", (const uint8_t *)body, strlen(body)); } +void +http_send_oom(int fd) { + http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); +} + static int read_title_lookup_cache_name(const char *title_id, char *out, size_t out_size) { if (!title_id || !out || out_size == 0) { @@ -475,7 +503,7 @@ handle_api_logs(int fd, const char *query) { char *txt = cJSON_PrintUnformatted(root); cJSON_Delete(root); if (!txt) { - http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); + http_send_oom(fd); return; } http_send_response(fd, 200, "application/json", (const uint8_t *)txt, strlen(txt)); @@ -510,7 +538,7 @@ handle_api_notifications(int fd) { char *txt = cJSON_PrintUnformatted(root); cJSON_Delete(root); if (!txt) { - http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); + http_send_oom(fd); return; } http_send_response(fd, 200, "application/json", (const uint8_t *)txt, strlen(txt)); @@ -568,7 +596,7 @@ handle_api_activity(int fd) { char *txt = cJSON_PrintUnformatted(root); cJSON_Delete(root); if (!txt) { - http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); + http_send_oom(fd); return; } http_send_response(fd, 200, "application/json", (const uint8_t *)txt, strlen(txt)); @@ -604,7 +632,7 @@ handle_api_activity_title(int fd, const char *query) { char *txt = cJSON_PrintUnformatted(root); cJSON_Delete(root); if (!txt) { - http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); + http_send_oom(fd); return; } http_send_response(fd, 200, "application/json", (const uint8_t *)txt, strlen(txt)); @@ -859,11 +887,11 @@ handle_api_games(int fd, const char *query) { if (debug_names) { size_t cap = 262144; char *arr = malloc(cap); - if (!arr) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); return; } + if (!arr) { http_send_oom(fd); return; } size_t arr_len = build_games_json_array(arr, cap, 1); size_t resp_cap = arr_len + 32; char *resp = malloc(resp_cap); - if (!resp) { free(arr); http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); return; } + if (!resp) { free(arr); http_send_oom(fd); return; } size_t resp_len = (size_t)snprintf(resp, resp_cap, "{\"ok\":true,\"games\":%.*s}", (int)arr_len, arr); http_send_response(fd, 200, "application/json", (const uint8_t *)resp, resp_len); free(resp); free(arr); @@ -957,7 +985,7 @@ handle_api_games(int fd, const char *query) { } size_t resp_cap = arr_len + 128; char *resp = malloc(resp_cap); - if (!resp) { free(arr_copy); http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); return; } + if (!resp) { free(arr_copy); http_send_oom(fd); return; } size_t resp_len = (size_t)snprintf(resp, resp_cap, "{\"ok\":true,\"games\":%.*s,\"refreshing\":%s,\"cacheAgeMs\":%lld,\"throttled\":%s}", (int)arr_len, arr_copy, busy ? "true" : "false", @@ -1104,7 +1132,7 @@ handle_api_cheats_index(int fd) { cJSON *files = cJSON_AddArrayToObject(root, "files"); if (!root || !files) { cJSON_Delete(root); - http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); + http_send_oom(fd); return; } cJSON_AddBoolToObject(root, "ok", 1); @@ -1182,7 +1210,7 @@ handle_api_cheats_index(int fd) { char *body = cJSON_PrintUnformatted(root); cJSON_Delete(root); if (!body) { - http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); + http_send_oom(fd); return; } http_send_response(fd, 200, "application/json", (const uint8_t *)body, strlen(body)); @@ -1298,7 +1326,7 @@ handle_api_cheats_get(int fd, const char *query) { char *out = malloc(base_len + (size_t)inj + 1); if (!out) { free(json); - http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); + http_send_oom(fd); return; } memcpy(out, json, base_len); @@ -1308,10 +1336,7 @@ handle_api_cheats_get(int fd, const char *query) { free(out); } -/* Single-slot cache: stores the decoded JSON text (post-decrypt) keyed by - * (title_id, path, mtime, kind). Avoids repeated disk reads + MC4/SHN decryption - * on every /api/cheats/state poll. cJSON_Parse from memory is ~10x faster than - * reading + decrypting from disk. */ +/* Single-slot decoded-JSON cache keyed by (title_id, path, mtime, kind); avoids re-decrypting MC4/SHN on every state poll. */ static struct { pthread_mutex_t lock; char title_id[10]; @@ -1334,9 +1359,7 @@ static struct { * Must be called after the doc cache has been loaded (we borrow the cached JSON). */ static void conflict_cache_ensure(const char *path, time_t mtime) { - /* Check + build under a single lock acquisition to prevent two concurrent - * callers from both passing the staleness check and double-building (and - * double-leaking the ~48 KB entries_p inside conflict_map_build). */ + /* Check+build under one lock so two concurrent callers can't both double-build (and leak entries_p). */ pthread_mutex_lock(&g_conflict_cache.lock); if (g_conflict_cache.ready && g_conflict_cache.mtime == mtime && @@ -1651,23 +1674,14 @@ handle_api_cheats_state(int fd, const char *query) { if (can_probe && kill(pid, 0) != 0 && errno == ESRCH) { can_probe = 0; } - /* Skip ptrace entirely while an apply is in progress — the game is already - * paused under ptrace by the apply thread; attaching again from here races - * with that attach and hangs for the full timeout. */ + /* Skip ptrace during an apply — attaching again here races the apply thread's attach and hangs. */ if (can_probe && g_cheat_applying) { can_probe = 0; } + /* Probing reads are mdbg-backed and need no ptrace attach. */ if (can_probe) { - int _at = pt_attach_timed(pid, 1000); - if (_at == 0) { - attach_ok = 1; - is_ps2_st = process_is_ps2_emu(pid); - } else { - if (_at == -2) { - cr_log("warn", "cheats", "pt_attach timeout on state check pid=%d title=%s", (int)pid, title_id); - } - can_probe = 0; - } + attach_ok = 1; + is_ps2_st = process_is_ps2_emu(pid); } const char *fmt = cheat_kind == 1 ? "json" : (cheat_kind == 2 ? "shn" : "mc4"); cr_log("debug", "cheats", "loaded file=%s format=%s mods=%d", cheat_path, fmt, cJSON_GetArraySize(mods)); @@ -1897,24 +1911,12 @@ handle_api_cheats_state(int fd, const char *query) { get_cheat_addr_flags(cheat_kind, cJSON_IsTrue(abs_j) ? 1 : 0, auto_detect, &af_s, &inj_s, &adet_s); /* PS2 emulation: all addresses absolute (matches apply_cheat_json). */ if (is_ps2_st) { af_s = 1; inj_s = 0; adet_s = 0; } - /* Determine expected reliability: JSON off_bytes and explicit expected are reliable; - * MC4/SHN ValueOff is not. */ + /* JSON off_bytes/explicit expected are reliable; MC4/SHN ValueOff is not. */ int exp_rel_s = (exp_len > 0) || (cheat_kind == 1 && off_len > 0); const uint8_t *expect_cmp_s = exp_rel_s ? (exp_len > 0 ? exp_b : off_b) : NULL; - /* For state reads, always resolve via legacy fallback so MC4/SHN get a usable address. - * When there are no reliable expected bytes, skip the x86/offbytes probe entirely: - * the probe reads both address candidates blindly, and if either address maps to a - * PS5 GPU/MMIO region the kernel page-fault never returns, hanging the game under - * ptrace before any cheat is applied. The probe result is discarded anyway when - * baseline_unknown is the expected outcome. */ + /* Skip the offbytes probe when there's no reliable expected: a blind read can hit PS5 GPU/MMIO and hang under ptrace. */ const char *fb_str_s = (cheat_kind == 2) ? shn_fb_s : mc4_fb_s; - /* Passive state read must default to RELATIVE: for non-absolute MC4/SHN - * the raw offset can map to PS5 GPU/MMIO, and reading it first under - * ptrace page-faults forever — freezing the game just from opening the - * cheat page. The relative candidate (base+offset) is the correct target - * for these cheats anyway. Honor an explicit "absolute"/"block" config, - * but treat the deprecated "legacy" magnitude heuristic (and any unknown - * value) as relative here so a misconfig can't freeze a passive poll. */ + /* Default passive reads to RELATIVE: a raw MC4/SHN offset can hit PS5 GPU/MMIO and freeze the game just from polling state. */ cr_addr_fallback_policy_t fb_pol_s = CR_ADDR_FALLBACK_RELATIVE; if (!exp_rel_s) { if (strcmp(fb_str_s, "absolute") == 0) fb_pol_s = CR_ADDR_FALLBACK_ABSOLUTE; @@ -1954,9 +1956,7 @@ handle_api_cheats_state(int fd, const char *query) { /* For MC4/SHN without explicit expected bytes, off_b is NOT a reliable original */ int off_reliable = (cheat_kind == 1) || (exp_len > 0); int is_off = off_reliable && (memcmp(cur, (exp_len > 0 ? exp_b : off_b), on_len) == 0); - /* cross_mod_inactive: another mod has claimed this address (cave or JMP redirect). - * Treat as OFF so mutually exclusive cheats (e.g. speed modes sharing a hook) show - * as "off" in the UI instead of "error/mismatch". */ + /* Another mod claimed this address (cave/JMP redirect) — treat as OFF, not mismatch, for mutually-exclusive cheats. */ int cross_mod_inactive = 0; if (!is_on && !is_off && off_reliable) { if (on_len >= 16) { @@ -1982,10 +1982,7 @@ handle_api_cheats_state(int fd, const char *query) { } } if (!is_on && !off_reliable) { - /* MC4/SHN without reliable baseline: check if current bytes match ValueOff. - * A ValueOff match means the cheat was disabled (or never applied). - * Count separately so the state decision can emit OFF_UNVERIFIED rather - * than BASELINE_UNKNOWN — this survives CheatRunner restarts. */ + /* ValueOff match means disabled/never-applied; counted separately so state can be OFF_UNVERIFIED, not BASELINE_UNKNOWN. */ if (off_len > 0 && memcmp(cur, off_b, on_len) == 0) { off_val_matches++; } else { @@ -2083,9 +2080,7 @@ handle_api_cheats_state(int fd, const char *query) { state = CHEAT_STATE_OFF_UNVERIFIED; reason = "MC4/SHN: ValueOff bytes match current memory; cheat appears disabled."; } else if (baseline_unknown > 0 && mismatch == 0 && on_matches == 0) { - /* No ON matches, no reliable-baseline mismatches — cheat is not active. - * Some entries may match ValueOff (soft OFF) and others may not (truly unknown); - * either way the cheat is not applied, so report BASELINE_UNKNOWN not MIXED. */ + /* No ON matches, no reliable mismatches — not applied either way, so report BASELINE_UNKNOWN not MIXED. */ state = CHEAT_STATE_BASELINE_UNKNOWN; reason = "MC4/SHN: no reliable original bytes; state cannot be confirmed"; } else { @@ -2226,9 +2221,6 @@ handle_api_cheats_state(int fd, const char *query) { } cJSON_AddItemToArray(arr, e); } - if (attach_ok) { - pt_detach(pid, 0); - } cJSON_Delete(root); cJSON *summary = cJSON_AddObjectToObject(out, "summary"); if (summary) { @@ -2244,7 +2236,7 @@ handle_api_cheats_state(int fd, const char *query) { char *txt = cJSON_PrintUnformatted(out); cJSON_Delete(out); if (!txt) { - http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); + http_send_oom(fd); return; } http_send_response(fd, 200, "application/json", (const uint8_t *)txt, strlen(txt)); @@ -2277,10 +2269,7 @@ handle_api_cheats_clear_crash_flags(int fd, const char *query) { http_send_json(fd, 200, body); } -/* POST /api/cheats/select?titleId=CUSA00900&path=[&force=1] - * Manually select a specific cheat file for a title. - * path must be present in the current candidates list (validated against scan results). - * wrong_version candidates require force=1. */ +/* POST /api/cheats/select: path must be in the current candidates list; wrong_version requires force=1. */ void handle_api_cheats_select(int fd, const char *query) { char raw_tid[32] = {0}, title_id[10] = {0}; @@ -2488,13 +2477,11 @@ check_mod_enable_state(const char *title_id, int mod_index, char *reason, size_t return fast_st; } } - { - int _at = pt_attach_timed(st.pid, 2000); - if (_at < 0) { - cJSON_Delete(root); - snprintf(reason, reason_size, _at == -2 ? "pt_attach timed out" : "pt_attach failed"); - return CHEAT_STATE_PROCESS_NOT_FOUND; - } + /* read_process_memory is mdbg-backed and needs no ptrace attach. */ + if (kill(st.pid, 0) != 0 && errno == ESRCH) { + cJSON_Delete(root); + snprintf(reason, reason_size, "game has exited"); + return CHEAT_STATE_PROCESS_NOT_FOUND; } int is_ps2_cme = process_is_ps2_emu(st.pid); cheat_state_kind_t state = CHEAT_STATE_UNKNOWN; @@ -2603,7 +2590,6 @@ check_mod_enable_state(const char *title_id, int mod_index, char *reason, size_t } } } - pt_detach(st.pid, 0); cJSON_Delete(root); /* Block crash_suspect mods from being re-enabled */ if (state != CHEAT_STATE_ON) { @@ -2648,9 +2634,7 @@ classify_cheat_error_code(const char *msg, char *out, size_t out_size) { snprintf(out, out_size, "%s", code); } -/* GET /api/cheats/apply-dryrun?titleId=X&mod=N - Returns what a real apply would do: resolved addresses, current bytes, - page protection, ON bytes — no actual writes. */ +/* GET /api/cheats/apply-dryrun: shows what a real apply would do (addresses, bytes, protection) with no writes. */ void handle_api_cheats_apply_dryrun(int fd, const char *query) { char raw_title_id[32] = {0}; @@ -2697,9 +2681,10 @@ handle_api_cheats_apply_dryrun(int fd, const char *query) { auto_detect = g_cfg.cheat_address_auto_detect; pthread_mutex_unlock(&g_cfg_lock); + /* Probing reads are mdbg-backed and need no ptrace attach. */ int attached = 0; if (running_ok && pid > 0) { - attached = (pt_attach_timed(pid, 2000) == 0); + attached = (kill(pid, 0) == 0 || errno != ESRCH); } int is_ps2_dr = attached ? process_is_ps2_emu(pid) : 0; @@ -2824,7 +2809,6 @@ handle_api_cheats_apply_dryrun(int fd, const char *query) { } } - if (attached) pt_detach(pid, 0); cJSON_Delete(root); char *body = cJSON_PrintUnformatted(out); @@ -3144,10 +3128,7 @@ handle_api_cheats_upload(int fd, const char *query, const char *body, size_t bod http_send_json(fd, 200, resp); } -/* Task 13: /api/cheats/address-debug?titleId=X - For each patch in the cheat file: computes absolute & relative candidate - addresses, reads bytes from both, and shows which one matches on/off/expected. - Only works if the game is currently running. */ +/* GET /api/cheats/address-debug: shows which of absolute/relative candidate addresses matches on/off/expected; needs game running. */ void handle_api_cheats_address_debug(int fd, const char *query) { char raw_title_id[32] = {0}; @@ -3183,9 +3164,10 @@ handle_api_cheats_address_debug(int fd, const char *query) { snprintf(shn_fb_adbg, sizeof(shn_fb_adbg), "%s", g_cfg.cheat_shn_unverified_fallback); pthread_mutex_unlock(&g_cfg_lock); + /* Probing reads are mdbg-backed and need no ptrace attach. */ int attach_ok = 0; if (title_match && pid > 0 && base != 0) { - attach_ok = (pt_attach_timed(pid, 2000) == 0); + attach_ok = (kill(pid, 0) == 0 || errno != ESRCH); } cJSON *mods = cJSON_GetObjectItem(root, "mods"); @@ -3363,12 +3345,11 @@ handle_api_cheats_address_debug(int fd, const char *query) { } } - if (attach_ok) { pt_detach(pid, 0); } cJSON_Delete(root); char *s = cJSON_PrintUnformatted(out); cJSON_Delete(out); - if (!s) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); return; } + if (!s) { http_send_oom(fd); return; } http_send_json(fd, 200, s); free(s); } @@ -3394,7 +3375,7 @@ handle_api_cheats_sources(int fd) { char *txt = cJSON_PrintUnformatted(out); cJSON_Delete(out); if (!txt) { - http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); + http_send_oom(fd); return; } http_send_json(fd, 200, txt); @@ -3450,14 +3431,14 @@ handle_api_sources_jobs_status(int fd, const char *query) { } cJSON *out = cr_source_job_status_json(job_id); if (!out) { - http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); + http_send_oom(fd); return; } int status = cJSON_GetObjectItem(out, "notFound") ? 404 : 200; char *txt = cJSON_PrintUnformatted(out); cJSON_Delete(out); if (!txt) { - http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); + http_send_oom(fd); return; } http_send_json(fd, status, txt); @@ -3585,7 +3566,7 @@ cheats_list_scan_subdir(const char *fmt_name, int kind, cJSON *arr) { void handle_api_cheats_list(int fd) { cJSON *out = cJSON_CreateObject(); - if (!out) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); return; } + if (!out) { http_send_oom(fd); return; } cJSON_AddBoolToObject(out, "ok", 1); cJSON *arr = cJSON_AddArrayToObject(out, "files"); @@ -3627,7 +3608,7 @@ handle_api_cheats_list(int fd) { char *txt = cJSON_PrintUnformatted(out); cJSON_Delete(out); - if (!txt) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); return; } + if (!txt) { http_send_oom(fd); return; } http_send_json(fd, 200, txt); free(txt); } @@ -3689,22 +3670,12 @@ handle_api_cheats_download_all_status(int fd) { handle_api_cheats_repo_download_status(fd); } -void -handle_api_cheats_index_status(int fd) { - http_send_json(fd, 200, "{\"ok\":true,\"available\":false,\"indexes\":[]}"); -} - -void -handle_api_cheats_index_refresh(int fd) { - http_send_json(fd, 410, "{\"ok\":false,\"error\":\"remote_index_removed\"}"); -} - /* ── /api/dev/privileges ────────────────────────────────────────── */ void handle_api_dev_privileges(int fd) { const cr_priv_status_t *ps = cr_priv_get(); cJSON *out = cJSON_CreateObject(); - if (!out) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); return; } + if (!out) { http_send_oom(fd); return; } cJSON_AddBoolToObject(out, "ok", ps->can_patch_game ? 1 : 0); cJSON_AddNumberToObject(out, "uid", ps->uid); @@ -3736,7 +3707,7 @@ handle_api_dev_privileges(int fd) { char *s = cJSON_PrintUnformatted(out); cJSON_Delete(out); - if (!s) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); return; } + if (!s) { http_send_oom(fd); return; } http_send_json(fd, 200, s); free(s); } @@ -3745,12 +3716,12 @@ handle_api_dev_privileges(int fd) { void handle_api_dev_diag(int fd) { cJSON *out = cJSON_CreateObject(); - if (!out) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); return; } + if (!out) { http_send_oom(fd); return; } cJSON_AddBoolToObject(out, "ok", 1); char *s = cJSON_PrintUnformatted(out); cJSON_Delete(out); - if (!s) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); return; } + if (!s) { http_send_oom(fd); return; } http_send_json(fd, 200, s); free(s); } @@ -3810,7 +3781,7 @@ handle_api_dev_open_browser(int fd, const char *method, const char *query) { void handle_api_dev_memtest(int fd) { cJSON *out = cJSON_CreateObject(); - if (!out) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); return; } + if (!out) { http_send_oom(fd); return; } cJSON_AddBoolToObject(out, "ok", 1); @@ -3863,9 +3834,7 @@ handle_api_dev_memtest(int fd) { intptr_t pg = (intptr_t)ROUND_PG_DOWN((uintptr_t)rgs.image_base); size_t sp = (size_t)0x4000; int orig_prot = kernel_get_vmem_protection(rgs.pid, pg, sp); - /* This is a read-only probe — only ensure the page is readable+executable. - * Never set W+X (PROT_WRITE | PROT_EXEC): PS5's hypervisor enforces W^X and - * a writable+executable code mapping can trigger a kernel panic. */ + /* Read-only probe: never set W+X here, PS5's hypervisor enforces W^X and RWX can panic the kernel. */ mprotect_rc = kernel_mprotect(rgs.pid, pg, sp, PROT_READ | PROT_EXEC); if (mprotect_rc == 0) { attach_rc = pt_attach_timed(rgs.pid, 2000); @@ -3892,14 +3861,14 @@ handle_api_dev_memtest(int fd) { char *s = cJSON_PrintUnformatted(out); cJSON_Delete(out); - if (!s) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); return; } + if (!s) { http_send_oom(fd); return; } http_send_json(fd, 200, s); free(s); } void -handle_api_dev_shutdown(int fd, const char *method, const char *query, const char *token_header, const char *client_ip) { - (void)query; (void)token_header; +handle_api_dev_shutdown(int fd, const char *method, const char *query, const char *client_ip) { + (void)query; int enabled = 0; int delay_ms = 700; char body[512]; @@ -4032,7 +4001,7 @@ handle_api_diag_title(int fd, const char *query) { } cJSON *out = cJSON_CreateObject(); if (!out) { - http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); + http_send_oom(fd); return; } cJSON_AddBoolToObject(out, "ok", 1); @@ -4230,7 +4199,7 @@ handle_api_diag_title(int fd, const char *query) { char *s = cJSON_PrintUnformatted(out); cJSON_Delete(out); - if (!s) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); return; } + if (!s) { http_send_oom(fd); return; } http_send_json(fd, 200, s); free(s); } @@ -4250,7 +4219,7 @@ handle_api_cheats_scan(int fd, const char *query) { cJSON *out = cJSON_CreateObject(); if (!out) { - http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); + http_send_oom(fd); return; } cJSON_AddBoolToObject(out, "ok", 1); @@ -4278,7 +4247,7 @@ handle_api_cheats_scan(int fd, const char *query) { } char *s = cJSON_PrintUnformatted(out); cJSON_Delete(out); - if (!s) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); return; } + if (!s) { http_send_oom(fd); return; } http_send_json(fd, 200, s); free(s); } @@ -4309,7 +4278,7 @@ handle_api_cheats_mc4_debug(int fd, const char *query) { cJSON *out = cJSON_CreateObject(); if (!out) { cJSON_Delete(root); - http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); + http_send_oom(fd); return; } cJSON_AddBoolToObject(out, "ok", 1); @@ -4381,7 +4350,7 @@ handle_api_cheats_mc4_debug(int fd, const char *query) { cJSON_Delete(root); char *s = cJSON_PrintUnformatted(out); cJSON_Delete(out); - if (!s) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); return; } + if (!s) { http_send_oom(fd); return; } http_send_json(fd, 200, s); free(s); } @@ -4458,7 +4427,7 @@ http_send_png_asset(int fd) { } void -http_route(int fd, const char *method, const char *path, const char *query, const char *token_header, +http_route(int fd, const char *method, const char *path, const char *query, const char *client_ip, const char *body, size_t body_len) { if (!g_startup_ms) { struct timespec _ts; clock_gettime(CLOCK_MONOTONIC, &_ts); @@ -4477,9 +4446,9 @@ http_route(int fd, const char *method, const char *path, const char *query, cons if (cr_api_logs_handle(fd, method, path, query, body, body_len)) return; if (cr_api_fan_handle(fd, method, path, query, body, body_len)) return; if (cr_api_profile_handle(fd, method, path, query, body, body_len)) return; - /* shutdown needs token_header + client_ip - handle directly */ + /* shutdown needs client_ip - handle directly */ if (!strcmp(path, "/api/dev/shutdown")) { - handle_api_dev_shutdown(fd, method, query, token_header, client_ip); + handle_api_dev_shutdown(fd, method, query, client_ip); return; } if (cr_api_dev_handle(fd, method, path, query, body, body_len)) return; diff --git a/src/cr_api.h b/src/cr_api.h index d1a77aa..207088f 100644 --- a/src/cr_api.h +++ b/src/cr_api.h @@ -4,7 +4,6 @@ #include void http_route(int fd, const char *method, const char *path, const char *query, - const char *token_header, const char *client_ip, - const char *body, size_t body_len); + const char *client_ip, const char *body, size_t body_len); #endif diff --git a/src/cr_api_config.c b/src/cr_api_config.c index cab93f7..53d8c60 100644 --- a/src/cr_api_config.c +++ b/src/cr_api_config.c @@ -16,7 +16,7 @@ handle_api_config(int fd) { "\"auto_download_missing_cheat\":%d,\"launch_kill_current\":%d,\"launch_kill_delay_ms\":%d," "\"launch_wait_timeout_ms\":%d,\"cheat_engine\":%d,\"cheat_validate_original_bytes\":%d," "\"cheat_restore_rx\":%d,\"cheat_restore_original_prot\":%d," - "\"cheat_index_cache_ttl_sec\":%d,\"allow_force_enable\":%d," + "\"allow_force_enable\":%d," "\"cheat_state_after_launch_delay_ms\":%d,\"dev_reload_enabled\":%d,\"dev_shutdown_delay_ms\":%d," "\"cheat_sources_enabled\":%d,\"cheat_remote_download_enabled\":%d," "\"cheat_source_cache_ttl_seconds\":%d,\"cheat_remote_max_file_bytes\":%d," @@ -26,7 +26,7 @@ handle_api_config(int fd) { "\"allow_legacy_mc4_without_expected\":%d,\"allow_legacy_shn_without_expected\":%d," "\"cheat_mc4_unverified_fallback\":\"%s\",\"cheat_shn_unverified_fallback\":\"%s\"," "\"cheat_min_stable_ms\":%d,\"cheat_apply_cooldown_ms\":%d," - "\"cheat_codecave_fallback\":%d,\"cheat_master_code_fixup\":%d," + "\"cheat_master_code_fixup\":%d," "\"cheat_addr_cache_enabled\":%d,\"cheat_inter_mod_delay_ms\":%d," "\"fan_min_c\":%d,\"fan_max_c\":%d," "\"allow_unsafe_mc4_apply\":%d,\"allow_unsafe_shn_apply\":%d," @@ -37,7 +37,7 @@ handle_api_config(int fd) { g_cfg.auto_download_missing_cheat, g_cfg.launch_kill_current, g_cfg.launch_kill_delay_ms, g_cfg.launch_wait_timeout_ms, g_cfg.cheat_engine, g_cfg.cheat_validate_original_bytes, g_cfg.cheat_restore_rx, g_cfg.cheat_restore_original_prot, - g_cfg.cheat_index_cache_ttl_sec, g_cfg.allow_force_enable, g_cfg.cheat_state_after_launch_delay_ms, + g_cfg.allow_force_enable, g_cfg.cheat_state_after_launch_delay_ms, g_cfg.dev_reload_enabled, g_cfg.dev_shutdown_delay_ms, g_cfg.cheat_sources_enabled, g_cfg.cheat_remote_download_enabled, g_cfg.cheat_source_cache_ttl_seconds, g_cfg.cheat_remote_max_file_bytes, @@ -47,7 +47,7 @@ handle_api_config(int fd) { g_cfg.allow_legacy_mc4_without_expected, g_cfg.allow_legacy_shn_without_expected, g_cfg.cheat_mc4_unverified_fallback, g_cfg.cheat_shn_unverified_fallback, g_cfg.cheat_min_stable_ms, g_cfg.cheat_apply_cooldown_ms, - g_cfg.cheat_codecave_fallback, g_cfg.cheat_master_code_fixup, + g_cfg.cheat_master_code_fixup, g_cfg.cheat_addr_cache_enabled, g_cfg.cheat_inter_mod_delay_ms, g_cfg.fan_min_c, g_cfg.fan_max_c, g_cfg.allow_unsafe_mc4_apply, g_cfg.allow_unsafe_shn_apply, @@ -89,8 +89,6 @@ handle_api_config_set(int fd, const char *query) { g_cfg.cheat_validate_original_bytes = atoi(value) ? 1 : 0; } else if (!strcmp(key, "cheat_restore_rx")) { g_cfg.cheat_restore_rx = atoi(value) ? 1 : 0; - } else if (!strcmp(key, "cheat_index_cache_ttl_sec")) { - g_cfg.cheat_index_cache_ttl_sec = atoi(value); } else if (!strcmp(key, "allow_force_enable")) { g_cfg.allow_force_enable = atoi(value) ? 1 : 0; } else if (!strcmp(key, "cheat_state_after_launch_delay_ms")) { @@ -176,8 +174,6 @@ handle_api_config_set(int fd, const char *query) { } else if (!strcmp(key, "cheat_apply_cooldown_ms")) { int v = atoi(value); g_cfg.cheat_apply_cooldown_ms = (v >= 0 && v <= 10000) ? v : 500; - } else if (!strcmp(key, "cheat_codecave_fallback")) { - g_cfg.cheat_codecave_fallback = atoi(value) ? 1 : 0; } else if (!strcmp(key, "cheat_master_code_fixup")) { g_cfg.cheat_master_code_fixup = atoi(value) ? 1 : 0; } else if (!strcmp(key, "cheat_addr_cache_enabled")) { @@ -232,10 +228,7 @@ handle_api_config_reset(int fd) { http_send_json(fd, rc == 0 ? 200 : 500, rc == 0 ? "{\"ok\":true}" : "{\"ok\":false,\"error\":\"save failed\"}"); } -/* Apply a curated bundle of related settings in one click. - * safe — validation on, unsafe applies off, relative fallback (default-ish) - * compat — maximum compatibility: allow unverified/unsafe applies for stubborn cheats - * debug — verbose logging + candidate diagnostics */ +/* Curated setting bundles: safe = validated defaults, compat = allow unsafe applies, debug = verbose logging. */ void handle_api_config_preset(int fd, const char *query) { char name[32] = {0}; @@ -251,7 +244,6 @@ handle_api_config_preset(int fd, const char *query) { g_cfg.allow_unsafe_mc4_apply = 0; g_cfg.allow_unsafe_shn_apply = 0; g_cfg.cheat_address_auto_detect = 1; - g_cfg.cheat_codecave_fallback = 1; g_cfg.cheat_master_code_fixup = 1; g_cfg.cheat_log_candidates = 0; snprintf(g_cfg.cheat_mc4_unverified_fallback, sizeof(g_cfg.cheat_mc4_unverified_fallback), "%s", "relative"); @@ -264,7 +256,6 @@ handle_api_config_preset(int fd, const char *query) { g_cfg.allow_unsafe_mc4_apply = 1; g_cfg.allow_unsafe_shn_apply = 1; g_cfg.cheat_address_auto_detect = 1; - g_cfg.cheat_codecave_fallback = 1; g_cfg.cheat_master_code_fixup = 1; snprintf(g_cfg.cheat_mc4_unverified_fallback, sizeof(g_cfg.cheat_mc4_unverified_fallback), "%s", "relative"); snprintf(g_cfg.cheat_shn_unverified_fallback, sizeof(g_cfg.cheat_shn_unverified_fallback), "%s", "relative"); diff --git a/src/cr_api_dashboard.c b/src/cr_api_dashboard.c index 0c1ad76..468ca75 100644 --- a/src/cr_api_dashboard.c +++ b/src/cr_api_dashboard.c @@ -10,6 +10,10 @@ static void send_asset(int fd, const char *content_type, const char *data) { http_send_response(fd, 200, content_type, (const uint8_t *)data, strlen(data)); } +static void send_static_asset(int fd, const char *content_type, const char *data) { + http_send_response_cached(fd, 200, content_type, (const uint8_t *)data, strlen(data)); +} + int cr_api_dashboard_handle(int fd, const char *method, const char *path, const char *query, const char *body, size_t body_len) { @@ -20,11 +24,11 @@ cr_api_dashboard_handle(int fd, const char *method, const char *path, return 1; } if (!strcmp(path, "/dashboard.css")) { - send_asset(fd, "text/css; charset=utf-8", g_dashboard_css); + send_static_asset(fd, "text/css; charset=utf-8", g_dashboard_css); return 1; } if (!strcmp(path, "/dashboard.js")) { - send_asset(fd, "application/javascript; charset=utf-8", g_dashboard_js); + send_static_asset(fd, "application/javascript; charset=utf-8", g_dashboard_js); return 1; } if (!strcmp(path, "/CheatRunner.png") || !strcmp(path, "/favicon.png") || @@ -32,6 +36,24 @@ cr_api_dashboard_handle(int fd, const char *method, const char *path, http_send_png_asset(fd); return 1; } + if (!strcmp(path, "/cache.appcache")) { + static const char appcache[] = + "CACHE MANIFEST\n" + "# CheatRunner v0.14\n" + "# Build: " __DATE__ " " __TIME__ "\n" + "\n" + "CACHE:\n" + "/\n" + "/dashboard.css\n" + "/dashboard.js\n" + "/CheatRunner.png\n" + "\n" + "NETWORK:\n" + "*\n"; + http_send_response(fd, 200, "text/cache-manifest; charset=utf-8", + (const uint8_t *)appcache, sizeof(appcache) - 1); + return 1; + } if (!strcmp(path, "/manifest.json")) { static const char manifest[] = "{\"name\":\"CheatRunner\",\"short_name\":\"CheatRunner\"," diff --git a/src/cr_api_dev.c b/src/cr_api_dev.c index 67fbb68..1adb091 100644 --- a/src/cr_api_dev.c +++ b/src/cr_api_dev.c @@ -40,6 +40,6 @@ cr_api_dev_handle(int fd, const char *method, const char *path, handle_api_dev_open_browser(fd, method, query); return 1; } - /* /api/dev/shutdown is handled directly in http_route (needs token_header + client_ip) */ + /* /api/dev/shutdown is handled directly in http_route (needs client_ip) */ return 0; } diff --git a/src/cr_api_internal.h b/src/cr_api_internal.h index 1c4ef79..1dc1244 100644 --- a/src/cr_api_internal.h +++ b/src/cr_api_internal.h @@ -1,19 +1,18 @@ #ifndef CR_API_INTERNAL_H #define CR_API_INTERNAL_H -/* - * Internal declarations shared between cr_api.c and its split modules. - * All handler functions are defined in cr_api.c and declared here so - * the module routing files can call them. - */ +/* Handlers live in cr_api.c; declared here so the split routing modules can call them. */ #include #include /* ---- HTTP send helpers (defined in cr_api.c) ---- */ void http_send_json(int fd, int status, const char *body); +void http_send_oom(int fd); void http_send_response(int fd, int status, const char *content_type, const uint8_t *body, size_t body_len); +void http_send_response_cached(int fd, int status, const char *content_type, + const uint8_t *body, size_t body_len); int query_value(const char *query, const char *key, char *out, size_t out_size); /* ---- /api/logs ---- */ @@ -85,7 +84,7 @@ void handle_api_dev_diag(int fd); void handle_api_dev_open_browser(int fd, const char *method, const char *query); void handle_api_dev_memtest(int fd); void handle_api_dev_shutdown(int fd, const char *method, const char *query, - const char *token_header, const char *client_ip); + const char *client_ip); void handle_api_user_context(int fd); void handle_api_diag_title(int fd, const char *query); diff --git a/src/cr_api_launch.c b/src/cr_api_launch.c index 76443d3..3660373 100644 --- a/src/cr_api_launch.c +++ b/src/cr_api_launch.c @@ -99,7 +99,7 @@ handle_launch(int fd, const char *query) { launch_worker_request_t *req = malloc(sizeof(*req)); if (!req) { - http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); + http_send_oom(fd); return; } snprintf(req->title_id, sizeof(req->title_id), "%s", title_id); diff --git a/src/cr_api_patches.c b/src/cr_api_patches.c index b0f46c6..189f592 100644 --- a/src/cr_api_patches.c +++ b/src/cr_api_patches.c @@ -15,10 +15,7 @@ /* Maximum XML source files to scan per title */ #define PATCHES_MAX_SOURCES 12 -/* Maximum entries across all source files for one title. - * Fixed cap so that the heap allocation stays bounded even with the - * larger PATCH_MAX_LINES (640). 128 covers 64 entries × 2 source files - * which is more than any game in the current patch repo. */ +/* Maximum entries across all source files for one title; 128 covers 64 entries x 2 sources, more than any current game. */ #define PATCHES_ALL_MAX 128 static const char * @@ -49,11 +46,7 @@ typedef struct { char game_title[128]; } merged_patches_t; -/* Returns a heap-allocated merged_patches_t that the caller must free. - * Fills entries in source-priority order. - * Prefers files whose basename starts with title_id (e.g. CUSA13529.xml) over - * cross-compatibility files (e.g. CUSA13893.xml that also lists CUSA13529). - * Falls back to all files only when no named-match file is found. */ +/* Caller must free the result; prefers files named after title_id over cross-compat files that merely list it. */ static merged_patches_t * build_merged_patches(const char *title_id) { char paths[PATCHES_MAX_SOURCES][384]; @@ -65,9 +58,7 @@ build_merged_patches(const char *title_id) { if (nsrc == 0) return m; - /* Determine which source files to use. Prefer files whose basename starts - * with title_id so that CUSA13529.xml wins over CUSA13893.xml when both - * list CUSA13529. Only fall back to all files if no named match exists. */ + /* Prefer basenames starting with title_id; fall back to all files only if none match. */ int use[PATCHES_MAX_SOURCES]; int nuse = 0; size_t tid_len = strlen(title_id); @@ -120,7 +111,7 @@ handle_patches_list(int fd, const char *query) { } merged_patches_t *m = build_merged_patches(title_id); - if (!m) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); return; } + if (!m) { http_send_oom(fd); return; } /* Detect running game version */ char game_ver[32] = {0}; @@ -270,7 +261,7 @@ handle_patches_apply(int fd, const char *query) { strcmp(force_str, "1") == 0); merged_patches_t *m = build_merged_patches(title_id); - if (!m) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); return; } + if (!m) { http_send_oom(fd); return; } if (m->count == 0) { free_merged(m); http_send_json(fd, 404, "{\"ok\":false,\"error\":\"patch_not_found\"}"); @@ -477,7 +468,7 @@ handle_patches_restore(int fd, const char *query) { static void handle_patches_files_list(int fd) { char *json = patch_files_list_json(); - if (!json) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}"); return; } + if (!json) { http_send_oom(fd); return; } http_send_json(fd, 200, json); free(json); } diff --git a/src/cr_appdb.c b/src/cr_appdb.c index baff4cc..a84c5d8 100644 --- a/src/cr_appdb.c +++ b/src/cr_appdb.c @@ -17,8 +17,6 @@ #include "third_party/sqlite3.h" #endif -#define CR_APPDB_MAX_GAMES 1024 - static int find_game_index(game_entry_t *entries, size_t count, const char *title_id) { for (size_t i = 0; i < count; i++) { @@ -132,10 +130,7 @@ collect_games(game_entry_t *entries, size_t *count) { } #if CHEATRUNNER_HAVE_SQLITE_APPDB -/* SQLite is compiled with SQLITE_THREADSAFE=0 — no internal locking at all. - * Concurrent sqlite3_* calls from multiple threads (even on separate connections) - * corrupt shared library state and crash. All SQLite entry points must hold - * this mutex. */ +/* SQLite is built with SQLITE_THREADSAFE=0; all entry points must hold this or concurrent calls corrupt state. */ static pthread_mutex_t g_sqlite_lock = PTHREAD_MUTEX_INITIALIZER; static int diff --git a/src/cr_appdb.h b/src/cr_appdb.h index 53706c2..f8165e1 100644 --- a/src/cr_appdb.h +++ b/src/cr_appdb.h @@ -4,6 +4,9 @@ #include #include "cr_titles.h" +/* Required capacity of entries[] passed to appdb_collect_games. */ +#define CR_APPDB_MAX_GAMES 1024 + int appdb_collect_games(game_entry_t *entries, size_t *count); int resolve_icon_path(const char *title_id, char *out, size_t out_size); diff --git a/src/cr_browser.h b/src/cr_browser.h index f6c673c..4c48d5b 100644 --- a/src/cr_browser.h +++ b/src/cr_browser.h @@ -5,12 +5,7 @@ #define CHEATRUNNER_HAVE_BROWSER_OPEN 0 #endif -/* - * cr_browser — thin wrapper around sceSystemServiceLaunchWebBrowser. - * Opens the system browser at the given URL. - * NOTE: This launches the normal system browser, NOT a confirmed in-game overlay. - * Overlay behavior is unconfirmed — test on console before enabling. - */ +/* Opens the system browser at url via sceSystemServiceLaunchWebBrowser; overlay behavior is unconfirmed, test on console. */ int cr_browser_open_url(const char *url); #endif /* CR_BROWSER_H */ diff --git a/src/cr_cheat_formats.c b/src/cr_cheat_formats.c index 82b76ca..056fd55 100644 --- a/src/cr_cheat_formats.c +++ b/src/cr_cheat_formats.c @@ -211,10 +211,7 @@ mc4_decrypt_to_xml(const char *cipher, size_t cipher_len, size_t *xml_size_out) *dst = '\0'; out_len = (size_t)(dst - xml); } - /* Sanity-check: a successful AES decrypt on corrupted or wrong-key input - * produces garbage bytes that happen not to be NULL. Valid XML always - * starts with '<'. Catching this here prevents shn_xml_to_json() from - * silently returning an empty mod list with no error. */ + /* Wrong-key/corrupted decrypt produces non-NULL garbage; valid XML always starts with '<'. */ if (out_len == 0 || xml[0] != '<') { free(xml); return NULL; diff --git a/src/cr_cheats.c b/src/cr_cheats.c index 55859c0..32084cf 100644 --- a/src/cr_cheats.c +++ b/src/cr_cheats.c @@ -167,11 +167,7 @@ find_cheat_file_for_title_dir(const char *dir, int depth, cheat_file_search_t *c closedir(d); } -/* - * Parse /data/cheatrunner/cheats/.txt for locally saved filenames. - * Each line may be a bare filename, a relative path, or a full path. - * URL lines (http/https) are skipped. - */ +/* Parse .txt for saved filenames (bare/relative/full path; URLs skipped). */ static void scan_local_txt_index(const char *fmt_name, int kind, cheat_file_search_t *ctx) { char idx_path[256]; @@ -349,12 +345,8 @@ apply_selection_rules(cheat_file_search_t *ctx) { ctx->selection_kind = CHEAT_SEL_GENERIC; return; } - /* 1c. No exact, no generic — fall back to best wrong-version candidate. - * This is a last-resort: if the only local files are wrong-version, loading - * them (with the visible mismatch warning) is better than showing "no cheat - * file found" and hiding the candidate selector entirely. The UI marks - * these with WRONG VER and the version-mismatch banner; the user can - * explicitly force or switch to a different file. */ + /* 1c. No exact/generic match — fall back to a wrong-version candidate + * (with a visible mismatch warning) rather than showing nothing. */ char wp[256]; int wk = 99, ws = -1; if (best_by_match(ctx, CAND_MATCH_WRONG_VERSION, wp, sizeof(wp), &wk, &ws)) { snprintf(ctx->best_path, sizeof(ctx->best_path), "%s", wp); @@ -836,12 +828,8 @@ crash_suspects_load(void) { cr_log("info", "cheats.guard", "loaded %d crash suspect(s) from disk", loaded); } -/* Returns the mastercode mod that governs target_mod_idx: the nearest preceding mod - * whose name contains "mastercode" / "master code" (case-insensitive). - * For files with a single mastercode group the result is identical to the old - * first-match search. For multi-group files (e.g. mod[0]="HP Mastercode" and - * mod[5]="Speed Mastercode") each group's dependent mods automatically pick up - * their own mastercode rather than always auto-enabling group 0's. */ +/* Nearest preceding mod whose name contains "mastercode" — governs target_mod_idx + * (supports multiple mastercode groups in one file). */ static cJSON * find_master_code_mod_for(cJSON *mods, int target_mod_idx) { cJSON *m = NULL; @@ -875,11 +863,8 @@ fixup_mc_dependent_addr(uint64_t mc_base_off, uint64_t dep_off) { return (mc_base_off & ~(uint64_t)0xff) | (dep_off & 0xff); } -/* Read the live MC region from process memory and scan for dep_off bytes. - * If found at offset i within the MC payload, *addr_out = mc_addr + i. - * Falls back to the low-byte combination when no match is found. - * Returns 0 on read failure (leaves addr_out unchanged so the caller keeps - * the normally-resolved address) or on hard allocation failure. */ +/* Scan the live MC region for dep_off bytes; falls back to the low-byte + * combination if not found. Returns 0 on read failure (addr_out unchanged). */ static int mc_scan_dep_addr(pid_t pid, intptr_t mc_addr, const uint8_t *mc_on, size_t mc_on_len, @@ -960,13 +945,11 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si char running_name[256] = {0}; char run_norm[10] = {0}; char cheat_norm[10] = {0}; - int attached = 0; int rc = -1; applied_patch_t applied[64]; /* pre-read: addr, old_bytes, len */ int pre_n = 0; /* entries pre-read */ uint8_t wi_data[64][128]; /* bytes to write per entry */ int wi_is_cave[64]; /* 1 if len >= 16 (code cave) */ - int wi_is_absolute[64]; /* 1 if entry used absolute addressing */ int written_order[64]; /* pre_n indices in actual write order (for rollback) */ int write_ok_n = 0; /* entries successfully written */ @@ -992,7 +975,6 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si int cooldown_ms = g_cfg.cheat_apply_cooldown_ms; int min_stable_ms = g_cfg.cheat_min_stable_ms; int mc_fixup = g_cfg.cheat_master_code_fixup; - int codecave_fb = g_cfg.cheat_codecave_fallback; int log_candidates = g_cfg.cheat_log_candidates; int addr_cache_enabled = g_cfg.cheat_addr_cache_enabled; int inter_mod_delay_ms = g_cfg.cheat_inter_mod_delay_ms; @@ -1052,10 +1034,8 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si } } - /* Base-address guard for relative-mode MC4/SHN: if the eboot base is still 0x0 - * the game module hasn't mapped yet. Relative addresses (base+offset) would equal - * the raw offset — same as absolute — producing wrong writes AND poisoning the - * addr_cache with incorrect addresses and zero orig_bytes for all future applies. */ + /* base==0 means the module hasn't mapped yet — relative addrs would equal + * the raw offset and poison the addr_cache with wrong addresses. */ if (base == 0 && kind != 1) { snprintf(err, err_size, "base_not_ready"); cr_log("warn", "cheats.guard", @@ -1238,47 +1218,35 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si } } - /* Mark apply in progress — suppresses scanner refresh during ptrace (Task 2) */ + /* Mark apply in progress — suppresses scanner refresh during the write loop (Task 2) */ g_cheat_applying = 1; cr_log("info", "cheats.guard", "pausing scanners during apply title=%s mod=%d pid=%d appId=0x%x", title_id, mod_index, (int)pid, app_id_live); - { - int _at = pt_attach_timed(pid, 2000); - if (_at < 0) { - g_cheat_applying = 0; - pthread_mutex_unlock(&g_cheat_apply_lock); - cJSON_Delete(root); - if (_at == -2) { - snprintf(err, err_size, "pt_attach timed out — game process unresponsive"); - cr_log("warn", "cheats.guard", "pt_attach timeout pid=%d title=%s — forcing monitor re-poll", - (int)pid, title_id); - /* Force the game monitor to re-evaluate state immediately: if the process - * is truly dead, kill(pid,0) will fail and the next monitor tick will clear it. */ - rpc_refresh_title_and_notify(); - } else { - pid_t live_pid = (app_id_live > 0) ? find_pid_for_app_id((uint32_t)app_id_live) : -1; - if (live_pid <= 0) { - snprintf(err, err_size, "game has exited"); - rpc_refresh_title_and_notify(); - } else if (live_pid != pid) { - snprintf(err, err_size, "game restarted (pid changed)"); - rpc_refresh_title_and_notify(); - } else { - snprintf(err, err_size, "pt_attach failed (%d)", errno); - } - } - return -1; + /* write_process_memory_forced/read_process_memory are mdbg-backed and need no + * ptrace attach, so just confirm the process is still alive before writing. */ + if (kill(pid, 0) != 0 && errno == ESRCH) { + g_cheat_applying = 0; + pthread_mutex_unlock(&g_cheat_apply_lock); + cJSON_Delete(root); + pid_t live_pid = (app_id_live > 0) ? find_pid_for_app_id((uint32_t)app_id_live) : -1; + if (live_pid <= 0) { + snprintf(err, err_size, "game has exited"); + rpc_refresh_title_and_notify(); + } else if (live_pid != pid) { + snprintf(err, err_size, "game restarted (pid changed)"); + rpc_refresh_title_and_notify(); + } else { + snprintf(err, err_size, "game process unresponsive"); } + return -1; } - attached = 1; /* Resolve per-mod base (may differ from eboot base if module_name is set) */ intptr_t mod_base = base; if (module_name[0]) { if (resolve_module_base(pid, module_name, base, &mod_base) != 0) { - pt_detach(pid, 0); g_cheat_applying = 0; pthread_mutex_unlock(&g_cheat_apply_lock); cJSON_Delete(root); @@ -1429,22 +1397,13 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si else if (!strcmp(title_addr_mode_pref, "relative")) { af = 0; inj = 0; adet = 0; } } - wi_is_absolute[pre_n] = af; /* MC fixup: save original offset for scan fallback, then apply low-byte adjustment */ uint64_t off_u_dep_raw = off_u; if (do_mc_fixup && mc_base_off != 0) { off_u = fixup_mc_dependent_addr(mc_base_off, off_u); } - /* Build the expected-bytes hint for address resolution probing. - * - * For JSON: off_bytes == original game code by contract → fully reliable. - * For MC4/SHN with an explicit field: exp_bytes → reliable. - * For MC4/SHN without explicit expected: pass off_bytes as a non-reliable - * hint (expected_reliable stays 0). cheat_resolve_write_addr_ex will - * try an off_bytes byte-exact probe before falling back to x86_probe. - * This catches the common case where ValueOff = original game code - * and eliminates x86_probe false-positives that land hooks in the - * wrong function. */ + /* Expected-bytes hint: JSON/explicit-expected are reliable; MC4/SHN + * without one is a soft hint only (expected_reliable stays 0). */ const uint8_t *expect_cmp = NULL; if (exp_len > 0) { expect_cmp = exp_bytes; @@ -1453,9 +1412,7 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si expect_cmp = off_bytes; expected_reliable = 1; } else if (kind != 1 && off_len > 0 && off_len == on_len) { - /* MC4/SHN: pass off_bytes as a soft probe hint. - * expected_reliable stays 0 so a mismatch falls through to - * x86_probe / fallback policy rather than blocking the apply. */ + /* Soft probe hint — a mismatch falls through to x86_probe/fallback. */ expect_cmp = off_bytes; } /* Fallback policy for MC4/SHN without a reliable expected baseline. */ @@ -1465,31 +1422,23 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si if (strcmp(fb_str, "legacy") == 0) fallback_pol = CR_ADDR_FALLBACK_LEGACY; else if (strcmp(fb_str, "absolute") == 0) fallback_pol = CR_ADDR_FALLBACK_ABSOLUTE; else if (strcmp(fb_str, "relative") == 0) fallback_pol = CR_ADDR_FALLBACK_RELATIVE; - /* Disable probe: reading both address candidates blindly can hang the game - * if the raw MC4/SHN offset maps to PS5 GPU/MMIO memory — the page-fault - * under ptrace never returns. Use the configured fallback policy directly. - * (Mirrors the identical guard in the dashboard state-read path.) */ + /* Disable probe — blindly reading both candidates can hang the game + * if the raw offset maps to PS5 GPU/MMIO. */ adet = 0; } addr = cheat_resolve_write_addr_ex(pid, entry_base, off_u, af, inj, adet, on_bytes, on_len, expect_cmp, expected_reliable, fallback_pol, &resolve_st, 0 /*silent=false*/); if (resolve_st == CR_ADDR_RESOLVE_UNRESOLVED) { - /* If expected bytes came from the addr_cache, the entry may be stale - * (game binary updated, or cache populated with wrong bytes). - * Recovery: try the cached address directly, then clear and retry - * without a baseline using the configured fallback policy. */ + /* addr_cache entry may be stale — try it directly before retrying without a baseline. */ if (entry_ace_hit[pre_n]) { intptr_t caddr = entry_ace_addr[pre_n]; int recovered = 0; if (caddr > 0) { uint8_t cprobe[128]; int cprobe_ok = (read_process_memory(pid, caddr, cprobe, on_len) == 0); - /* Accept the cached address when it holds either: - * (a) the original bytes stored at first-enable time, OR - * (b) the ValueOff bytes — cheat was disabled but address is - * still correct; this is the common case after a disable cycle - * where off_bytes (not orig bytes) were written back. */ + /* Accept the cached address if it holds the original bytes or ValueOff + * (disabled-but-correct-address case). */ int match_orig = cprobe_ok && memcmp(cprobe, exp_bytes, on_len) == 0; int match_off = cprobe_ok && effective_on && off_len == on_len && memcmp(cprobe, off_bytes, off_len) == 0; @@ -1498,11 +1447,8 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si resolve_st = CR_ADDR_RESOLVE_OK_VERIFIED; recovered = 1; if (match_off) { - /* ValueOff confirms the address is right, but the stored orig_bytes - * are stale (e.g. zeros from a prior apply when base was 0x0). - * Clear expected_reliable so the validation block below actually - * enters unverified mode instead of comparing cur_bytes against - * those stale orig_bytes and failing with "mismatch before ON". */ + /* orig_bytes are stale here — drop expected_reliable so validation + * doesn't compare against them and falsely fail. */ expected_reliable = 0; exp_len = 0; } @@ -1546,10 +1492,7 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si kind == 2 ? "shn" : "mc4"); break; } - /* Stale cache was discarded and the retry used no baseline. - * Reset expected_reliable so the validation block treats this - * entry as unverified — prevents spurious "mismatch before OFF" - * on disable when the addr_cache orig_bytes came from ON state. */ + /* Cache discarded, retried with no baseline — mark unverified. */ expected_reliable = 0; exp_len = 0; } @@ -1608,15 +1551,9 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si resolve_st == CR_ADDR_RESOLVE_OK_UNVERIFIED_RELATIVE || resolve_st == CR_ADDR_RESOLVE_OK_X86_PROBE || resolve_st == CR_ADDR_RESOLVE_OK_OFFBYTES_PROBE); - /* Also fire for explicit relative/absolute address modes: those resolve as - * OK_VERIFIED (no probe), but an MC4/SHN entry with no bytes is - * still unverified and is exactly the case we need to diagnose. */ + /* Also fires for explicit relative/absolute modes with no bytes. */ if ((is_legacy_unv || !expected_reliable) && kind != 1) { - /* Full byte dump (cur = what's there now, write = what we put down) so a - * frozen cheat can be diagnosed: for a hook entry the write bytes reveal - * the JMP target; for a cave entry the cur bytes reveal whether the cave - * address is real free space (zeros / 0xCC / NOP padding) or live game - * code/data we'd be corrupting. */ + /* Dump cur/write bytes so a frozen cheat can be diagnosed from the logs. */ char cur_hex[268] = {0}; char new_hex[268] = {0}; bytes_to_hex(cur_bytes, wlen, cur_hex, sizeof(cur_hex)); @@ -1625,10 +1562,8 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si "pre_write_bytes addr=0x%lx len=%zu format=%s type=%s cur=%s write=%s title=%s mod=%d entry=%d", (long)addr, wlen, kind == 2 ? "shn" : "mc4", (wlen >= 16 ? "cave" : "hook"), cur_hex, new_hex, title_id, mod_index, pre_n); - /* Null-byte hint: warn when target looks like uninitialized/data memory. - * Suppressed when offbytes_probe confirmed the address — in that case - * the null bytes ARE the expected ValueOff state of a pre-zeroed cave - * (MC4 caves live in BSS-adjacent space and are zero until written). */ + /* Warn on a null-byte target, unless offbytes_probe already confirmed + * it's a legitimately pre-zeroed cave. */ if (wlen >= 4 && resolve_st != CR_ADDR_RESOLVE_OK_OFFBYTES_PROBE) { int zero_cnt = 0; for (size_t _hz = 0; _hz < 4 && _hz < wlen; _hz++) @@ -1639,14 +1574,11 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si "likely not executable code; check cheat_%s_unverified_fallback config " "title=%s mod=%d", (long)addr, kind == 2 ? "shn" : "mc4", title_id, mod_index); - /* Cave writes (len>=16) to null memory mean shellcode would land in - * BSS/data — the hook JMP will crash the game. Block it unless - * allow_legacy / allow_unsafe is set, in which case the user has - * already accepted unverified writes and we warn-and-proceed. */ + /* Cave write to null memory has frozen the console before — gated on + * allow_unsafe only, never allow_legacy (that one defaults on). */ if (wlen >= 16) { - int _al_noex = (kind == 2) ? allow_legacy_shn : allow_legacy_mc4; int _al_unsafe = (kind == 2) ? allow_unsafe_shn : allow_unsafe_mc4; - if (!_al_noex && !_al_unsafe) { + if (!_al_unsafe) { rc = -1; snprintf(err, err_size, "cave_null_target entry[%d] addr=0x%lx — cave target is null bytes, " @@ -1656,7 +1588,7 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si } cr_log("warn", "cheats.mem", "cave_null_target_bypassed entry[%d] addr=0x%lx len=%zu — null bytes at cave target, " - "proceeding because allow_legacy=1; address may be wrong title=%s mod=%d", + "proceeding because allow_unsafe=1; address may be wrong title=%s mod=%d", pre_n, (long)addr, wlen, title_id, mod_index); } } @@ -1715,14 +1647,8 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si (long)addr, title_id, mod_index); } else { const uint8_t *must = (exp_len > 0) ? exp_bytes : off_bytes; - /* A cheat sitting in its ValueOff (disabled) state is a valid pre-ON - * baseline: after a disable cycle the address holds off_bytes, and for - * MC4/SHN cheats ValueOff frequently differs from the captured original - * bytes (exp_bytes from the addr_cache). Comparing only against - * exp_bytes here made every cache-backed MC4 cheat fail to re-enable - * after being turned off ("bytes mismatch before ON"). Accept off_bytes - * as an equally-valid baseline — the same recovery logic already exists - * in the address-resolution stale-cache path. */ + /* off_bytes is also a valid pre-ON baseline — exp_bytes alone made + * cache-backed MC4 cheats fail to re-enable after a disable. */ int matches_baseline = (memcmp(cur_bytes, must, wlen) == 0) || (off_len == wlen && memcmp(cur_bytes, off_bytes, wlen) == 0); if (!matches_baseline) { @@ -1732,10 +1658,8 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si "entry[%d] already_on at 0x%lx — re-enabling after session loss title=%s mod=%d", pre_n, (long)addr, title_id, mod_index); } else if (wlen >= 16) { - /* Cave overwrite: a code cave (long write) is free-form writable space. - * Allowed for same-file mode switching (e.g. speed cheats sharing a cave). - * Blocked when mods from a different file are active — their hooks still - * point to this cave; overwriting it causes a SIGSEGV when the hook fires. */ + /* Cave overwrite ok for same-file mode switching; blocked across files + * since another file's hooks may still point here (SIGSEGV risk). */ if (cheat_any_enabled_for_title(title_id, pid)) { rc = -1; snprintf(err, err_size, @@ -1753,21 +1677,13 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si "entry[%d] cave_overwrite at 0x%lx len=%zu — replacing existing cave content title=%s mod=%d", pre_n, (long)addr, wlen, title_id, mod_index); } else if (wlen >= 5 && on_bytes[0] == 0xE9 && cur_bytes[0] == 0xE9) { - /* JMP redirect: both the current bytes and the ON bytes are JMP rel32 - * instructions. This means a compatible hook is already installed but - * pointing to a different cave (e.g. Speed Normal → Speed 3x switch). - * Redirecting one JMP to another is safe — allow it. */ + /* Both sides are JMP rel32 — a compatible hook pointing elsewhere, safe to redirect. */ cr_log("info", "cheats", "entry[%d] hook_redirect at 0x%lx — replacing JMP with new target title=%s mod=%d", pre_n, (long)addr, title_id, mod_index); } else if (entry_ace_hit[pre_n]) { - /* The mismatch baseline came from the addr_cache, which is stale or - * poisoned: its orig_bytes don't match live memory, and the target - * isn't in the ON or ValueOff state either. Don't brick the cheat — - * clear the cache and proceed as an unverified write at the resolved - * address (which is just base+offset). This self-heals a bad cache - * instead of failing every apply, and is reached even when - * auto_detect=0 bypasses the resolve-time recovery path. */ + /* Stale/poisoned addr_cache entry — clear it and proceed unverified + * instead of bricking the cheat. */ cr_log("warn", "addr_cache", "entry[%d] stale cache baseline at 0x%lx — clearing entry and applying unverified title=%s mod=%d", pre_n, (long)addr, title_id, mod_index); @@ -1830,10 +1746,8 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si } } - /* ---- Cave-page promotion: reclassify short writes on known-cave pages ---- - * A write >=16 bytes establishes its page as a "cave page" for this mod. - * Any shorter write to that same page is also part of the cave region and must - * follow the same enable/disable ordering (caves-first / hooks-first). */ + /* A >=16 byte write marks its page as a cave page; shorter writes on that + * same page follow the same enable/disable ordering. */ if (rc == 0 && pre_n > 1) { intptr_t cave_pgs[64]; int cave_pg_n = 0; @@ -1892,72 +1806,49 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si "begin title=%s mod=%d write=%d addr=0x%lx len=%zu page=0x%lx span=0x%zx type=%s", title_id, mod_index, write_ok_n, (long)addr, wlen, (long)_pg, _sp, wi_is_cave[i] ? "cave" : "hook"); - int used_cave = 0; /* write_process_memory_forced skips kernel_get_vmem_protection, preventing kernel * panics on PS4 BC games and PS5 games with special vmem entry types that cause * kernel_get_vmem_protection to dereference garbage before it can return -5. */ int wrc = write_process_memory_forced(pid, addr, data, wlen); - if (wrc != 0 && codecave_fb) { - cr_log("info", "cheats.mem", "write failed trying codecave addr=0x%lx len=%zu rc=%d", - (long)addr, wlen, wrc); - wrc = write_via_codecave(pid, addr, data, wlen); - if (wrc == 0) { - used_cave = 1; - } - } - cr_log("debug", "cheats.mem", "write_rc=%d cave=%d addr=0x%lx", - wrc, used_cave, (long)addr); + cr_log("debug", "cheats.mem", "write_rc=%d addr=0x%lx", wrc, (long)addr); if (wrc != 0) { rc = -1; snprintf(err, err_size, "entry[%d] write failed at 0x%lx (rc=%d)", i, (long)addr, wrc); break; } - if (!used_cave) { - /* write_process_memory_forced always restores PROT_READ|PROT_EXEC, so the page is - * always readable for the external readback verify. */ - { - uint8_t cur_v[128]; - int rdrc = read_process_memory(pid, addr, cur_v, wlen); - if (rdrc != 0) { - cr_log("warn", "cheats.mem", - "ext_verify_unreadable addr=0x%lx len=%zu — write assumed ok", - (long)addr, wlen); - } else if (memcmp(cur_v, data, wlen) != 0) { - /* Bytes didn't stick — try anonymous-mmap codecave before giving up */ - if (codecave_fb && !wi_is_absolute[i] && - write_via_codecave(pid, addr, data, wlen) == 0) { - cr_log("info", "cheats.mem", - "ext_verify_fail recovered via codecave addr=0x%lx len=%zu", - (long)addr, wlen); - used_cave = 1; - } else { - char exp_h[52] = {0}, got_h[52] = {0}; - fmt_hex16(data, wlen, exp_h, sizeof(exp_h)); - fmt_hex16(cur_v, wlen, got_h, sizeof(got_h)); - cr_log("error", "cheats.mem", - "ext_verify_fail title=%s mod=%d write=%d addr=0x%lx exp=[%s] got=[%s]", - title_id, mod_index, write_ok_n, (long)addr, exp_h, got_h); - rc = -1; - snprintf(err, err_size, - "entry[%d] verify failed at 0x%lx — bytes not observed after write; see cheats.mem logs", - i, (long)addr); - break; - } - } - } - /* PS5 is x86-64: i/d caches coherent; write_process_memory_forced already restored RX. - * restore_rx is a legacy flag (default=0). Only apply if explicitly set AND - * cheat_restore_original_prot is disabled, to avoid stomping the correct protection. */ - cr_log("debug", "cheats.mem", - "icache_sync addr=0x%lx len=%zu x86_coherent prot_handled_by_write_fn", - (long)addr, wlen); - if (restore_rx && !restore_orig_prot) { - int rrc = kernel_mprotect(pid, _pg, _sp, PROT_READ | PROT_EXEC); + { + /* read_process_memory has its own XOM fallback if the page isn't readable. */ + uint8_t cur_v[128]; + int rdrc = read_process_memory(pid, addr, cur_v, wlen); + if (rdrc != 0) { cr_log("warn", "cheats.mem", - "restore_rx_legacy page=0x%lx span=0x%zx rc=%d (deprecated: use cheat_restore_original_prot)", - (long)_pg, _sp, rrc); + "ext_verify_unreadable addr=0x%lx len=%zu — write assumed ok", + (long)addr, wlen); + } else if (memcmp(cur_v, data, wlen) != 0) { + char exp_h[52] = {0}, got_h[52] = {0}; + fmt_hex16(data, wlen, exp_h, sizeof(exp_h)); + fmt_hex16(cur_v, wlen, got_h, sizeof(got_h)); + cr_log("error", "cheats.mem", + "ext_verify_fail title=%s mod=%d write=%d addr=0x%lx exp=[%s] got=[%s]", + title_id, mod_index, write_ok_n, (long)addr, exp_h, got_h); + rc = -1; + snprintf(err, err_size, + "entry[%d] verify failed at 0x%lx — bytes not observed after write; see cheats.mem logs", + i, (long)addr); + break; } } + /* x86-64 i/d caches are coherent, no flush needed. restore_rx is a legacy + * flag (default=0), only applies if explicitly set. */ + cr_log("debug", "cheats.mem", + "icache_sync addr=0x%lx len=%zu x86_coherent prot_untouched_by_write_fn", + (long)addr, wlen); + if (restore_rx && !restore_orig_prot) { + int rrc = kernel_mprotect(pid, _pg, _sp, PROT_READ | PROT_EXEC); + cr_log("warn", "cheats.mem", + "restore_rx_legacy page=0x%lx span=0x%zx rc=%d (deprecated: use cheat_restore_original_prot)", + (long)_pg, _sp, rrc); + } cr_log("info", "cheats", "write[%d] addr=0x%lx len=%zu type=%s ok", write_ok_n, (long)addr, wlen, wi_is_cave[i] ? "cave" : "hook"); written_order[write_ok_n++] = i; @@ -2086,10 +1977,6 @@ apply_cheat_json(const char *title_id, int mod_index, int turn_on, char *err, si pthread_mutex_unlock(&g_last_apply_lock); } - if (attached) { - pt_detach(pid, 0); - } - g_cheat_applying = 0; g_last_apply_at_ms = now_ms(); cr_log("info", "cheats.guard", "resuming scanners after apply title=%s mod=%d rc=%d", diff --git a/src/cr_cheats.h b/src/cr_cheats.h index 45fe789..b9490d0 100644 --- a/src/cr_cheats.h +++ b/src/cr_cheats.h @@ -110,11 +110,7 @@ extern last_game_exit_t g_last_game_exit; extern pthread_mutex_t g_last_apply_lock; extern last_apply_rec_t g_last_apply_rec; extern _Atomic int g_cheat_applying; -/* Global ptrace-session gate. Held across the whole attach→write→detach window by - * the cheat-apply path; the patch-apply and patch-restore paths take it too so no - * two threads ever hold the game pid under ptrace at once (a second pt_attach on an - * already-traced pid fails and its cleanup PT_DETACHes the first session). Dashboard - * read paths back off on g_cheat_applying, which every writer sets while holding it. */ +/* Held by cheat-apply/patch-apply/patch-restore so only one runs at a time. */ extern pthread_mutex_t g_cheat_apply_lock; extern volatile uint64_t g_last_apply_at_ms; extern volatile uint64_t g_post_apply_guard_until_ms; diff --git a/src/cr_config.c b/src/cr_config.c index f5527d4..5d897cc 100644 --- a/src/cr_config.c +++ b/src/cr_config.c @@ -22,7 +22,6 @@ cheatrunner_config_t g_cfg = { .cheat_validate_original_bytes = 1, .cheat_restore_rx = 0, .cheat_restore_original_prot = 1, - .cheat_index_cache_ttl_sec = 86400, .allow_force_enable = 0, .cheat_state_after_launch_delay_ms = 8000, .launch_post_timeout_grace_ms = 3000, @@ -43,7 +42,6 @@ cheatrunner_config_t g_cfg = { .cheat_mc4_unverified_fallback = "relative", .cheat_shn_unverified_fallback = "relative", .cheat_log_candidates = 0, - .launch_quick_verify_ms = 5000, .cheat_post_apply_watch_ms = 8000, .cheat_mark_crash_suspect = 1, .cheat_apply_one_at_a_time = 1, @@ -60,12 +58,9 @@ cheatrunner_config_t g_cfg = { .games_cache_ttl_ms = 30000, .appdb_debug_names = 0, .log_level = "info", - /* Enabled by default for MC4/SHN code-cave + master-code cheat compatibility. - * Both are additive (codecave_fallback only engages when a write doesn't stick; - * master_code_fixup only for MC-dependent mods), so currently-working cheats - * are unaffected. */ + /* Enabled by default: additive, only engages for MC-dependent mods, so + * currently-working cheats are unaffected. */ .cheat_master_code_fixup = 1, - .cheat_codecave_fallback = 1, .cheat_addr_cache_enabled = 1, .cheat_inter_mod_delay_ms = 0, .fan_min_c = 30, @@ -88,7 +83,6 @@ config_set_defaults(cheatrunner_config_t *cfg) { .cheat_validate_original_bytes = 1, .cheat_restore_rx = 0, .cheat_restore_original_prot = 1, - .cheat_index_cache_ttl_sec = 86400, .allow_force_enable = 0, .cheat_state_after_launch_delay_ms = 8000, .launch_post_timeout_grace_ms = 3000, @@ -106,7 +100,6 @@ config_set_defaults(cheatrunner_config_t *cfg) { .cheat_mc4_unverified_fallback = "relative", .cheat_shn_unverified_fallback = "relative", .cheat_log_candidates = 0, - .launch_quick_verify_ms = 5000, .cheat_post_apply_watch_ms = 8000, .cheat_mark_crash_suspect = 1, .cheat_apply_one_at_a_time = 1, @@ -124,7 +117,6 @@ config_set_defaults(cheatrunner_config_t *cfg) { .appdb_debug_names = 0, .log_level = "info", .cheat_master_code_fixup = 0, - .cheat_codecave_fallback = 0, .cheat_addr_cache_enabled = 1, .cheat_inter_mod_delay_ms = 0, .fan_min_c = 30, @@ -143,12 +135,10 @@ config_save_locked(void) { "launch_kill_current=%d\n" "launch_kill_delay_ms=%d\n" "launch_wait_timeout_ms=%d\n" - "launch_quick_verify_ms=%d\n" "cheat_engine=%d\n" "cheat_validate_original_bytes=%d\n" "cheat_restore_rx=%d\n" "cheat_restore_original_prot=%d\n" - "cheat_index_cache_ttl_sec=%d\n" "allow_force_enable=%d\n" "cheat_state_after_launch_delay_ms=%d\n" "launch_post_timeout_grace_ms=%d\n" @@ -182,7 +172,6 @@ config_save_locked(void) { "appdb_debug_names=%d\n" "log_level=%s\n" "cheat_master_code_fixup=%d\n" - "cheat_codecave_fallback=%d\n" "cheat_addr_cache_enabled=%d\n" "cheat_inter_mod_delay_ms=%d\n" "fan_min_c=%d\n" @@ -190,10 +179,10 @@ config_save_locked(void) { "theme=%s\n", g_cfg.http_port, g_cfg.auto_load_cheat_menu, g_cfg.auto_download_missing_cheat, g_cfg.launch_kill_current, - g_cfg.launch_kill_delay_ms, g_cfg.launch_wait_timeout_ms, g_cfg.launch_quick_verify_ms, + g_cfg.launch_kill_delay_ms, g_cfg.launch_wait_timeout_ms, g_cfg.cheat_engine, g_cfg.cheat_validate_original_bytes, g_cfg.cheat_restore_rx, g_cfg.cheat_restore_original_prot, - g_cfg.cheat_index_cache_ttl_sec, g_cfg.allow_force_enable, + g_cfg.allow_force_enable, g_cfg.cheat_state_after_launch_delay_ms, g_cfg.launch_post_timeout_grace_ms, g_cfg.cheat_address_auto_detect, g_cfg.cheat_mc4_address_mode, g_cfg.cheat_shn_address_mode, g_cfg.allow_unsafe_mc4_apply, g_cfg.allow_unsafe_shn_apply, @@ -209,7 +198,7 @@ config_save_locked(void) { g_cfg.cheat_source_cache_ttl_seconds, g_cfg.cheat_remote_max_file_bytes, g_cfg.title_lookup_enabled, g_cfg.title_lookup_cache_enabled, g_cfg.title_lookup_timeout_ms, g_cfg.games_cache_ttl_ms, g_cfg.appdb_debug_names, g_cfg.log_level, - g_cfg.cheat_master_code_fixup, g_cfg.cheat_codecave_fallback, + g_cfg.cheat_master_code_fixup, g_cfg.cheat_addr_cache_enabled, g_cfg.cheat_inter_mod_delay_ms, g_cfg.fan_min_c, g_cfg.fan_max_c, g_cfg.theme); @@ -269,8 +258,6 @@ config_load(void) { g_cfg.cheat_restore_rx = atoi(v) ? 1 : 0; /* legacy: kept for compat; default is now 0 */ } else if (!strcmp(k, "cheat_restore_original_prot")) { g_cfg.cheat_restore_original_prot = atoi(v) ? 1 : 0; - } else if (!strcmp(k, "cheat_index_cache_ttl_sec")) { - g_cfg.cheat_index_cache_ttl_sec = atoi(v); } else if (!strcmp(k, "allow_force_enable")) { g_cfg.allow_force_enable = atoi(v) ? 1 : 0; } else if (!strcmp(k, "cheat_state_after_launch_delay_ms")) { @@ -299,8 +286,6 @@ config_load(void) { g_cfg.cheat_log_candidates = atoi(v) ? 1 : 0; } else if (!strcmp(k, "launch_user_id")) { snprintf(g_cfg.launch_user_id, sizeof(g_cfg.launch_user_id), "%s", v); - } else if (!strcmp(k, "launch_quick_verify_ms")) { - g_cfg.launch_quick_verify_ms = atoi(v); } else if (!strcmp(k, "cheat_post_apply_watch_ms")) { g_cfg.cheat_post_apply_watch_ms = atoi(v); if (g_cfg.cheat_post_apply_watch_ms < 0) g_cfg.cheat_post_apply_watch_ms = 8000; @@ -339,8 +324,6 @@ config_load(void) { snprintf(g_cfg.log_level, sizeof(g_cfg.log_level), "%s", v); } else if (!strcmp(k, "cheat_master_code_fixup")) { g_cfg.cheat_master_code_fixup = atoi(v) ? 1 : 0; - } else if (!strcmp(k, "cheat_codecave_fallback")) { - g_cfg.cheat_codecave_fallback = atoi(v) ? 1 : 0; } else if (!strcmp(k, "cheat_addr_cache_enabled")) { g_cfg.cheat_addr_cache_enabled = atoi(v) ? 1 : 0; } else if (!strcmp(k, "cheat_inter_mod_delay_ms")) { @@ -414,7 +397,10 @@ void config_check_reload(void) { struct stat st; if (stat(CHEATRUNNER_CONFIG_PATH, &st) != 0) return; - if (st.st_mtime == g_cfg_mtime) return; + pthread_mutex_lock(&g_cfg_lock); + time_t last_mtime = g_cfg_mtime; + pthread_mutex_unlock(&g_cfg_lock); + if (st.st_mtime == last_mtime) return; cr_log("info", "config", "config file changed on disk, reloading"); config_load(); } diff --git a/src/cr_config.h b/src/cr_config.h index 04bf07c..c5d3610 100644 --- a/src/cr_config.h +++ b/src/cr_config.h @@ -18,7 +18,6 @@ typedef struct cheatrunner_config { int cheat_validate_original_bytes; int cheat_restore_rx; /* legacy/deprecated: use cheat_restore_original_prot instead */ int cheat_restore_original_prot; - int cheat_index_cache_ttl_sec; int allow_force_enable; int cheat_state_after_launch_delay_ms; int launch_post_timeout_grace_ms; @@ -36,7 +35,6 @@ typedef struct cheatrunner_config { char cheat_mc4_unverified_fallback[16]; /* block|legacy|absolute|relative */ char cheat_shn_unverified_fallback[16]; int cheat_log_candidates; - int launch_quick_verify_ms; int cheat_post_apply_watch_ms; int cheat_mark_crash_suspect; int cheat_apply_one_at_a_time; @@ -54,7 +52,6 @@ typedef struct cheatrunner_config { int appdb_debug_names; char log_level[16]; int cheat_master_code_fixup; - int cheat_codecave_fallback; int cheat_addr_cache_enabled; int cheat_inter_mod_delay_ms; int fan_min_c; /* lower bound for the fan-on threshold slider (default 30) */ diff --git a/src/cr_conflict.c b/src/cr_conflict.c index df1b843..c8b83b7 100644 --- a/src/cr_conflict.c +++ b/src/cr_conflict.c @@ -12,6 +12,8 @@ typedef struct { uint64_t off; size_t len; int is_abs; + int section; /* per-entry "section" (dynlib handle index), 0 = none */ + char module_name[64]; /* per-mod "module_name", "" = eboot/main */ } cmap_entry_t; int @@ -46,12 +48,16 @@ conflict_map_build(const char *json_text, cheat_conflict_map_t *out) { if (!cJSON_IsArray(mem)) mem = cJSON_GetObjectItem(mod, "patches"); if (!cJSON_IsArray(mem)) continue; + cJSON *mname_j = cJSON_GetObjectItem(mod, "module_name"); + const char *module_name = (cJSON_IsString(mname_j) && mname_j->valuestring) ? mname_j->valuestring : ""; + cJSON *e = NULL; cJSON_ArrayForEach(e, mem) { if (entry_n[mi] >= CONFLICT_MAX_ENTRIES) break; cJSON *off_j = cJSON_GetObjectItem(e, "offset"); cJSON *on_j = cJSON_GetObjectItem(e, "on"); cJSON *abs_j = cJSON_GetObjectItem(e, "absolute"); + cJSON *sec_j = cJSON_GetObjectItem(e, "section"); if (!cJSON_IsString(off_j) || !cJSON_IsString(on_j)) continue; uint64_t off_u = 0; if (parse_offset_hex_checked(off_j->valuestring, &off_u) != 0) continue; @@ -62,15 +68,12 @@ conflict_map_build(const char *json_text, cheat_conflict_map_t *out) { ce->off = off_u; ce->len = on_len; ce->is_abs = cJSON_IsTrue(abs_j) ? 1 : 0; + ce->section = (cJSON_IsNumber(sec_j) && sec_j->valuedouble > 0) ? (int)sec_j->valuedouble : 0; + snprintf(ce->module_name, sizeof(ce->module_name), "%s", module_name); } } - /* Compute mastercode group for each mod: group_id = index of nearest preceding - * mastercode, or -1 for mods before the first mastercode and -2 for mastercodes - * themselves. Two non-mastercode mods in different groups only conflict when - * they share an address AND both their mastercodes are active simultaneously — - * but the auto-enable logic treats them independently, so we skip cross-group - * pairs to avoid false-positive conflict blocks. */ + /* group_id = index of nearest preceding mastercode (-1 before any, -2 if mastercode itself); cross-group address overlaps are skipped to avoid false-positive conflicts. */ int mod_group[CONFLICT_MAX_MODS]; for (int mi = 0; mi < mod_count; mi++) mod_group[mi] = -1; { @@ -93,15 +96,16 @@ conflict_map_build(const char *json_text, cheat_conflict_map_t *out) { /* Skip: mastercode i vs one of its own dependents — they don't conflict; * enabling a dependent requires the mastercode to be active. */ if (mod_group[i] == -2 && mod_group[j] == i) continue; - /* Skip: two non-mastercode mods from different defined groups. - * Their raw offsets are both eboot-relative but the auto-enable system - * treats them under separate mastercodes; they can be on simultaneously. */ + /* Skip: different mastercode groups can be on simultaneously, so they don't conflict. */ if (mod_group[i] >= 0 && mod_group[j] >= 0 && mod_group[i] != mod_group[j]) continue; int found = 0; for (int ei = 0; ei < entry_n[i] && !found; ei++) { for (int ej = 0; ej < entry_n[j] && !found; ej++) { /* Don't compare abs vs relative — can't resolve without base */ if ((*entries_p)[i][ei].is_abs != (*entries_p)[j][ej].is_abs) continue; + /* Different module base — same offset, different real address. */ + if ((*entries_p)[i][ei].section != (*entries_p)[j][ej].section) continue; + if (strcmp((*entries_p)[i][ei].module_name, (*entries_p)[j][ej].module_name) != 0) continue; uint64_t a0 = (*entries_p)[i][ei].off; uint64_t a1 = a0 + (uint64_t)(*entries_p)[i][ei].len; uint64_t b0 = (*entries_p)[j][ej].off; diff --git a/src/cr_fan.c b/src/cr_fan.c index 76355d7..b316df1 100644 --- a/src/cr_fan.c +++ b/src/cr_fan.c @@ -159,10 +159,10 @@ fan_init(void) { static void send_obj(int fd, int status, cJSON *root) { - if (!root) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); return; } + if (!root) { http_send_oom(fd); return; } char *txt = cJSON_PrintUnformatted(root); cJSON_Delete(root); - if (!txt) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); return; } + if (!txt) { http_send_oom(fd); return; } http_send_json(fd, status, txt); free(txt); } diff --git a/src/cr_fan.h b/src/cr_fan.h index 96e28be..ba05f44 100644 --- a/src/cr_fan.h +++ b/src/cr_fan.h @@ -3,16 +3,9 @@ #include -/* CPU/SoC fan threshold control. - * - * Reads console temperatures and pins a fan-on threshold via the - * /dev/icc_fan ioctl (requires kstuff to be loaded for the device to exist). - * A watcher thread re-applies the pinned threshold periodically because the - * firmware resets fan state on every game/app launch. The pinned value is - * persisted to disk so it survives a payload redeploy. */ +/* Pins a fan-on threshold via /dev/icc_fan (needs kstuff); a watcher thread re-applies it since firmware resets fan state on every app launch. */ -/* Load the persisted threshold and start the re-apply watcher. Call once at - * boot. Safe to call when /dev/icc_fan is unavailable (no-op until a set). */ +/* Loads the persisted threshold and starts the re-apply watcher; call once at boot, no-op if /dev/icc_fan is unavailable. */ void fan_init(void); /* HTTP dispatcher — returns 1 if it handled the path, 0 otherwise. diff --git a/src/cr_game_monitor.c b/src/cr_game_monitor.c index 1e44ca8..d5e5500 100644 --- a/src/cr_game_monitor.c +++ b/src/cr_game_monitor.c @@ -57,9 +57,7 @@ find_pid_by_name(const char *name) { for (uint8_t *ptr = buf; ptr < (buf + buf_size);) { int ki_structsize = *(int *)ptr; if (ki_structsize <= 0 || (size_t)ki_structsize > (size_t)((buf + buf_size) - ptr)) break; - /* ki_pid at +72, ki_comm at +447 (MAXCOMLEN+1 = 20 bytes, last byte at 466). - * Need structsize >= 467; strict-less-than avoids off-by-one that would - * skip a record of exactly the minimum valid size. */ + /* Need structsize >= 467 to cover ki_comm at +447..+466; avoids off-by-one. */ if (ki_structsize < 467) { ptr += ki_structsize; continue; } pid_t ki_pid = *(pid_t *)&ptr[72]; char *ki_tdname = (char *)&ptr[447]; @@ -120,9 +118,7 @@ get_running_game_ex(pid_t *out_pid, char *out_title, size_t title_size, intptr_t if (kill(pid, 0) != 0 && errno == ESRCH) { return -1; } - /* Get title ID: try sceSystemServiceGetAppTitleId first — faster and doesn't - * require reading from the process namespace. - * Fall back to sceKernelGetAppInfo if unavailable. */ + /* Try the faster sceSystemServiceGetAppTitleId first, fall back to sceKernelGetAppInfo. */ char title_id_buf[16] = {0}; if (sceSystemServiceGetAppTitleId(app_id, title_id_buf) != 0 || title_id_buf[0] == '\0') { app_info_t info; @@ -200,21 +196,8 @@ read_running_state(running_game_state_t *out) { if (!st.title_name[0]) { snprintf(st.title_name, sizeof(st.title_name), "%s", title); } - /* Version detection — probe in priority order: - * 1. /proc/{pid}/root/patch0 — installed update, contains the current game version - * 2. /proc/{pid}/root/app0 — base game, correct only when no update is installed - * 3. /app0 — direct path (works if CheatRunner runs in the game namespace) - * 4. Static installed paths — appmeta scan + known filesystem layout patterns - * - * The old "/proc/{pid}/root/user/patch/{title}/..." path was wrong: PS5 updates are - * mounted as patch0 inside the game's namespace, not under user/patch/. */ - /* Per-pid version cache. Once a pid's version is known we never re-probe its - * filesystem again. Critical for robustness: reading /proc/{pid}/root/... of a - * FROZEN game (e.g. after a cave cheat strips WRITE and the game's next store - * page-faults) blocks the calling thread indefinitely in kernel VFS — this is - * what took CheatRunner down (monitor + HTTP threads all stuck → "network - * error" → resend ELF). A pid's game version never changes during its life, so - * caching is safe and also eliminates redundant per-500ms filesystem I/O. */ + /* Probe patch0 (update), then app0, then /app0, then static paths; PS5 updates mount as patch0, not user/patch/. */ + /* Cache version per pid: re-reading /proc/{pid}/root/... of a frozen game (cave cheat strips WRITE) blocks the thread in kernel VFS indefinitely, which previously hung the whole daemon. */ static pthread_mutex_t ver_cache_lock = PTHREAD_MUTEX_INITIALIZER; static pid_t ver_cache_pid = -1; static char ver_cache_cv[32]; @@ -402,13 +385,7 @@ rpc_refresh_title_and_notify(void) { } pthread_mutex_unlock(&g_mods_disabled_lock); mod_enabled_clear_for_pid(prev.pid); - /* Skip the ptrace-bearing patch restore while a cheat apply is in - * progress: the apply thread already owns this pid under ptrace, so a - * second pt_attach here would stall the 500 ms monitor poll for the full - * 2 s attach timeout and fail anyway (pid already traced). If the game - * genuinely died mid-apply the patched bytes are gone with the process, - * so there is nothing to restore; backups are still cleared below and the - * next poll re-evaluates once the apply releases. */ + /* Skip restore mid-apply: it shares g_cheat_apply_lock with apply and would stall the monitor poll until apply releases it. */ if (!g_cheat_applying) { patch_restore_all_for_pid(prev.pid, prev.title_id); } @@ -451,9 +428,7 @@ rpc_refresh_title_and_notify(void) { } } - /* Auto-clear crash suspects whose watch window passed without a game crash. - * Runs every poll tick while the game is running. If a mod stayed enabled for - * longer than cheat_post_apply_watch_ms with no crash, it is no longer a suspect. */ + /* Clear crash suspects that outlived cheat_post_apply_watch_ms with no crash. */ if (cur.running) { int wcms = 8000; pthread_mutex_lock(&g_cfg_lock); diff --git a/src/cr_http.c b/src/cr_http.c index dbf96b5..b4723bb 100644 --- a/src/cr_http.c +++ b/src/cr_http.c @@ -146,15 +146,33 @@ http_send_json(int fd, int status, const char *body) { } } +/* Grows *buf (cap+1 bytes allocated) so cap >= need, capped at MAX_REQ_SIZE. */ +static int +http_grow_buf(char **buf, size_t *cap, size_t need) { + if (need <= *cap) return 0; + size_t new_cap = *cap; + while (new_cap < need) { + new_cap *= 2; + if (new_cap >= MAX_REQ_SIZE) { new_cap = MAX_REQ_SIZE; break; } + } + char *nb = realloc(*buf, new_cap + 1); + if (!nb) return -1; + *buf = nb; + *cap = new_cap; + return 0; +} + static void http_handle_client(int fd, const char *client_ip) { - char *req = malloc(MAX_REQ_SIZE + 1); + /* Most requests (polling, toggles) are a few KB — start small and grow only + * for the rare large upload, instead of malloc'ing MAX_REQ_SIZE every time. */ + size_t cap = 65536; + char *req = malloc(cap + 1); if (!req) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); return; } size_t off = 0; - char *body_ptr = NULL; size_t body_len = 0; struct timeval tv; @@ -167,7 +185,12 @@ http_handle_client(int fd, const char *client_ip) { if (off >= MAX_REQ_SIZE) { break; } - int n = recv(fd, req + off, MAX_REQ_SIZE - off, 0); + if (off >= cap && http_grow_buf(&req, &cap, off + 1) != 0) { + http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); + free(req); + return; + } + int n = recv(fd, req + off, cap - off, 0); if (n <= 0) { if (n < 0 && errno == EINTR) { continue; @@ -203,7 +226,6 @@ http_handle_client(int fd, const char *client_ip) { size_t header_bytes = (size_t)(headers_end - req); req[header_bytes] = '\0'; - body_ptr = headers_end + sep_len; size_t already_body = off > (header_bytes + sep_len) ? (off - (header_bytes + sep_len)) : 0; size_t content_len = parse_content_length_header(req); size_t used = header_bytes + sep_len; @@ -212,13 +234,20 @@ http_handle_client(int fd, const char *client_ip) { free(req); return; } + /* Headers parsed — grow straight to the full known size up front, since + * pointers taken before a realloc would otherwise dangle. */ + if (http_grow_buf(&req, &cap, used + content_len) != 0) { + http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); + free(req); + return; + } while (already_body < content_len) { if (off >= MAX_REQ_SIZE) { http_send_json(fd, 413, "{\"ok\":false,\"error\":\"payload_too_large\"}"); free(req); return; } - int n = recv(fd, req + off, MAX_REQ_SIZE - off, 0); + int n = recv(fd, req + off, cap - off, 0); if (n <= 0) { if (n < 0 && errno == EINTR) { continue; @@ -234,30 +263,17 @@ http_handle_client(int fd, const char *client_ip) { return; } body_len = content_len; + char *body_ptr = req + header_bytes + sep_len; body_ptr[body_len] = '\0'; char method[8] = {0}; char target[1024] = {0}; char version[16] = {0}; - char token_header[128] = {0}; if (sscanf(req, "%7s %1023s %15s", method, target, version) != 3) { http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad request\"}"); free(req); return; } - const char *th = strstr(req, "\nX-CheatRunner-Token:"); - if (th) { - th += strlen("\nX-CheatRunner-Token:"); - while (*th == ' ' || *th == '\t') { - th++; - } - size_t i = 0; - while (*th && *th != '\r' && *th != '\n' && i + 1 < sizeof(token_header)) { - token_header[i++] = *th++; - } - token_header[i] = '\0'; - str_trim(token_header); - } char *query = strchr(target, '?'); if (query) { *query = '\0'; @@ -265,7 +281,7 @@ http_handle_client(int fd, const char *client_ip) { } else { query = ""; } - http_route(fd, method, target, query, token_header, client_ip ? client_ip : "unknown", + http_route(fd, method, target, query, client_ip ? client_ip : "unknown", body_ptr ? body_ptr : "", body_len); free(req); } @@ -302,11 +318,7 @@ http_server_thread(void *arg) { int opt = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); - /* SO_REUSEPORT lets a new instance bind immediately even if the old - * process is still alive (e.g. stuck in uninterruptible kernel sleep - * after SIGKILL). The old listener dies within milliseconds once it - * wakes from sleep; until then the kernel distributes new connections - * between both sockets — harmless given the overlap is extremely brief. */ + /* SO_REUSEPORT lets a new instance bind immediately even if a SIGKILL'd old process is still alive in kernel sleep. */ setsockopt(listen_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); @@ -318,11 +330,7 @@ http_server_thread(void *arg) { int be = errno; close(listen_fd); bind_fails++; - /* EADDRINUSE (FreeBSD errno 48) almost always means a previous CheatRunner - * is still listening on this port (SO_REUSEADDR only reclaims dead TIME_WAIT - * sockets, not a live listener). Surface that clearly once, then back off so - * the log/toast isn't flooded — but keep retrying so we recover on our own if - * the port frees up (e.g. the old instance finally exits). */ + /* EADDRINUSE almost always means a previous CheatRunner still holds the port (SO_REUSEADDR can't reclaim a live listener); warn once, then keep retrying. */ if (bind_fails <= 5) { log_msg("[HTTP] bind() failed on %d: %d", http_port, be); } @@ -356,10 +364,7 @@ http_server_thread(void *arg) { { char ip[64]; local_ip(ip, sizeof(ip)); - /* After a socket reopen (e.g. ECONNABORTED when a game exits), the - * network interface may reset briefly and local_ip() transiently - * returns "127.0.0.1". Keep the last known real LAN address so that - * dashboard QR/URL links remain valid. */ + /* local_ip() can transiently return 127.0.0.1 after a socket reopen; keep the last real LAN address for dashboard links. */ if (strcmp(ip, "127.0.0.1") != 0) snprintf(g_listen_ip, sizeof(g_listen_ip), "%s", ip); log_msg("Listening on: %s:%d", g_listen_ip, http_port); diff --git a/src/cr_launch.c b/src/cr_launch.c index 85ed800..cf25310 100644 --- a/src/cr_launch.c +++ b/src/cr_launch.c @@ -227,14 +227,7 @@ launch_title(uint64_t gen, const char *title_id, const char *args, const char ** *fguser_out = has_ctx; } - /* - * The launch worker is responsible for closing/waiting on the current BigApp. - * Do not kill the current app again here; doing so can race the actual launch. - * - * sceSystemServiceLaunchApp frequently returns 0x80940005 for mounted, fake-installed, - * or otherwise non-standard titles. Try the lower-level LncUtil path first, then keep - * SystemService as a compatibility fallback for titles that prefer it. - */ + /* Don't kill the current app here (launch worker already handles it, would race); try LncUtil first since SystemService often 0x80940005s on non-standard titles. */ lnc_rc = launch_title_with_lncutil(title_id, &ctx, has_ctx, argc > 0 ? argv_ptrs : NULL); if (rc_out) { *rc_out = lnc_rc; @@ -559,9 +552,7 @@ launch_worker_thread(void *arg) { uint64_t my_gen = req.generation; set_launch_status_ex_gen(my_gen, 1, "killing_current", req.title_id, "Closing current game...", 0, "", 0, 0); cr_log("info", "launch", "request title=%s args=\"%s\"", req.title_id, req.args[0] ? req.args : ""); - /* Preflight: if the monitor cache says the game is running, do a live system - * check before short-circuiting. The cache can be up to 500 ms stale after a - * manual close, causing CheatRunner to wrongly block a relaunch. */ + /* Verify with a live check before trusting the cache, which can be up to 500ms stale after a manual close. */ { running_game_state_t cur; running_state_get(&cur); diff --git a/src/cr_mdbg.c b/src/cr_mdbg.c new file mode 100644 index 0000000..e57ad16 --- /dev/null +++ b/src/cr_mdbg.c @@ -0,0 +1,137 @@ +#include +#include + +#include +#include + +#include "cr_log.h" +#include "cr_mdbg.h" +#include "pt.h" + +/* x86-64 page-table entry flags (machine/pmap.h defines these too, but pulls + * in struct pmap_statistics which isn't visible without extra SDK headers — + * just the 4 constants we need). */ +#define PG_FRAME (0x000ffffffffff000ul) +#define X86_PG_V 0x001ul +#define X86_PG_PS 0x080ul + +static unsigned int +mdbg_fw_version(void) { + static unsigned int cached = 0; + if (cached == 0) { + cached = kernel_get_fw_version() >> 16; + } + return cached; +} + + +int +mdbg_io_copyout(pid_t pid, intptr_t addr, void *buf, size_t len) { + pt_ucred_lock(); + int rc = mdbg_copyout(pid, addr, buf, len); + pt_ucred_unlock(); + return rc; +} + + +/* mdbg_copyin breaks on FW 8.20+ — walk CR3 to the physical page and write + * through the kernel direct map instead (ported from ps-patch-system). */ + +static unsigned long +vmspace_pmap_offset(unsigned int fw) { + if (fw >= 0x100 && fw <= 0x102) return 0x2C0; + if (fw >= 0x105 && fw <= 0x550) return 0x2E0; + if (fw >= 0x600 && fw <= 0x1200) return 0x2E8; + return 0; /* unsupported fw version */ +} + +static unsigned long g_dmap_base = 0; + +static unsigned long +mdbg_get_proc_cr3(pid_t pid) { + intptr_t proc = kernel_get_proc(pid); + if (!proc) return 0; + + unsigned long vmspace = (unsigned long)kernel_getlong(proc + KERNEL_OFFSET_PROC_P_VMSPACE); + if (!vmspace) return 0; + + unsigned long off = vmspace_pmap_offset(mdbg_fw_version()); + if (!off) return 0; + + /* pm_pml4 (KVA) + pm_cr3 (phys) sit next to each other; read both in one + * shot — free to read the extra 8 bytes and it lets us populate the dmap + * base cache for virt2phys below. */ + unsigned long data[2]; + if (kernel_copyout((intptr_t)(vmspace + off + 32), data, sizeof(data))) return 0; + + g_dmap_base = data[0] - data[1]; + return data[1]; +} + +static unsigned long +mdbg_virt2phys(unsigned long cr3, unsigned long va, unsigned long *phys_limit) { + unsigned long dmap_base = g_dmap_base; + cr3 &= PG_FRAME; + + /* PML4 (39) -> PDP (30) -> PD (21) -> PT (12) */ + for (int shift = 39; shift >= 12; shift -= 9) { + unsigned long index = (va >> shift) & 0x1FFul; + unsigned long entry_off = index * sizeof(unsigned long); + cr3 = (unsigned long)kernel_getlong((intptr_t)(dmap_base + cr3 + entry_off)); + + if (!(cr3 & X86_PG_V)) return (unsigned long)-1; /* not present */ + if ((cr3 & X86_PG_PS) || shift == 12) { + cr3 &= (1ul << 52) - (1ul << shift); + cr3 |= va & ((1ul << shift) - 1); + if (phys_limit) *phys_limit = (cr3 | ((1ul << shift) - 1)) + 1; + return cr3; + } + cr3 &= PG_FRAME; + } + return (unsigned long)-1; +} + +static int +mdbg_phys_write(unsigned long cr3, const void *src, unsigned long remote_va, unsigned long len) { + const unsigned char *p = src; + if (g_dmap_base == 0) return -1; + + while (len) { + unsigned long phys_end = 0; + unsigned long phys = mdbg_virt2phys(cr3, remote_va, &phys_end); + if (phys == (unsigned long)-1) return -1; + + size_t chunk = (size_t)(phys_end - phys); + if (chunk > len) chunk = len; + + if (kernel_copyin(p, (intptr_t)(g_dmap_base + phys), chunk)) return -1; + + remote_va += chunk; + p += chunk; + len -= chunk; + } + return 0; +} + + +int +mdbg_io_copyin(pid_t pid, const void *buf, intptr_t addr, size_t len) { + pt_ucred_lock(); + + if (mdbg_fw_version() <= 0x820) { + int rc = mdbg_copyin(pid, buf, addr, len); + pt_ucred_unlock(); + return rc; + } + + unsigned long cr3 = mdbg_get_proc_cr3(pid); + if (!cr3) { + cr_log("warn", "mdbg", "cr3_resolve_failed pid=%d fw=0x%x — write blocked", (int)pid, mdbg_fw_version()); + pt_ucred_unlock(); + return -1; + } + + int rc = mdbg_phys_write(cr3, buf, (unsigned long)addr, len); + pt_ucred_unlock(); + return rc; +} diff --git a/src/cr_mdbg.h b/src/cr_mdbg.h new file mode 100644 index 0000000..b877c1b --- /dev/null +++ b/src/cr_mdbg.h @@ -0,0 +1,15 @@ +#ifndef CR_MDBG_H +#define CR_MDBG_H + +#include +#include +#include + +/* mdbg-backed memory I/O — unlike ptrace, doesn't require attaching/stopping + * the target. Writes fall back to a CR3 walk on FW 8.20+ (mdbg_copyin breaks + * there); reads are unaffected. */ + +int mdbg_io_copyout(pid_t pid, intptr_t addr, void *buf, size_t len); +int mdbg_io_copyin(pid_t pid, const void *buf, intptr_t addr, size_t len); + +#endif diff --git a/src/cr_memory.c b/src/cr_memory.c index b2d3234..1a5e4d7 100644 --- a/src/cr_memory.c +++ b/src/cr_memory.c @@ -11,9 +11,9 @@ #include "cr_log.h" #include "cr_config.h" +#include "cr_mdbg.h" #include "cr_memory.h" #include "ps5sdk_compat.h" -#include "pt.h" static int hex_nibble(char c) { @@ -61,45 +61,18 @@ parse_hex_bytes_checked(const char *s, uint8_t *out, size_t out_cap, size_t *out return 0; } -/* Canonical x86-64 user-space limit. Probe reads above this address risk - * hitting kernel space or PS5 MMIO regions that cause ptrace page-fault hangs. */ -#define ADDR_IN_USER_RANGE(a) ((intptr_t)(a) >= 0x1000L && (intptr_t)(a) <= (intptr_t)0x7FFFFFFFFFFFL) - int read_process_memory(pid_t pid, intptr_t addr, uint8_t *out, size_t len) { if (!out || len == 0) return -1; - /* Fast path: data/heap pages are readable, so this succeeds for most reads - * and the page protection is never touched. */ - if (pt_copyout(pid, addr, out, len) == 0) return 0; - - /* Slow path. PS5 maps game .text execute-only (PROT_EXEC, no PROT_READ), so a - * raw pt_copyout on code faults — this is why baseline reads of hook sites came - * back unreadable and cheats showed mismatch/partial. Temporarily add PROT_READ, - * re-read, then restore the page's ORIGINAL protection so W^X is preserved. - * - * Guard the address first: kernel_get_vmem_protection can panic on a bad pointer - * (the same reason write_process_memory_forced avoids it), so never call it for - * an address outside user space. */ - if (!ADDR_IN_USER_RANGE(addr)) return -1; - - intptr_t page = (intptr_t)ROUND_PG_DOWN((uintptr_t)addr); - size_t span = (size_t)(ROUND_PG_UP((uintptr_t)addr + len) - (uintptr_t)page); - - int op = kernel_get_vmem_protection(pid, page, span); - if (op < 0) return -1; /* genuinely unmapped */ - if (op & PROT_READ) return -1; /* already readable — the read truly failed */ - - if (kernel_mprotect(pid, page, span, op | PROT_READ) != 0) return -1; - int rc = pt_copyout(pid, addr, out, len); - kernel_mprotect(pid, page, span, op); /* restore original protection (keeps W^X) */ - return rc < 0 ? -1 : 0; + /* No XOM fallback anymore — kernel_get_vmem_protection/kernel_mprotect on a + * wrong/special address can hang or panic, same risk already fixed on the + * write side. An execute-only page with no PROT_READ just reads as failed. */ + return mdbg_io_copyout(pid, addr, out, len) == 0 ? 0 : -1; } -/* Returns 1 if the first two bytes look like a plausible x86-64 instruction start. - * Used to distinguish absolute vs relative SHN/MC4 offsets when no expected bytes exist. - * Using two bytes for REX-prefixed instructions cuts the false-positive rate significantly: - * 0x48 ("H") is common in ASCII strings and pointer data, but REX+valid_opcode is not. */ +/* Returns 1 if the first two bytes look like a plausible x86-64 instruction + * start — used to guess abs vs relative offsets when no expected bytes exist. */ static int x86_looks_like_code(const uint8_t *b, size_t len) { if (!b || len == 0) return 0; @@ -179,8 +152,9 @@ cheat_resolve_write_addr_ex(pid_t pid, intptr_t base, uint64_t off_u, cr_addr_resolve_status_t *resolve_status, int silent) { if (resolve_status) *resolve_status = CR_ADDR_RESOLVE_OK_VERIFIED; + /* Unsigned add avoids signed-overflow UB on a huge/garbage off_u. */ intptr_t abs_addr = (intptr_t)off_u; - intptr_t rel_addr = base + (intptr_t)off_u; + intptr_t rel_addr = (intptr_t)((uint64_t)base + off_u); /* JSON or explicit absolute flag: address is determined without heuristics. */ if (!is_non_json || abs_flag) { @@ -189,20 +163,13 @@ cheat_resolve_write_addr_ex(pid_t pid, intptr_t base, uint64_t off_u, /* Non-JSON without reliable expected bytes: try several probes before falling back. */ if (!expected_reliable) { - /* Tier 1 — off_bytes byte-exact probe (highest confidence, runs for any length). - * - * When the caller supplies expect_b (off_bytes from MC4/SHN ValueOff) we read - * the full byte_len from both candidates and pick the one that matches exactly. - * This is far more reliable than the x86 heuristic: for well-formed cheats - * ValueOff == original game code bytes, which are unique to the correct address. - * Covers both hooks AND code caves (byte_len >= 16) which x86_probe skips. */ + /* Tier 1: exact off_bytes match against both candidates — most reliable, + * covers hooks and caves alike (unlike the x86 heuristic below). */ if (expect_b != NULL && auto_detect && pid > 0 && off_u >= 0x1000ULL && byte_len > 0 && byte_len <= 128) { uint8_t probe_off[128]; - /* Read the relative candidate first: the absolute candidate is the raw - * cheat offset and can map to PS5 GPU/MMIO, where a ptrace read page-faults - * and freezes the game. Checking base+offset first means the raw offset is - * only touched when relative didn't match. */ + /* Relative first — the raw absolute offset can map to PS5 GPU/MMIO and + * freeze the game on read. */ int rel_off_match = (ADDR_IN_USER_RANGE(rel_addr) && read_process_memory(pid, rel_addr, probe_off, byte_len) == 0 && memcmp(probe_off, expect_b, byte_len) == 0); @@ -234,19 +201,11 @@ cheat_resolve_write_addr_ex(pid_t pid, intptr_t base, uint64_t off_u, (unsigned long long)off_u, (long)abs_addr, (long)rel_addr); } - /* Tier 2 — X86 instruction heuristic probe (hooks only, len < 16). - * - * Reads 4 bytes from both candidates and checks which one starts with a - * plausible x86-64 instruction prefix. Weaker than off_bytes probe (first-byte - * heuristic has ~30-40% false-positive rate) but still useful when ValueOff is - * generic (NOPs/zeros) and both abs and rel are mapped memory. */ + /* Tier 2: x86 instruction-prefix heuristic, hooks only — weaker than Tier 1 + * but useful when ValueOff is generic (NOPs/zeros). */ if (auto_detect && pid > 0 && off_u >= 0x1000ULL && byte_len > 0 && byte_len < 16) { uint8_t pb_rel[4] = {0}, pb_abs[4] = {0}; - /* Relative-first (same rule as Tier 1 and the reliable-probe branch): never - * read the raw absolute offset until the relative candidate is ruled out. - * The raw offset can map to PS5 GPU/MMIO, where a ptrace read page-faults and - * never returns, freezing the game. If the relative candidate already looks - * like code we return it without ever touching the absolute candidate. */ + /* Relative first again — same GPU/MMIO freeze risk on the raw offset. */ int rel_read = (ADDR_IN_USER_RANGE(rel_addr) && read_process_memory(pid, rel_addr, pb_rel, 4) == 0); if (rel_read && x86_looks_like_code(pb_rel, 4)) { @@ -320,13 +279,7 @@ cheat_resolve_write_addr_ex(pid_t pid, intptr_t base, uint64_t off_u, return rel_addr; } - /* Probe the RELATIVE candidate first and return on match. The absolute - * candidate is the raw cheat offset, which on PS5 can map to GPU/MMIO; - * reading it under ptrace triggers a kernel page-fault that never returns, - * freezing the game. base+offset is the correct target for the vast - * majority of MC4/SHN cheats, so checking it first means the raw offset is - * never touched in the common case. Preserves the previous tie-break - * (relative wins when both match). */ + /* Relative candidate first (same GPU/MMIO freeze risk); relative wins ties. */ uint8_t probe[128]; if (read_process_memory(pid, rel_addr, probe, byte_len) == 0 && @@ -416,10 +369,7 @@ write_process_memory_forced(pid_t pid, intptr_t addr, const uint8_t *data, size_ (long)addr); return -7; } - /* Split cross-page writes at each page boundary. kernel_mprotect with a - * multi-page span returns 0 on PS5 but silently only unlocks the first page; - * subsequent pages stay R-X and pt_copyin silently truncates the write. - * One mprotect+write per page is the only reliable approach. */ + /* Split cross-page writes — old kernel_mprotect multi-page quirk, kept defensively. */ intptr_t next_page = (intptr_t)ROUND_PG_UP((uintptr_t)addr + 1); if (addr + (intptr_t)len > next_page) { size_t first = (size_t)(next_page - addr); @@ -428,68 +378,14 @@ write_process_memory_forced(pid_t pid, intptr_t addr, const uint8_t *data, size_ return write_process_memory_forced(pid, next_page, data + first, len - first); } - intptr_t page = (intptr_t)ROUND_PG_DOWN((uintptr_t)addr); - size_t span = (size_t)(ROUND_PG_UP((uintptr_t)addr + len) - (uintptr_t)page); - /* W^X: make the page WRITABLE (drop EXEC) for the write, never W+X. PS5's - * hypervisor rejects a writable+executable code mapping; setting RWX — even - * momentarily — can trigger a delayed kernel panic when the page is next - * executed (this was the repeated PPSA30803 cheat-apply panic). We restore - * R-X after the write so the patched code stays executable but not writable. */ - int mrc = kernel_mprotect(pid, page, span, PROT_READ | PROT_WRITE); - cr_log("debug", "cheats.mem", "mprotect_rw page=0x%lx span=0x%zx rc=%d", - (long)page, span, mrc); - if (mrc != 0) { - /* kernel_mprotect refused to make this page writable (special vmem entry, - * hardware-enforced protection, or PS5 integrity guard). - * - * Strategy: - * 1. Try pt_copyin without mprotect — ptrace on a jailbroken PS5 can - * often write to protected pages directly. - * 2. If that also fails AND the write is a large cave (>=16 bytes): - * try write_via_codecave (remaps the page with MAP_FIXED). - * 3. For small hook writes (<16 bytes): NEVER remap — MAP_FIXED on a - * kernel-protected code page causes a kernel panic when the page is - * executed. Fail cleanly instead. */ - cr_log("warn", "cheats.mem", - "mprotect_failed page=0x%lx rc=%d — trying direct ptrace write addr=0x%lx len=%zu", - (long)page, mrc, (long)addr, len); - int drc = pt_copyin(pid, data, addr, len); - if (drc == 0) { - cr_log("info", "cheats.mem", - "direct_ptrace_write ok addr=0x%lx len=%zu (mprotect was refused)", (long)addr, len); - return 0; - } - cr_log("warn", "cheats.mem", - "direct_ptrace_write failed addr=0x%lx len=%zu rc=%d", (long)addr, len, drc); - if (len >= 16) { - cr_log("info", "cheats.mem", - "codecave_remap fallback addr=0x%lx len=%zu (large cave write)", (long)addr, len); - return write_via_codecave(pid, addr, data, len); - } - cr_log("error", "cheats.mem", - "hook_write_blocked addr=0x%lx len=%zu — page is kernel-protected and cannot be remapped safely; skipping to prevent kernel panic", - (long)addr, len); - return -6; - } - int wrc = pt_copyin(pid, data, addr, len); - cr_log("debug", "cheats.mem", "copyin addr=0x%lx len=%zu rc=%d", (long)addr, len, wrc); - /* Restore R-X (drop WRITE, re-add EXEC). Combined with the RW mprotect above, - * the page is never Writable+eXecutable at the same instant — satisfying PS5's - * hypervisor W^X rule. Future writes go through pt_copyin (ptrace PT_IO), which - * bypasses page protection on a jailbroken PS5, so dropping WRITE here is safe. */ - int rrc = kernel_mprotect(pid, page, span, PROT_READ | PROT_EXEC); - if (rrc != 0) { - /* Restore failed. On PS4 BC pages the emulation layer manages page protections - * independently — mprotect(RX) is rejected but the hardware PTE still allows - * execution, so the written bytes execute correctly. Returning success here - * matches the pre-existing behaviour that patches on Bloodborne and similar - * PS4 BC titles depended on. */ - cr_log("warn", "cheats.mem", - "restore_rx_failed page=0x%lx span=0x%zx rc=%d", (long)page, span, rrc); - } + /* No kernel_mprotect: mdbg bypasses page protection, and mprotect itself was + * the thing corrupting kernel VM structures on wrong/special pages. */ + int wrc = mdbg_io_copyin(pid, data, addr, len); + cr_log("debug", "cheats.mem", "direct_mdbg_write addr=0x%lx len=%zu rc=%d", + (long)addr, len, wrc); if (wrc == 0 && len <= 128) { uint8_t vbuf[128]; - if (pt_copyout(pid, addr, vbuf, len) == 0 && memcmp(vbuf, data, len) != 0) + if (mdbg_io_copyout(pid, addr, vbuf, len) == 0 && memcmp(vbuf, data, len) != 0) cr_log("warn", "cheats.mem", "write_verify_mismatch addr=0x%lx len=%zu — bytes did not change after write", (long)addr, len); @@ -497,60 +393,6 @@ write_process_memory_forced(pid_t pid, intptr_t addr, const uint8_t *data, size_ return (wrc < 0) ? -1 : 0; } -int -write_via_codecave(pid_t pid, intptr_t addr, const uint8_t *data, size_t len) { - if (!data || len == 0 || len > 256) return -1; - intptr_t page = (intptr_t)ROUND_PG_DOWN((uintptr_t)addr); - size_t span = (size_t)(ROUND_PG_UP((uintptr_t)addr + len) - (uintptr_t)page); - - /* Save original page content before replacing the mapping */ - uint8_t *orig = malloc(span); - if (!orig) return -1; - if (pt_copyout(pid, page, orig, span) < 0) { - cr_log("warn", "cheats.cave", "cave read orig failed addr=0x%lx span=%zu", (long)page, span); - free(orig); - return -1; - } - - /* Replace the existing mapping with an anonymous RW page at the same address */ - intptr_t cave = pt_mmap(pid, page, span, - PROT_READ | PROT_WRITE, - MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, - -1, 0); - if (cave <= 0) { - cr_log("warn", "cheats.cave", "cave pt_mmap failed addr=0x%lx span=%zu rc=%ld", (long)page, span, (long)cave); - free(orig); - return -1; - } - - /* Restore original bytes then apply the patch. Keep `orig` until after the - * verify so a failed write can be rolled back — freeing it here would leave a - * half-applied patch in the anonymous mapping with no way to restore. */ - pt_copyin(pid, orig, page, span); - pt_copyin(pid, data, addr, len); - - /* Set R-X before verify. Never leave a page RWX — PS5's memory integrity - * scanner panics on executable+writable pages. pt_copyin (ptrace PT_IO) - * bypasses page protections on jailbroken PS5, so the rollback write below - * works without needing to re-elevate to RWX. */ - kernel_mprotect(pid, page, span, PROT_READ | PROT_EXEC); - - /* Verify */ - uint8_t vbuf[256]; - if (pt_copyout(pid, addr, vbuf, len) < 0 || memcmp(vbuf, data, len) != 0) { - cr_log("warn", "cheats.cave", - "cave verify failed addr=0x%lx len=%zu — restoring original page", (long)addr, len); - /* Roll back via ptrace (bypasses R-X protection on jailbroken PS5). */ - pt_copyin(pid, orig, page, span); - kernel_mprotect(pid, page, span, PROT_READ | PROT_EXEC); - free(orig); - return -1; - } - free(orig); - cr_log("info", "cheats.cave", "cave write ok addr=0x%lx len=%zu", (long)addr, len); - return 0; -} - void get_cheat_addr_flags(int kind, int entry_abs_flag, int auto_detect, int *abs_flag_out, int *is_non_json_out, int *auto_detect_out) { diff --git a/src/cr_memory.h b/src/cr_memory.h index 81db130..d54ee20 100644 --- a/src/cr_memory.h +++ b/src/cr_memory.h @@ -8,6 +8,10 @@ #define ROUND_PG_DOWN(x) ((uintptr_t)(x) & ~(uintptr_t)0x3fff) #define ROUND_PG_UP(x) (((uintptr_t)(x) + 0x3fff) & ~(uintptr_t)0x3fff) +/* Canonical x86-64 user-space limit — addresses outside this risk hitting + * kernel space or PS5 MMIO regions. */ +#define ADDR_IN_USER_RANGE(a) ((intptr_t)(a) >= 0x1000L && (intptr_t)(a) <= (intptr_t)0x7FFFFFFFFFFFL) + int parse_offset_hex_checked(const char *s, uint64_t *out); int parse_hex_bytes_checked(const char *s, uint8_t *out, size_t out_cap, size_t *out_len); @@ -61,14 +65,7 @@ int resolve_module_base(pid_t pid, const char *module_name, intptr_t eboot_base, /* Returns 1 if the process has libScePs2EmuMenuDialog.sprx loaded (PS2 emulation mode). */ int process_is_ps2_emu(pid_t pid); -/* Code-cave write: allocates an anonymous page at the target address via pt_mmap MAP_FIXED, - * preserves surrounding code, writes data, then mprotects RX. - * Use when write_process_memory_forced fails and codecave config is enabled. */ -int write_via_codecave(pid_t pid, intptr_t addr, const uint8_t *data, size_t len); - -/* Forced write: skips kernel_get_vmem_protection, uses RWX→write→RX directly. - * Safe for all games including PS4 BC and PS5 games with special vmem entry types that - * cause kernel_get_vmem_protection to panic. Always restores page to PROT_READ|PROT_EXEC. */ +/* Forced write via mdbg — bypasses page protection, never touches it, no ptrace. */ int write_process_memory_forced(pid_t pid, intptr_t addr, const uint8_t *data, size_t len); diff --git a/src/cr_notifications.c b/src/cr_notifications.c index b3aac65..6f03c40 100644 --- a/src/cr_notifications.c +++ b/src/cr_notifications.c @@ -15,15 +15,35 @@ notification_entry_t g_notifications[MAX_NOTIFICATIONS]; int g_notifications_count = 0; int g_notification_next_id = 1; +static void * +notify_send_thread(void *arg) { + notify_request_t *req = arg; + sceKernelSendNotificationRequest(0, req, sizeof(*req), 0); + free(req); + return NULL; +} + +/* Fire-and-forget on a detached thread — sceKernelSendNotificationRequest can + * stall under rapid repeated calls, and notify() is often called while a + * caller still holds g_cheat_apply_lock. */ void notify(const char *fmt, ...) { - notify_request_t req; + notify_request_t *req = malloc(sizeof(*req)); + if (!req) return; va_list args; - bzero(&req, sizeof(req)); + bzero(req, sizeof(*req)); va_start(args, fmt); - vsnprintf(req.message, sizeof(req.message), fmt, args); + vsnprintf(req->message, sizeof(req->message), fmt, args); va_end(args); - sceKernelSendNotificationRequest(0, &req, sizeof(req), 0); + + pthread_t t; + pthread_attr_t a; + pthread_attr_init(&a); + pthread_attr_setdetachstate(&a, PTHREAD_CREATE_DETACHED); + if (pthread_create(&t, &a, notify_send_thread, req) != 0) { + free(req); + } + pthread_attr_destroy(&a); } void diff --git a/src/cr_patch_parser.c b/src/cr_patch_parser.c index 92677e4..66d1418 100644 --- a/src/cr_patch_parser.c +++ b/src/cr_patch_parser.c @@ -12,6 +12,7 @@ #include #include "cr_log.h" +#include "cr_mdbg.h" #include "cr_memory.h" #include "cr_notifications.h" #include "cr_paths.h" @@ -214,26 +215,24 @@ patch_restore_entry(const char *title_id, const char *entry_id, pid_t pid, *rec = g_patch_backups[slot]; pthread_mutex_unlock(&g_patch_backup_lock); - /* Hold the global ptrace gate across attach→detach (blocking — this is a user - * action, so waiting for an in-flight apply to finish is fine). Prevents a - * concurrent attach from another thread tearing down this restore session. */ + /* Block on the apply gate (fine, this is a user action) and flag g_cheat_applying for other subsystems. */ pthread_mutex_lock(&g_cheat_apply_lock); g_cheat_applying = 1; - int _at = pt_attach_timed(pid, 3000); - if (_at < 0) { + /* write_process_memory_forced is mdbg-backed and needs no ptrace attach, + * so just confirm the process is still alive before writing. */ + if (kill(pid, 0) != 0 && errno == ESRCH) { g_cheat_applying = 0; pthread_mutex_unlock(&g_cheat_apply_lock); free(rec); if (result) { snprintf(result->error, sizeof(result->error), "attach_failed"); - snprintf(result->message, sizeof(result->message), "pt_attach failed — game may have exited"); + snprintf(result->message, sizeof(result->message), "game has exited"); } return -1; } int errors = 0; - pt_batch_begin(); /* elevate once for the whole restore loop */ for (int i = 0; i < rec->line_count; i++) { int wrc = write_process_memory_forced(pid, (intptr_t)rec->lines[i].address, rec->lines[i].bytes, rec->lines[i].len); @@ -243,8 +242,6 @@ patch_restore_entry(const char *title_id, const char *entry_id, pid_t pid, errors++; } } - pt_batch_end(); - pt_detach(pid, 0); g_cheat_applying = 0; pthread_mutex_unlock(&g_cheat_apply_lock); @@ -303,27 +300,14 @@ patch_restore_all_for_pid(pid_t pid, const char *title_id) { "game_stop: restoring %d applied patch(es) title=%s pid=%d", count, title_id ? title_id : "?", (int)pid); - /* Take the global ptrace gate so we never attach a pid another thread is - * already tracing. trylock (not blocking): the monitor poll must not stall, - * and a non-recursive blocking lock here could deadlock if ever reached while - * the caller already holds it. If busy, defer — backups stay for a later tick. */ + /* trylock, not blocking: the monitor poll must not stall; if busy, defer to a later tick. */ if (pthread_mutex_trylock(&g_cheat_apply_lock) != 0) { cr_log("info", "patches", - "game_stop: ptrace busy, deferring restore of %d patch(es) pid=%d", + "game_stop: apply busy, deferring restore of %d patch(es) pid=%d", count, (int)pid); return; } - int _at = pt_attach_timed(pid, 2000); - if (_at < 0) { - cr_log("warn", "patches", - "game_stop: pt_attach failed for pid=%d — %d patch(es) not restored", - (int)pid, count); - patch_clear_backups_for_pid(pid); - pthread_mutex_unlock(&g_cheat_apply_lock); - return; - } - /* Snapshot names + entry_ids to iterate while lock is released per write */ char entry_ids[PATCH_BACKUP_STORE_MAX][PATCH_ENTRY_ID_LEN]; int snap_n = 0; @@ -333,7 +317,6 @@ patch_restore_all_for_pid(pid_t pid, const char *title_id) { snprintf(entry_ids[snap_n++], PATCH_ENTRY_ID_LEN, "%s", g_patch_backups[i].entry_id); pthread_mutex_unlock(&g_patch_backup_lock); - pt_batch_begin(); /* elevate once across all restore writes */ for (int ei = 0; ei < snap_n; ei++) { pthread_mutex_lock(&g_patch_backup_lock); int slot = -1; @@ -361,8 +344,6 @@ patch_restore_all_for_pid(pid_t pid, const char *title_id) { free(rec); } - pt_batch_end(); - pt_detach(pid, 0); patch_clear_backups_for_pid(pid); pthread_mutex_unlock(&g_cheat_apply_lock); } @@ -795,6 +776,7 @@ parse_hex_bytes(const char *s, uint8_t *out, size_t *out_len, size_t max) { size_t hex_digits = strlen(s + 2); size_t nbytes = (hex_digits + 1) / 2; if (nbytes == 0) nbytes = 1; + if (nbytes > 8) nbytes = 8; /* v only holds 64 bits; shifting by >=64 is UB */ if (nbytes > max) return -1; for (size_t i = 0; i < nbytes; i++) out[i] = (uint8_t)(v >> (8 * i)); @@ -1098,8 +1080,13 @@ scan_for_pattern(pid_t pid, intptr_t scan_start, size_t scan_size, if (scanned + read_sz > scan_size) read_sz = scan_size - scanned; intptr_t chunk_addr = scan_start + (intptr_t)scanned; - int rrc = pt_copyout(pid, chunk_addr, buf, read_sz); - if (rrc < 0) { scanned += want; continue; } + /* Stop at the first unreadable chunk instead of skipping past it — that's + * the edge of the mapped module, not a gap to scan through. Continuing + * blindly toward the MASK_SCAN_LIMIT ceiling risks hitting unmapped/MMIO + * memory, which has caused a kernel panic. */ + if (!ADDR_IN_USER_RANGE(chunk_addr)) break; + int rrc = mdbg_io_copyout(pid, chunk_addr, buf, read_sz); + if (rrc < 0) break; size_t search_len = (read_sz >= pat_len) ? (read_sz - pat_len + 1) : 0; for (size_t i = 0; i < search_len; i++) { @@ -1177,27 +1164,21 @@ patch_apply_entry_ex(const char *title_id, const patch_entry_t *entry, return -1; } - /* Serialize against cheat-applies and dashboard ptrace reads on the SAME global - * gate the cheat path uses. Two threads attaching the same pid is the classic - * failure where the second pt_attach fails and its cleanup PT_DETACHes the first - * thread's session mid-write — observed as mid-patch mprotect/copyout failures. - * g_cheat_applying makes the dashboard poll back off while we hold the game. */ + /* Same global gate as the cheat path, plus g_cheat_applying so the dashboard poll backs off. */ pthread_mutex_lock(&g_cheat_apply_lock); g_cheat_applying = 1; - int _at = pt_attach_timed(pid, 5000); - if (_at < 0) { + if (kill(pid, 0) != 0 && errno == ESRCH) { g_cheat_applying = 0; pthread_mutex_unlock(&g_cheat_apply_lock); if (result) { snprintf(result->error, 64, "attach_failed"); - snprintf(result->message, 256, "pt_attach failed"); } + snprintf(result->message, 256, "game has exited"); } return -1; } /* Heap-allocate — 64 × 280 bytes = 17.5 KB would overflow the thread stack. */ cr_patch_backup_t *backups = calloc(PATCH_MAX_LINES, sizeof(*backups)); if (!backups) { - pt_detach(pid, 0); g_cheat_applying = 0; pthread_mutex_unlock(&g_cheat_apply_lock); if (result) { snprintf(result->error, 64, "oom"); @@ -1211,12 +1192,6 @@ patch_apply_entry_ex(const char *title_id, const patch_entry_t *entry, char err_msg[256] = {0}; char err_code[64] = {0}; - /* Elevate ucred once for the whole write+verify loop. A large patch (100+ - * lines) does thousands of ptrace ops; the per-call credential swap would - * otherwise dominate and exceed the request timeout. Safe because we hold the - * global ptrace gate, so no other thread does ptrace while this batch is open. */ - pt_batch_begin(); - for (int i = 0; i < entry->line_count; i++) { const patch_line_t *ln = &entry->lines[i]; intptr_t write_addr = 0; @@ -1241,7 +1216,7 @@ patch_apply_entry_ex(const char *title_id, const patch_entry_t *entry, char val_hex[PATCH_LINE_MAX_BYTES * 3 + 4] = {0}; uint8_t pre[PATCH_MASK_MAX_BYTES]; size_t rd = (ln->pattern_len <= PATCH_MASK_MAX_BYTES) ? ln->pattern_len : PATCH_MASK_MAX_BYTES; - if (pt_copyout(pid, write_addr, pre, rd) >= 0) { + if (mdbg_io_copyout(pid, write_addr, pre, rd) >= 0) { size_t pos = 0; for (size_t b = 0; b < rd && pos + 4 < sizeof(pre_hex); b++) pos += (size_t)snprintf(pre_hex + pos, sizeof(pre_hex) - pos, "%02x ", pre[b]); @@ -1271,7 +1246,7 @@ patch_apply_entry_ex(const char *title_id, const patch_entry_t *entry, /* Save backup before writing */ if (ln->value_len <= PATCH_LINE_MAX_BYTES) { - int rrc = pt_copyout(pid, write_addr, backups[backup_count].old_bytes, ln->value_len); + int rrc = mdbg_io_copyout(pid, write_addr, backups[backup_count].old_bytes, ln->value_len); if (rrc < 0) { snprintf(err_code, sizeof(err_code), "backup_read_failed"); snprintf(err_msg, sizeof(err_msg), @@ -1300,7 +1275,7 @@ patch_apply_entry_ex(const char *title_id, const patch_entry_t *entry, } if (ln->value_len <= PATCH_LINE_MAX_BYTES) { uint8_t vbuf[PATCH_LINE_MAX_BYTES]; - if (pt_copyout(pid, write_addr, vbuf, ln->value_len) >= 0 && + if (mdbg_io_copyout(pid, write_addr, vbuf, ln->value_len) >= 0 && memcmp(vbuf, ln->value, ln->value_len) != 0) { cr_log("warn", "patches", "mask_verify_mismatch addr=0x%lx len=%zu", (long)write_addr, ln->value_len); @@ -1326,7 +1301,7 @@ patch_apply_entry_ex(const char *title_id, const patch_entry_t *entry, /* Readback verify (write_process_memory_forced has no internal verify) */ if (ln->value_len <= PATCH_LINE_MAX_BYTES) { uint8_t vbuf[PATCH_LINE_MAX_BYTES]; - if (pt_copyout(pid, write_addr, vbuf, ln->value_len) >= 0 && + if (mdbg_io_copyout(pid, write_addr, vbuf, ln->value_len) >= 0 && memcmp(vbuf, ln->value, ln->value_len) != 0) { char exp_h[52] = {0}, got_h[52] = {0}; size_t hn = ln->value_len < 16 ? ln->value_len : 16; @@ -1358,8 +1333,6 @@ patch_apply_entry_ex(const char *title_id, const patch_entry_t *entry, title_id, entry->name, backup_count, rollback_errors); } - pt_batch_end(); - pt_detach(pid, 0); g_cheat_applying = 0; pthread_mutex_unlock(&g_cheat_apply_lock); diff --git a/src/cr_patch_parser.h b/src/cr_patch_parser.h index 095a26f..48c4161 100644 --- a/src/cr_patch_parser.h +++ b/src/cr_patch_parser.h @@ -85,15 +85,10 @@ typedef struct { * Condition: type==MASK && match_offset>=0 && match_offset+value_len < pattern_len */ int cr_patch_line_has_trailing_pattern_bytes(const patch_line_t *ln); -/* Compute a stable 8-hex-char entryId from source metadata; writes into entry->entry_id. - * Must be called after source_path, source_kind, metadata_index, name, app_ver, line_count - * are all populated. */ +/* Computes a stable 8-hex-char entryId from source metadata; call after those fields are populated. */ void cr_patch_compute_entry_id(patch_entry_t *entry); -/* Find all XML files for a given title ID across all patch directories. - * Fills out_paths[][384] and out_kinds[] with up to max results in deterministic order: - * 1. xml_prospero, 2. xml, 3. external — alphabetical within each dir. - * Returns count found (0 = none). */ +/* Finds all XML files for a title ID; order is xml_prospero, xml, external, alphabetical within each. */ int patch_find_xmls_for_title(const char *title_id, char out_paths[][384], cr_patch_source_kind_t out_kinds[], int max); @@ -101,15 +96,11 @@ int patch_find_xmls_for_title(const char *title_id, /* Compatibility shim: returns first matching XML path only. */ int patch_find_xml_for_title(const char *title_id, char *out_path, size_t out_sz); -/* Parse the XML at xml_path for entries referencing title_id. - * source_kind and source_path are stamped onto every returned entry. - * Returns number of entries found, -1 on hard error (file missing, parse failure). */ +/* Parses xml_path for entries referencing title_id; returns count found, -1 on hard error. */ int patch_parse_xml_file(const char *xml_path, const char *title_id, cr_patch_source_kind_t source_kind, patch_doc_t *doc); -/* Apply all lines in one patch entry with backup/rollback (all-or-nothing). - * Returns 0 on full success, non-zero on failure. - * result may be NULL if the caller does not need structured output. */ +/* Applies all lines in one entry with backup/rollback (all-or-nothing); result may be NULL. */ int patch_apply_entry_ex(const char *title_id, const patch_entry_t *entry, patch_apply_result_t *result); @@ -120,11 +111,7 @@ int patch_is_applied(const char *title_id, const char *entry_id, pid_t pid); void patch_clear_for_pid(pid_t pid); /* called by game monitor on game exit */ void patch_clear_all_applied(void); /* called on rescan */ -/* Backup store — original bytes captured at apply time, used to restore. - * patch_restore_entry: restores one entry's bytes to the live process. - * patch_restore_all_for_pid: called by game monitor on game stop; attempts - * restore if process still alive, then clears all backup records for pid. - * patch_clear_backups_for_pid: unconditional state-only clear. */ +/* Backup store of original bytes captured at apply time, used to restore on game stop. */ int patch_restore_entry(const char *title_id, const char *entry_id, pid_t pid, patch_apply_result_t *result); void patch_restore_all_for_pid(pid_t pid, const char *title_id); @@ -138,9 +125,7 @@ void patch_index_invalidate(void); int patch_global_enabled(void); void patch_global_set(int on); -/* XML patch file management — list/toggle/delete patch XML files. - * dir_idx: 0=xml_prospero, 1=xml, 2=external. - * Toggle renames .xml <-> .xml.off; the scanner already ignores .xml.off files. */ +/* List/toggle/delete patch XML files; dir_idx: 0=xml_prospero, 1=xml, 2=external. Toggle renames .xml <-> .xml.off. */ char *patch_files_list_json(void); int patch_file_toggle(const char *name, int on, int dir_idx); int patch_file_delete(const char *name, int dir_idx); diff --git a/src/cr_profile.c b/src/cr_profile.c index 33895a4..6d49dc4 100644 --- a/src/cr_profile.c +++ b/src/cr_profile.c @@ -29,10 +29,10 @@ static const int AVATAR_SIZES[] = {64, 128, 260, 440}; static void send_obj(int fd, int status, cJSON *root) { - if (!root) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); return; } + if (!root) { http_send_oom(fd); return; } char *txt = cJSON_PrintUnformatted(root); cJSON_Delete(root); - if (!txt) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}"); return; } + if (!txt) { http_send_oom(fd); return; } http_send_json(fd, status, txt); free(txt); } @@ -409,6 +409,12 @@ avatar_build_request(int fd, const char *query, const char *body, size_t body_le send_err(fd, 400, err); return; } + /* Reject absurd source dimensions before the w*h*4 resize allocations. */ + if (w > 4096 || h > 4096) { + stbi_image_free(rgba); + send_err(fd, 400, "image too large (max 4096x4096)"); + return; + } char err[256] = {0}; int rc = build_all_sizes(rgba, w, h, mode[0] ? mode : "crop", err, sizeof(err)); diff --git a/src/cr_profile.h b/src/cr_profile.h index 8f9e978..33985d9 100644 --- a/src/cr_profile.h +++ b/src/cr_profile.h @@ -3,20 +3,7 @@ #include -/* PS5 user profile — read/change the foreground user's name and avatar. - * - * Name change goes through sceUserServiceSetUserName (no external payload). - * Avatar change decodes an uploaded image, builds the four DXT5 .dds sizes - * the firmware expects, and writes them (plus online.json) into the user's - * profile cache at /system_data/priv/cache/profile/0x/. - * - * Routes (dispatcher returns 1 if handled): - * GET /api/profile - whoami (foreground user id + name) - * POST /api/profile/name?name=... - set the foreground user's name - * POST /api/profile/avatar?mode=... - body = raw image; build preview sizes - * POST /api/profile/avatar/apply - apply built sizes to the user - * GET /api/profile/avatar/preview?size=N - serve a built PNG preview - */ +/* Read/change foreground user's name (sceUserServiceSetUserName) and avatar (builds 4 DXT5 .dds sizes into the profile cache). */ int cr_api_profile_handle(int fd, const char *method, const char *path, const char *query, const char *body, size_t body_len); diff --git a/src/cr_shutdown.c b/src/cr_shutdown.c index 07a76f1..6c2668b 100644 --- a/src/cr_shutdown.c +++ b/src/cr_shutdown.c @@ -12,6 +12,8 @@ volatile int g_shutdown_requested = 0; extern volatile int g_game_monitor_running; extern int g_http_listen_fd; +/* Held by cr_cheats.c during a cheat apply/disable's writes. */ +extern pthread_mutex_t g_cheat_apply_lock; static void close_fd_safe(int *fd) { @@ -28,6 +30,14 @@ cheatrunner_close_servers(void) { close_fd_safe(&g_http_listen_fd); } +/* Wait for any in-flight cheat apply to finish, then kill. */ +static void +kill_after_no_apply_in_flight(void) { + pthread_mutex_lock(&g_cheat_apply_lock); + kill(getpid(), SIGKILL); + _exit(0); +} + static void * delayed_shutdown_thread(void *arg) { int delay_ms = arg ? *(int *)arg : 700; @@ -44,12 +54,10 @@ delayed_shutdown_thread(void *arg) { activity_save(); notifications_save(); cr_log("info", "dev", "CheatRunner exiting for reload"); - kill(getpid(), SIGKILL); - /* Guaranteed exit: if self-SIGKILL is somehow refused/deferred (PS5 credential - * quirk), fall through to _exit so the process always dies — otherwise the - * thread would just return and CheatRunner would keep running ("shutdown - * didn't work"). */ - _exit(0); + /* Guaranteed exit: kill_after_no_apply_in_flight always reaches SIGKILL/_exit, + * so the thread never just returns and leaves CheatRunner running + * ("shutdown didn't work"). */ + kill_after_no_apply_in_flight(); return NULL; } @@ -60,8 +68,7 @@ cheatrunner_request_shutdown(int delay_ms) { g_shutdown_requested = 1; g_game_monitor_running = 0; cheatrunner_close_servers(); - kill(getpid(), SIGKILL); - _exit(0); + kill_after_no_apply_in_flight(); return; } *p = delay_ms; @@ -74,6 +81,5 @@ cheatrunner_request_shutdown(int delay_ms) { g_shutdown_requested = 1; g_game_monitor_running = 0; cheatrunner_close_servers(); - kill(getpid(), SIGKILL); - _exit(0); + kill_after_no_apply_in_flight(); } diff --git a/src/cr_store_lookup.h b/src/cr_store_lookup.h index 2cfbf71..051deff 100644 --- a/src/cr_store_lookup.h +++ b/src/cr_store_lookup.h @@ -3,10 +3,7 @@ #include -/* Fetch game name from PlayStation Store product page using contentId. - * content_id: 36-char PlayStation content ID (e.g. UP9000-CUSA00001_00-...). - * Returns 1 and fills name_out on success, 0 on failure (network error, not found, parse error). - * timeout_ms: per-request timeout (0 = library default). */ +/* Fetches game name from the PS Store product page for content_id; returns 1 on success, 0 on failure. */ int cr_store_lookup(const char *content_id, char *name_out, size_t name_size, int timeout_ms); #endif /* CR_STORE_LOOKUP_H */ diff --git a/src/cr_tile_pkg.c b/src/cr_tile_pkg.c index 2d429c6..fea4132 100644 --- a/src/cr_tile_pkg.c +++ b/src/cr_tile_pkg.c @@ -131,13 +131,12 @@ tile_autoinstall_thread(void *arg) { notify("CheatRunner: installing PKG..."); cr_log("info", "tile_pkg", "tile not yet installed — starting install"); - /* Short pause: give SceShellCore's installer queue a moment to be ready. - * 3 s is enough when CheatRunner is loaded from a running loader (elf-arsenal, - * goldhen, etc.); the full 30 s elf-arsenal uses is only needed on a cold - * first-boot before any other payload has warmed up the system. */ + /* 3s lets SceShellCore's installer queue settle; only a cold first-boot needs elf-arsenal's full 30s. */ sleep(3); - jb_escalate_pid(getpid()); + if (jb_escalate_pid(getpid()) != 0) { + cr_log("warn", "tile_pkg", "jb_escalate_pid failed; PKG install may fail"); + } /* Write the embedded PKG bytes to disk. */ cr_log("info", "tile_pkg", "writing %s (%zu bytes)", diff --git a/src/cr_tile_pkg.h b/src/cr_tile_pkg.h index e745e00..cf52992 100644 --- a/src/cr_tile_pkg.h +++ b/src/cr_tile_pkg.h @@ -1,10 +1,7 @@ #ifndef CR_TILE_PKG_H #define CR_TILE_PKG_H -/* Spawn the background thread that auto-installs the CheatRunner home-screen - * tile PKG on first boot. Safe to call unconditionally — is a no-op when - * the PKG was not embedded at build time or when the tile is already - * installed. */ +/* Spawns the home-screen tile auto-install thread; safe to call unconditionally (no-op if not embedded or already installed). */ void cr_tile_autoinstall_init(void); #endif /* CR_TILE_PKG_H */ diff --git a/src/cr_titles.c b/src/cr_titles.c index 1f14a80..a2b3fd4 100644 --- a/src/cr_titles.c +++ b/src/cr_titles.c @@ -370,10 +370,7 @@ read_param_value_from_dir(const char *dir, const char *key, char *out, size_t ou return -1; } -/* Scan appmeta directories for a content entry matching title_id. - * Content IDs embed the title ID: e.g. UP9000-CUSA42556_00-HASH. - * From the content ID we derive the sandbox mount: /mnt/sandbox/{contentId}_000/. - * Returns 0 and fills *out on success, -1 on failure. */ +/* Finds the content ID matching title_id (e.g. UP9000-CUSA42556_00-HASH) to derive the sandbox mount path. */ static int appmeta_scan_for_title(const char *title_id, const char *key, char *out, size_t out_size) { static const char *const bases[] = { diff --git a/src/dashboard_css.inc b/src/dashboard_css.inc index 75b7255..2d6a0cf 100644 --- a/src/dashboard_css.inc +++ b/src/dashboard_css.inc @@ -40,9 +40,9 @@ " --cr-radius: 14px;\n" " --cr-radius-sm: 8px;\n" " --cr-radius-lg: 22px;\n" -" --cr-font-base: Inter, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n" -" --cr-font-display: \"Space Grotesk\", Inter, system-ui, sans-serif;\n" -" --cr-font-mono: \"JetBrains Mono\", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n" +" --cr-font-base: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n" +" --cr-font-display: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n" +" --cr-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Courier New\", monospace;\n" " }\n" "\n" " * { box-sizing: border-box; }\n" @@ -248,6 +248,34 @@ " .cr-x-button:active { transform: scale(.96); }\n" " .cr-x-icon { width: 16px; height: 16px; flex: 0 0 auto; color: #fff; }\n" " .cr-x-label { color: #fff; }\n" +" .cr-bsky-button {\n" +" display: inline-flex;\n" +" align-items: center;\n" +" justify-content: center;\n" +" gap: 8px;\n" +" min-height: 36px;\n" +" padding: 8px 14px;\n" +" border-radius: 9999px;\n" +" font-size: 13px;\n" +" font-weight: 800;\n" +" letter-spacing: .02em;\n" +" text-decoration: none;\n" +" color: #fff;\n" +" border: 1px solid rgba(0,133,254,.55);\n" +" background: rgba(0,72,135,.35);\n" +" box-shadow: 0 0 18px rgba(0,133,254,.18);\n" +" transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease, filter .18s ease;\n" +" white-space: nowrap;\n" +" }\n" +" .cr-bsky-button:hover {\n" +" transform: translateY(-1px);\n" +" border-color: rgba(0,133,254,.8);\n" +" box-shadow: 0 0 28px rgba(0,133,254,.38);\n" +" filter: brightness(1.1);\n" +" }\n" +" .cr-bsky-button:active { transform: scale(.96); }\n" +" .cr-bsky-icon { width: 16px; height: 16px; flex: 0 0 auto; color: #fff; }\n" +" .cr-bsky-label { color: #fff; }\n" " .cr-support-modal-overlay {\n" " position: fixed; inset: 0; z-index: 200;\n" " background: rgba(0,0,0,.72); backdrop-filter: blur(6px);\n" @@ -293,11 +321,12 @@ " gap: 18px; flex-wrap: wrap;\n" " }\n" " .cr-supporter-name {\n" -" font-family: var(--font-mono); font-size: 15px; font-weight: 700;\n" +" font-family: var(--cr-font-mono); font-size: 15px; font-weight: 700;\n" " animation: cr-rgb-cycle 3s linear infinite;\n" " }\n" " .cr-supporter-name:nth-child(2) { animation-delay: -1s; }\n" " .cr-supporter-name:nth-child(3) { animation-delay: -2s; }\n" +" .cr-supporter-name:nth-child(4) { animation-delay: -0.5s; }\n" " .cr-crop-overlay {\n" " position: fixed; inset: 0; z-index: 300;\n" " background: rgba(0,0,0,.8); backdrop-filter: blur(6px);\n" @@ -1110,7 +1139,8 @@ " .kofi-link,\n" " .cr-support-button,\n" " .cr-discord-button,\n" -" .cr-x-button { transition: none !important; animation: none !important; }\n" +" .cr-x-button,\n" +" .cr-bsky-button { transition: none !important; animation: none !important; }\n" " .tab:hover, .tab:active { transform: none !important; }\n" " }\n" "\n" @@ -1183,11 +1213,11 @@ " .muted { color: var(--cr-muted); font-size: 13px; }\n" "\n" " @keyframes crLineReveal { from { transform: scaleX(0); opacity:0; } to { transform: scaleX(1); opacity:1; } }\n" -" @keyframes crPanelDeploy { from { opacity:0; transform: translateY(18px); clip-path: inset(8px 0 0 0 round 14px); } to { opacity:1; transform: translateY(0); clip-path: inset(0px round 14px); } }\n" +" @keyframes crPanelDeploy { from { opacity:0; transform: translateY(18px); } to { opacity:1; transform: translateY(0); } }\n" " @keyframes crScanSweep { from { transform: translateX(-120%); } to { transform: translateX(120%); } }\n" " @keyframes crStatusTick { 0%,100% { opacity:1; } 50% { opacity:.35; } }\n" -" @keyframes crToastSnap { from { opacity:0; transform: translateY(-8px); clip-path: inset(0 0 100% 0); } to { opacity:1; transform: translateY(0); clip-path: inset(0 0 0% 0); } }\n" -" @keyframes crToastIn { from { opacity:0; transform: translateY(-6px) scale(.97); clip-path: inset(0 0 100% 0); } to { opacity:1; transform: translateY(0) scale(1); clip-path: inset(0 0 0% 0); } }\n" +" @keyframes crToastSnap { from { opacity:0; transform: translateY(-8px); } to { opacity:1; transform: translateY(0); } }\n" +" @keyframes crToastIn { from { opacity:0; transform: translateY(-6px) scale(.97); } to { opacity:1; transform: translateY(0) scale(1); } }\n" " @keyframes crCardPulse { 0%,100% { border-left-color: var(--cr-brand); box-shadow: 0 0 0 1px rgba(225,29,72,.14); } 50% { border-left-color: var(--cr-brand-2); box-shadow: 0 0 28px rgba(225,29,72,.30), 0 0 0 2px rgba(244,63,94,.18); } }\n" " @keyframes crVertScan { 0% { top: -4px; opacity: 1; } 100% { top: 100%; opacity: .4; } }\n" " @keyframes crModalScan { from { transform: translateY(-100%); opacity: .9; } to { transform: translateY(160%); opacity: 0; } }\n" @@ -1203,7 +1233,7 @@ "\n" " /* Controller / keyboard focus visibility */\n" " .btn:focus-visible, .cr-btn:focus-visible, .tab:focus-visible, .cr-gear-button:focus-visible,\n" -" .cr-support-button:focus-visible, .cr-discord-button:focus-visible, .cr-x-button:focus-visible,\n" +" .cr-support-button:focus-visible, .cr-discord-button:focus-visible, .cr-x-button:focus-visible, .cr-bsky-button:focus-visible,\n" " .cr-fav-btn:focus-visible, .input:focus-visible, .select:focus-visible, .cr-game-tile:focus-visible {\n" " outline: none;\n" " border-color: var(--cr-brand-line);\n" @@ -1226,8 +1256,8 @@ " @keyframes crShimmer { to { transform: translateX(100%); } }\n" "\n" " /* Live (running) pill pulse */\n" -" .cr-pill.cr-live::before { content: \"\"; width: 7px; height: 7px; border-radius: 50%; background: var(--cr-brand-2); animation: crLivePulse 1.8s infinite; }\n" -" @keyframes crLivePulse { 0% { box-shadow: 0 0 0 0 rgba(244,63,94,.5); } 70% { box-shadow: 0 0 0 6px rgba(244,63,94,0); } 100% { box-shadow: 0 0 0 0 rgba(244,63,94,0); } }\n" +" .cr-pill.cr-live::before { content: \"\"; width: 7px; height: 7px; border-radius: 50%; background: var(--cr-brand-2); animation: crLivePulse 1.8s ease-in-out infinite; will-change: opacity; }\n" +" @keyframes crLivePulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }\n" "\n" " /* Grid count header */\n" " .cr-grid-head { display: flex; align-items: baseline; gap: 7px; padding: 2px 4px 11px; }\n" @@ -1281,8 +1311,8 @@ " .topbar { padding: 12px; }\n" " }\n" " @media (max-width:640px) {\n" -" .cr-support-button, .cr-discord-button, .cr-x-button { padding: 0; width: 36px; min-height: 36px; }\n" -" .cr-support-label, .cr-discord-label, .cr-x-label { display: none; }\n" +" .cr-support-button, .cr-discord-button, .cr-x-button, .cr-bsky-button { padding: 0; width: 36px; min-height: 36px; }\n" +" .cr-support-label, .cr-discord-label, .cr-x-label, .cr-bsky-label { display: none; }\n" " }\n" "\n" " /* --- Mod search bar --- */\n" @@ -1484,14 +1514,38 @@ " }\n" "\n" " /* ---- Active cheats pulsing border on game tiles ---- */\n" -" @keyframes crCheatGlow {\n" -" 0%, 100% { box-shadow: 0 0 0 1px rgba(34,197,94,.18), 0 0 14px rgba(34,197,94,.10); border-left-color: var(--cr-green); }\n" -" 50% { box-shadow: 0 0 0 1px rgba(34,197,94,.45), 0 0 28px rgba(34,197,94,.28); border-left-color: #4ade80; }\n" -" }\n" +" @keyframes crCheatGlow { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }\n" " .cr-game-tile.has-active-cheats {\n" " border-left-color: var(--cr-green);\n" +" box-shadow: 0 0 0 1px rgba(34,197,94,.18), 0 0 14px rgba(34,197,94,.10);\n" +" }\n" +" .cr-game-tile.has-active-cheats::after {\n" +" content: \"\";\n" +" position: absolute;\n" +" inset: -1px;\n" +" border-radius: var(--cr-radius);\n" +" border: 1px solid rgba(34,197,94,.45);\n" +" box-shadow: 0 0 28px rgba(34,197,94,.28);\n" " animation: crCheatGlow 2.4s ease-in-out infinite;\n" +" pointer-events: none;\n" +" will-change: opacity;\n" " }\n" " @media (prefers-reduced-motion: reduce) {\n" " .cr-running-pill-inner, .cr-game-tile.has-active-cheats { transition: none !important; animation: none !important; }\n" " }\n" +" /* ---- PS5 WebKit overrides: strip GPU-expensive effects ---- */\n" +" .ps5 .cr-command-bg,\n" +" .ps5 .cr-scan-grid,\n" +" .ps5 .cr-circuit-lines { display: none; }\n" +" .ps5 .cr-support-modal-overlay,\n" +" .ps5 .cr-crop-overlay { backdrop-filter: none !important; background: rgba(0,0,0,.92) !important; }\n" +" .ps5 .cr-settings-overlay { backdrop-filter: none !important; background: rgba(0,0,0,.92) !important; }\n" +" .ps5 .cr-game-tile.has-active-cheats::after { animation: none; }\n" +" .ps5 .cr-pill.cr-live::before { animation: none; }\n" +" .ps5 .cr-game-tile.cr-tile-enter { animation-delay: 0ms; }\n" +" .ps5 .cr-game-tile { transition: transform 160ms cubic-bezier(.2,.8,.2,1) !important; background: var(--cr-panel) !important; }\n" +" .ps5 .cr-game-tile.is-launching { animation: none !important; }\n" +" .ps5 .cr-skel-tile::after { animation: none !important; }\n" +" .ps5 .cr-cheat-switch.is-pending .cr-cheat-switch-track::after,\n" +" .ps5 .cr-cheat-switch.is-applying .cr-cheat-switch-track::after { animation: none !important; }\n" +" .ps5 .cr-launch-overlay::before { animation: none !important; }\n" diff --git a/src/dashboard_html.inc b/src/dashboard_html.inc index 50b3b9c..76d4cbd 100644 --- a/src/dashboard_html.inc +++ b/src/dashboard_html.inc @@ -1,5 +1,5 @@ "\n" -"\n" +"\n" "\n" " \n" " \n" @@ -9,9 +9,6 @@ " \n" " \n" " \n" -" \n" -" \n" -" \n" " \n" "\n" "\n" @@ -29,12 +26,13 @@ " \n" " \n" "
\n" -" v0.13\n" +" v0.14\n" " No game\n" " Engine ?\n" " \n" " Discord\n" " X\n" +" Bluesky\n" " \n" "
\n" " \n" @@ -46,6 +44,7 @@ " \n" " \n" " \n" +" \n" " \n" " \n" " \n" @@ -309,6 +308,7 @@ " primo\n" " lal3x\n" " jawad\n" +" Maffioh\n" " \n" " \n" "
\n" diff --git a/src/dashboard_js.inc b/src/dashboard_js.inc index 3fe7325..f41089c 100644 --- a/src/dashboard_js.inc +++ b/src/dashboard_js.inc @@ -2,7 +2,17 @@ " const ICON_STAR_O = '';\n" " const ICON_PLAY = '';\n" " const ICON_CHEATS = '';\n" -" const DEFAULT_VERSION = '0.1';\n" +" const DEFAULT_VERSION = '0.14';\n" +" const isPS5 = /PlayStation/i.test(navigator.userAgent);\n" +" if (isPS5) document.documentElement.classList.add('ps5');\n" +" if (window.applicationCache) {\n" +" (function() {\n" +" var ac = applicationCache;\n" +" function acReload() { try { ac.swapCache(); } catch (e) {} window.location.reload(); }\n" +" if (ac.status === ac.UPDATEREADY) acReload();\n" +" else ac.addEventListener('updateready', acReload, false);\n" +" })();\n" +" }\n" " function normalizeVersion(v) {\n" " if (!v || v === 'unknown' || String(v).trim() === '') return DEFAULT_VERSION;\n" " v = String(v).trim();\n" @@ -400,6 +410,7 @@ " const app = isApp(g);\n" " if (platformFilter === 'FAV') { if (!isFav(gameId(g))) return false; }\n" " else if (platformFilter === 'RECENT') { if (recentList.indexOf(gameId(g)) < 0) return false; }\n" +" else if (platformFilter === 'CHEATS') { if (!g.hasCheat) return false; }\n" " else if (platformFilter === 'APP') { if (!app) return false; }\n" " else if (platformFilter === 'ALL') { if (app) return false; }\n" " else { if (app || gamePlatform(g) !== platformFilter) return false; }\n" @@ -459,7 +470,8 @@ " if (!gamesView.length) {\n" " const msg = platformFilter === 'FAV' ? 'No favorites yet \xe2\x80\x94 tap the star on a game to add it.'\n" " : (platformFilter === 'RECENT' ? 'No recent games yet \xe2\x80\x94 launch a game or open its cheats.'\n" -" : 'No games match current filters.');\n" +" : (platformFilter === 'CHEATS' ? 'No games with cheats found.'\n" +" : 'No games match current filters.'));\n" " node.innerHTML = '
' + msg + '
';\n" " return;\n" " }\n" @@ -469,7 +481,7 @@ " const active = current && gameId(current) === id;\n" " const isLaunching = launchingId === id;\n" " const cls = 'game cr-game-tile' + (active ? ' active' : '') + (g.running ? ' running' : '') + (activeCheatsSet.has(id) ? ' has-active-cheats' : '') + (isLaunching ? ' is-launching' : '') + (animate ? ' cr-tile-enter' : '');\n" -" const animStyle = animate ? ' style=\"--ti:' + Math.min(i, 14) + '\"' : '';\n" +" const animStyle = (animate && !isPS5) ? ' style=\"--ti:' + Math.min(i, 14) + '\"' : '';\n" " const runningBadge = g.running ? 'Running' : '';\n" " const dis = launchBusy ? ' disabled' : '';\n" " const launchOverlay = isLaunching ? '
LAUNCHING…
' : '';\n" @@ -507,7 +519,10 @@ " const g = gamesRaw.find(x => gameId(x) === id);\n" " if (!g) return;\n" " current = g;\n" -" renderGames();\n" +" // Just move .active instead of re-rendering the whole grid.\n" +" const prevActive = node.querySelector('.cr-game-tile.active');\n" +" if (prevActive && prevActive !== el) prevActive.classList.remove('active');\n" +" el.classList.add('active');\n" " });\n" " });\n" " node.querySelectorAll('[data-launch]').forEach(el => {\n" @@ -642,7 +657,7 @@ " const SETTINGS_DEFAULTS = {\n" " cheat_engine:1, cheat_validate_original_bytes:1, cheat_address_auto_detect:1,\n" " cheat_mc4_unverified_fallback:'relative', cheat_shn_unverified_fallback:'relative',\n" -" cheat_addr_cache_enabled:1, cheat_codecave_fallback:1, cheat_master_code_fixup:1,\n" +" cheat_addr_cache_enabled:1, cheat_master_code_fixup:1,\n" " allow_legacy_mc4_without_expected:1, allow_legacy_shn_without_expected:1,\n" " allow_unsafe_mc4_apply:0, allow_unsafe_shn_apply:0,\n" " cheat_min_stable_ms:8000, cheat_apply_cooldown_ms:500, cheat_inter_mod_delay_ms:0,\n" @@ -740,7 +755,6 @@ " num('cheat_apply_cooldown_ms','Cooldown between cheats (ms)', 0, 10000, {hint:'Minimum gap between two cheat toggles.'}),\n" " ])\n" " + group('Advanced (can crash games)', [\n" -" toggle('cheat_codecave_fallback','Auto-relocate code caves', {hint:'Allocate memory for cheats whose cave region is missing.', recommend:1}),\n" " toggle('cheat_master_code_fixup','Master-code address fixup', {hint:'Resolve master-code-dependent cheats at runtime.', recommend:1}),\n" " toggle('cheat_address_auto_detect','Auto address detection', {hint:'Probe memory to pick the correct address.', recommend:1}),\n" " toggle('allow_legacy_mc4_without_expected','Apply unverified MC4', {hint:'Allow .mc4 cheats that have no reference bytes.', risk:1}),\n" @@ -1888,6 +1902,15 @@ "\n" " document.getElementById('supportBtn').addEventListener('click', () => {\n" " document.getElementById('supportModal').style.display = 'flex';\n" +" /* The rgb-cycle animation is defined on .cr-supporter-name while the modal\n" +" * is display:none — some WebKit builds never start a CSS animation that was\n" +" * declared on a hidden element, leaving it permanently frozen on its first\n" +" * frame even after display flips to visible. Force a reflow to kick it off. */\n" +" document.querySelectorAll('.cr-supporter-name').forEach(function(el) {\n" +" el.style.animation = 'none';\n" +" void el.offsetHeight;\n" +" el.style.animation = '';\n" +" });\n" " });\n" " document.getElementById('supportModalCloseBtn').addEventListener('click', () => {\n" " document.getElementById('supportModal').style.display = 'none';\n" @@ -2773,8 +2796,37 @@ " }\n" " initPanelCollapse();\n" "\n" -" setInterval(refreshStatus, 7000);\n" -" setInterval(refreshLaunchStatus, 3000);\n" -" setInterval(refreshGlobalState, 10000);\n" -" setInterval(refreshLogs, 6000);\n" -" setInterval(refreshFan, 4000);\n" +" // PS5 browser has no JIT: continuous fetch+parse is the main lag source.\n" +" // Pollers pause when the tab is hidden, skip work when their panel is\n" +" // collapsed, and back off on repeated failures instead of hammering.\n" +" function crStartPoller(fn, baseMs, shouldRun) {\n" +" var fails = 0;\n" +" async function tick() {\n" +" var delay = baseMs;\n" +" if (document.hidden) {\n" +" delay = baseMs * 3;\n" +" } else if (shouldRun && !shouldRun()) {\n" +" delay = baseMs;\n" +" } else {\n" +" try { await fn(); fails = 0; }\n" +" catch (_) { fails++; }\n" +" if (fails) delay = Math.min(baseMs * (1 << Math.min(fails, 4)), 60000);\n" +" }\n" +" setTimeout(tick, delay);\n" +" }\n" +" setTimeout(tick, baseMs);\n" +" }\n" +" function crPanelOpen(id) {\n" +" var e = document.getElementById(id);\n" +" return !!e && !e.classList.contains('collapsed');\n" +" }\n" +" crStartPoller(refreshStatus, 7000);\n" +" crStartPoller(refreshLaunchStatus, 3000);\n" +" crStartPoller(refreshGlobalState, 10000);\n" +" crStartPoller(refreshLogs, 6000, function() { return crPanelOpen('pb_logs'); });\n" +" crStartPoller(refreshFan, 4000, function() { return crPanelOpen('pb_thermal'); });\n" +" // Returning to the foreground: refresh the always-on panels immediately\n" +" // instead of waiting out the (slowed) hidden interval.\n" +" document.addEventListener('visibilitychange', function() {\n" +" if (!document.hidden) { refreshStatus(); refreshGlobalState(); refreshLaunchStatus(); }\n" +" });\n" diff --git a/src/main.c b/src/main.c index b6c1195..9602b94 100644 --- a/src/main.c +++ b/src/main.c @@ -27,7 +27,7 @@ #include "cr_tile_pkg.h" #ifndef CHEATRUNNER_VERSION -#define CHEATRUNNER_VERSION "0.1" +#define CHEATRUNNER_VERSION "0.14" #endif /* Async-signal-safe crash handler: writes a one-line entry to the crash log @@ -68,10 +68,11 @@ main(void) { cr_log_klog_banner(); - puts(".------------------------------------."); - puts("| CheatRunner |"); - puts("| PS5 web launcher + cheats |"); - puts("'------------------------------------'"); + puts(" ____ _ _ ____ "); + puts(" / ___| |__ ___ __ _| |_| _ \\ _ _ _ __ _ __ ___ _ __ "); + puts(" | | | '_ \\ / _ \\/ _` | __| |_) | | | | '_ \\| '_ \\ / _ \\ '__|"); + puts(" | |___| | | | __/ (_| | |_| _ <| |_| | | | | | | | __/ | "); + puts(" \\____|_| |_|\\___|\\__,_|\\__|_| \\_\\\\__,_|_| |_|_| |_|\\___|_| "); log_msg("version: %s", CHEATRUNNER_VERSION); #ifdef __SCE__ @@ -94,19 +95,7 @@ main(void) { } #endif - /* Kill any previous CheatRunner instance before binding the HTTP port. - * - * On PS5, SYS_thr_set_name updates ki_comm (kinfo_proc offset 447) in - * addition to ki_tdname (offset 394) — both elfldr and elf-arsenal rely on - * this behaviour. We search offset 447 for "CheatRunner.elf" which is what - * we set via thr_set_name above. - * - * EPERM means the old instance is escalated and we are not — break fast so - * we surface the error instead of spinning then flooding bind() logs. - * ESRCH means the process vanished between find_pid and kill — treat as gone. - * - * Cap at 60 spins (~9s) to guard against looping forever on a zombie. - * Zombies hold no sockets, so the port will be free even if we time out. */ + /* Kill any previous instance before binding; EPERM (old instance escalated, we aren't) breaks fast, ESRCH treated as gone, capped at 60 spins so a zombie can't loop forever. */ { int kill_spins = 0; while ((old_pid = find_pid_by_name("CheatRunner.elf")) > 0 && kill_spins < 60) { diff --git a/src/pt.c b/src/pt.c index ea40141..637bbe4 100644 --- a/src/pt.c +++ b/src/pt.c @@ -32,16 +32,8 @@ along with this program; see the file COPYING. If not, see #include "pt.h" -/* sys_ptrace temporarily elevates THIS process's ucred (authid + caps) for the - * duration of one ptrace syscall, then restores it. Those fields are - * process-wide, so two threads doing this concurrently corrupt each other: - * thread B can save thread A's already-elevated caps as its "baseline" and then - * restore the process to a permanently-privileged state, while thread A's - * mid-flight restore drops privileges out from under B's syscall (intermittent - * EPERM ptrace failures). CheatRunner calls ptrace from the apply thread, the - * game-monitor thread (patch restore), and HTTP handler threads, so this must be - * serialized. A single static mutex around the save/elevate/syscall/restore is - * sufficient and cheap — ptrace operations are not on any hot path. */ +/* sys_ptrace elevates this process's ucred process-wide for one syscall, so + * concurrent callers stomp each other's privileges — serialize with a mutex. */ static pthread_mutex_t g_ptrace_ucred_lock = PTHREAD_MUTEX_INITIALIZER; /* Per-thread batch-elevation state (see pt_batch_begin). When depth>0 this thread @@ -145,6 +137,18 @@ pt_batch_end(void) { } +void +pt_ucred_lock(void) { + pthread_mutex_lock(&g_ptrace_ucred_lock); +} + + +void +pt_ucred_unlock(void) { + pthread_mutex_unlock(&g_ptrace_ucred_lock); +} + + intptr_t pt_resolve(pid_t pid, const char* nid) { intptr_t addr; diff --git a/src/pt.h b/src/pt.h index 9ca5d44..7bba22b 100644 --- a/src/pt.h +++ b/src/pt.h @@ -39,18 +39,16 @@ int pt_setregs(pid_t pid, const struct reg *r); int pt_copyin(pid_t pid, const void* buf, intptr_t addr, size_t len); int pt_copyout(pid_t pid, intptr_t addr, void* buf, size_t len); -/* Batch elevation: each ptrace syscall normally raises this process's ucred and - * restores it (≈4 kernel credential ops per call). For bulk operations (e.g. a - * 100+ line patch) that overhead dominates and can exceed the request timeout. - * pt_batch_begin() raises the credentials ONCE and holds the ucred lock; every - * pt_* call on the same thread until pt_batch_end() then skips the per-call swap. - * The CALLER MUST serialize ptrace (hold the global ptrace gate) so no other - * thread does ptrace while a batch is open. begin/end nest via an internal depth - * counter and MUST be balanced. Returns 0 on success, -1 if elevation failed - * (calls still work, just at per-call cost). */ +/* Raises ucred once for a run of pt_* calls instead of per-call (cheap for bulk + * patches). Caller must hold the ptrace gate; begin/end must balance. */ int pt_batch_begin(void); void pt_batch_end(void); +/* mdbg_copyin/copyout have the same ucred race as ptrace — cr_mdbg.c holds this + * lock around them. Don't call while pt_batch_begin() is open (same mutex, deadlocks). */ +void pt_ucred_lock(void); +void pt_ucred_unlock(void); + int pt_setchar(pid_t pid, intptr_t addr, char val); int pt_setshort(pid_t pid, intptr_t addr, short val); int pt_setint(pid_t pid, intptr_t addr, int val);