mirror of
https://github.com/notmaj0r/CheatRunner.git
synced 2026-10-18 23:07:10 -05:00
v0.14 update
This commit is contained in:
1 parent
536679544a
commit
bddaf7f61d
43 files changed
+731
-885
No files matched your search
+1
-5
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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 */
|
||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
@@ -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
@@ -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…</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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user