v0.14 update

This commit is contained in:
maj0r authored and GitHub committed 2026-06-18 17:19:03 -03:00
1 parent 536679544a
commit bddaf7f61d
43 files changed
+731 -885

No files matched your search

+1 -5
View File
@@ -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);
+89 -120
View File
@@ -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=<full-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;
+1 -2
View File
@@ -4,7 +4,6 @@
#include <stddef.h>
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
+5 -14
View File
@@ -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");
+24 -2
View File
@@ -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\","
+1 -1
View File
@@ -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;
}
+5 -6
View File
@@ -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 <stddef.h>
#include <stdint.h>
/* ---- 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);
+1 -1
View File
@@ -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);
+6 -15
View File
@@ -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);
}
+1 -6
View File
@@ -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
+3
View File
@@ -4,6 +4,9 @@
#include <stddef.h>
#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);
+1 -6
View File
@@ -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 */
+1 -4
View File
@@ -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;
+85 -198
View File
@@ -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/<fmt_name>.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 <fmt_name>.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 <expected> 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 <expected> bytes is
* still unverified and is exactly the case we need to diagnose. */
/* Also fires for explicit relative/absolute modes with no <expected> 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",
+1 -5
View File
@@ -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;
+9 -23
View File
@@ -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();
}
-3
View File
@@ -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) */
+13 -9
View File
@@ -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;
+2 -2
View File
@@ -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);
}
+2 -9
View File
@@ -3,16 +3,9 @@
#include <stddef.h>
/* 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.
+6 -31
View File
@@ -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);
+39 -34
View File
@@ -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);
+2 -11
View File
@@ -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);
+137
View File
@@ -0,0 +1,137 @@
#include <stdint.h>
#include <sys/types.h>
#include <ps5/kernel.h>
#include <ps5/mdbg.h>
#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;
}
+15
View File
@@ -0,0 +1,15 @@
#ifndef CR_MDBG_H
#define CR_MDBG_H
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
/* 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
+24 -182
View File
@@ -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) {
+5 -8
View File
@@ -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);
+24 -4
View File
@@ -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
+23 -50
View File
@@ -12,6 +12,7 @@
#include <unistd.h>
#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);
+6 -21
View File
@@ -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);
+8 -2
View File
@@ -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));
+1 -14
View File
@@ -3,20 +3,7 @@
#include <stddef.h>
/* 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<uid>/.
*
* 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);
+16 -10
View File
@@ -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();
}
+1 -4
View File
@@ -3,10 +3,7 @@
#include <stddef.h>
/* 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 */
+4 -5
View File
@@ -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)",
+1 -4
View File
@@ -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 */
+1 -4
View File
@@ -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[] = {
+71 -17
View File
@@ -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"
+5 -5
View File
@@ -1,5 +1,5 @@
"<!doctype html>\n"
"<html lang=\"en\">\n"
"<html lang=\"en\" manifest=\"/cache.appcache\">\n"
"<head>\n"
" <meta charset=\"utf-8\">\n"
" <meta name=\"viewport\" content=\"width=device-width,initial-scale=1,viewport-fit=cover\">\n"
@@ -9,9 +9,6 @@
" <link rel=\"apple-touch-icon\" href=\"/CheatRunner.png\">\n"
" <link rel=\"manifest\" href=\"/manifest.json\">\n"
" <meta name=\"theme-color\" content=\"#E11D48\">\n"
" <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n"
" <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n"
" <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300..700&family=JetBrains+Mono:wght@400..700&family=Space+Grotesk:wght@300..700&display=swap\" rel=\"stylesheet\">\n"
" <link rel=\"stylesheet\" href=\"/dashboard.css\">\n"
"</head>\n"
"<body>\n"
@@ -29,12 +26,13 @@
" </div>\n"
" </div>\n"
" <div class=\"topbar-right\">\n"
" <span class=\"cr-pill\" id=\"headVersion\">v0.13</span>\n"
" <span class=\"cr-pill\" id=\"headVersion\">v0.14</span>\n"
" <span class=\"cr-pill\" id=\"headRunning\">No game</span>\n"
" <span class=\"cr-pill\" id=\"headEngine\">Engine ?</span>\n"
" <button class=\"cr-gear-button\" id=\"settingsBtn\" title=\"Settings\" aria-label=\"Open settings\"><svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65c-.03-.24-.24-.42-.49-.42h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7z\"/></svg><span class=\"cr-gear-label\">Settings</span></button>\n"
" <a class=\"cr-discord-button\" href=\"https://discord.gg/E4g6fEqp46\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Join the Discord\" aria-label=\"Join the Discord server\"><svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057c.002.022.015.043.03.056a19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z\"/></svg><span class=\"cr-discord-label\">Discord</span></a>\n"
" <a class=\"cr-x-button\" href=\"https://x.com/callmemaj0r\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Follow maj0r on X\" aria-label=\"Follow maj0r on X\"><svg class=\"cr-x-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M18.244 2H21.5l-7.11 8.13L22.75 22h-6.55l-5.13-6.7L5.2 22H1.94l7.6-8.69L1.5 2h6.72l4.64 6.14L18.244 2Zm-1.14 17.91h1.8L7.24 3.99H5.31l11.793 15.92Z\"/></svg><span class=\"cr-x-label\">X</span></a>\n"
" <a class=\"cr-bsky-button\" href=\"https://bsky.app/profile/notmaj0r.bsky.social\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Follow maj0r on Bluesky\" aria-label=\"Follow maj0r on Bluesky\"><svg class=\"cr-bsky-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.06-.138.022-.276.04-.415.06-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.06c.14.02.279.04.415.06 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.203-.659-.299-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z\"/></svg><span class=\"cr-bsky-label\">Bluesky</span></a>\n"
" <button class=\"cr-support-button\" id=\"supportBtn\" title=\"Support this project\" aria-label=\"Support this project\"><svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z\"/></svg><span class=\"cr-support-label\">Support this project</span></button>\n"
" </div>\n"
" </header>\n"
@@ -46,6 +44,7 @@
" <button class=\"tab active\" data-filter=\"ALL\">All</button>\n"
" <button class=\"tab\" data-filter=\"FAV\"><svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M12 17.27l5.18 3.13-1.37-5.9 4.58-3.97-6.04-.52L12 4.5 9.65 10.01l-6.04.52 4.58 3.97-1.37 5.9z\"/></svg>Favorites</button>\n"
" <button class=\"tab\" data-filter=\"RECENT\">Recent</button>\n"
" <button class=\"tab\" data-filter=\"CHEATS\">Cheats</button>\n"
" <button class=\"tab\" data-filter=\"PS5\">PS5</button>\n"
" <button class=\"tab\" data-filter=\"PS4\">PS4</button>\n"
" <button class=\"tab\" data-filter=\"APP\">Apps</button>\n"
@@ -309,6 +308,7 @@
" <span class=\"cr-supporter-name\">primo</span>\n"
" <span class=\"cr-supporter-name\">lal3x</span>\n"
" <span class=\"cr-supporter-name\">jawad</span>\n"
" <span class=\"cr-supporter-name\">Maffioh</span>\n"
" </div>\n"
" </div>\n"
" <div class=\"modal-close-row\"><button class=\"btn secondary\" id=\"supportModalCloseBtn\">Close</button></div>\n"
+63 -11
View File
@@ -2,7 +2,17 @@
" const ICON_STAR_O = '<svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M12 17.27l5.18 3.13-1.37-5.9 4.58-3.97-6.04-.52L12 4.5 9.65 10.01l-6.04.52 4.58 3.97-1.37 5.9z\"/></svg>';\n"
" const ICON_PLAY = '<svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M8 5v14l11-7z\"/></svg>';\n"
" const ICON_CHEATS = '<svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"7\"/><path d=\"M12 2v3M12 19v3M2 12h3M19 12h3\"/></svg>';\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 = '<div class=\"status-item cr-empty-state\" style=\"grid-column:1/-1\">' + msg + '</div>';\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 ? '<span class=\"badge blue\"><span class=\"running-dot\"></span>Running</span>' : '';\n"
" const dis = launchBusy ? ' disabled' : '';\n"
" const launchOverlay = isLaunching ? '<div class=\"cr-launch-overlay\">LAUNCHING&hellip;</div>' : '';\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"
+7 -18
View File
@@ -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) {
+14 -10
View File
@@ -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;
+7 -9
View File
@@ -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);