mirror of
https://github.com/phantomptr/ps5upload.git
synced 2026-10-19 02:20:02 -05:00
cheats: write game memory through its physical pages, all-or-nothing
Every Marvel's Wolverine cheat crashed the console; CheatRunner's worked. Our engine PT_ATTACHed the game, set the protection byte of the vm_map entry holding the address to RWX with kernel_mprotect, wrote with PT_IO, and "restored" it to R-X. Measured on the Pro (FW 13.60): the game's .text is one 95 MiB entry (0x400000-0x6324000), execute-only (0x04), so each toggle made the whole executable RWX under a copy-on-write PT_IO write and left it R-X. kernel_mprotect edits the entry byte directly (no pmap update, no split); CheatRunner traced two kernel panics to that call and moved to the page-table walk this commit uses. - Read/write through kernel_proc_getpaddr + the direct map (mdbg as a fallback), inside kernel_rw_lock; read back after every write. No ptrace stop, no protection change. - Apply a cheat as a whole: every location is read and checked against its cheat and original bytes before anything is written; on a write failure the earlier writes are undone. The old engine wrote whichever locations matched, so a jump could land without its code cave. - Write code caves before the jumps into them when turning on, and the reverse when turning off (cheats_plan.c, host-tested on the real Wolverine pack). - Refuse a cheat the parser had to truncate, and a base of 0. - Toggle state is keyed by the flat index across all of a title's files; a toggle in the second file used to mark the first file's mods, which the watcher then re-applied at the next launch. Verified on the Pro with Wolverine running: God Mode, Max Resources/XP (20 locations), Infinite Item Charges and an MC4 Infinite Health on and off, 60 s each, game and console stayed up; a clashing MC4 cheat was refused with nothing written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
This commit is contained in:
1 parent
a90ea16b4b
commit
02469c725b
6 files changed
+620
-277
No files matched your search
@@ -972,6 +972,12 @@ test-payload: payload
|
||||
$(PAYLOAD_DIR)/tests/cheats_list_selftest.c
|
||||
@/tmp/ps5upload-cheats-list-selftest
|
||||
@echo "✓ a large or badly encoded cheat pack lists as valid, bounded JSON"
|
||||
@echo "Running cheat apply-plan self-test (host build)..."
|
||||
@cc -O2 -Wall -Wextra -Werror -I$(PAYLOAD_DIR)/include \
|
||||
-o /tmp/ps5upload-cheats-plan-selftest \
|
||||
$(PAYLOAD_DIR)/tests/cheats_plan_selftest.c
|
||||
@/tmp/ps5upload-cheats-plan-selftest
|
||||
@echo "✓ a cheat's code caves are written before the jumps into them, and removed after"
|
||||
@echo "Checking per-console isolation..."
|
||||
@./scripts/check-per-console-isolation.sh
|
||||
@echo "✓ one console's data cannot be shown under another's name"
|
||||
|
||||
+1
-1
@@ -190,7 +190,7 @@ SRCS := src/main.c src/runtime.c src/shell_builtin.c src/takeover.c src/takeover
|
||||
src/sony_api_lock.c src/kernel_rw_lock.c src/sys_time.c \
|
||||
src/sys_registry.c src/smp_meta.c src/profile.c \
|
||||
src/backup.c src/remoteplay.c src/rp_pair.c src/fan_curve.c src/fan_map.c src/indicator.c src/notif.c \
|
||||
src/wake_watchdog.c src/icon_keeper.c src/ffpfsc_unpack.c src/cheats.c src/cheats_list.c src/xml_encoding.c src/activity.c src/sdk_changer.c src/fakelib_overlay.c \
|
||||
src/wake_watchdog.c src/icon_keeper.c src/ffpfsc_unpack.c src/cheats.c src/cheats_plan.c src/cheats_list.c src/xml_encoding.c src/activity.c src/sdk_changer.c src/fakelib_overlay.c \
|
||||
src/tmdb.c src/fw_spoof.c src/focus_probe.c \
|
||||
src/content_db.c src/mgmt_rpc.c src/fs_jobs.c src/net_probe.c src/net_ports.c src/mgmt_fs.c src/path_policy.c \
|
||||
src/webui_http.c src/webui_auth.c src/webui_server.c src/webui_codec.c src/webui_api.c \
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#ifndef PS5UPLOAD_CHEATS_PLAN_H
|
||||
#define PS5UPLOAD_CHEATS_PLAN_H
|
||||
|
||||
/* cheats_plan — the pure part of applying one cheat: what each location holds now, which
|
||||
* locations are code caves, and the order the writes go in. No console calls, so the host
|
||||
* self-test (tests/cheats_plan_selftest.c) runs it on real cheat files.
|
||||
*
|
||||
* A cheat is applied as a whole or not at all. The old engine wrote whichever locations
|
||||
* matched and skipped the rest, so a cheat whose jump landed but whose code cave did not
|
||||
* sent the game into int3 padding. */
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define CHEAT_PLAN_MAX_BYTES 256
|
||||
|
||||
/* One location of a cheat, with the bytes read from the game before anything is written. */
|
||||
typedef struct {
|
||||
intptr_t addr;
|
||||
int len; /* on, off and cur are all this long */
|
||||
uint8_t on[CHEAT_PLAN_MAX_BYTES];
|
||||
uint8_t off[CHEAT_PLAN_MAX_BYTES];
|
||||
uint8_t cur[CHEAT_PLAN_MAX_BYTES];
|
||||
} cheat_loc_t;
|
||||
|
||||
enum {
|
||||
CHEAT_LOC_MISMATCH = -1, /* neither the cheat's bytes nor the original: wrong game build */
|
||||
CHEAT_LOC_OFF = 0, /* holds the original bytes */
|
||||
CHEAT_LOC_ON = 1, /* holds the cheat's bytes */
|
||||
};
|
||||
|
||||
/* Parse "90 1F-00,00" / "901F0000" into bytes. Separators: space , - : tab CR LF. A trailing
|
||||
* odd nibble becomes the low nibble of a last byte. Returns the byte count, or -1 on a
|
||||
* non-hex character, an empty string, or more than `max` bytes. */
|
||||
int cheat_parse_hex(const char *s, uint8_t *out, int max);
|
||||
|
||||
/* Bare or 0x-prefixed hex offset. Returns 0 and fills *out, or -1 when it is not hex. */
|
||||
int cheat_parse_offset(const char *s, uint64_t *out);
|
||||
|
||||
/* Which state a location is in, from the bytes read before writing. */
|
||||
int cheat_loc_state(const cheat_loc_t *l);
|
||||
|
||||
/* A code cave: the original bytes are all int3 (0xCC) or all zero padding, so nothing in the
|
||||
* game runs there until the cheat's own jump sends it there. */
|
||||
int cheat_loc_is_cave(const cheat_loc_t *l);
|
||||
|
||||
/* The order to write the n locations in. Turning on: caves first, then everything else, each
|
||||
* group in file order, so no jump is live before the code it jumps to. Turning off is the exact
|
||||
* reverse, so the jumps are restored before their caves go back to padding. The game keeps
|
||||
* running while we write, so this order is what keeps it from executing half a cheat.
|
||||
* Fills order[0..n-1] with indices into locs. */
|
||||
void cheat_plan_order(const cheat_loc_t *locs, int n, int turn_on, int *order);
|
||||
|
||||
/* A user-space address the engine is willing to write: above the null page, below the
|
||||
* canonical user limit, and not wrapping. */
|
||||
int cheat_addr_ok(intptr_t addr, int len);
|
||||
|
||||
#endif /* PS5UPLOAD_CHEATS_PLAN_H */
|
||||
+284
-276
@@ -6,16 +6,17 @@
|
||||
* - Three file formats: JSON (native), SHN (XML trainer), MC4 (base64 over
|
||||
* AES-256-CBC, decrypting to the same XML as SHN). All parsed into a
|
||||
* common in-memory representation.
|
||||
* - Memory writes via ptrace (PT_ATTACH + PT_IO), with kernel_mprotect
|
||||
* to flip execute-only pages to RWX temporarily.
|
||||
* - Memory reads/writes through the game's physical pages (page-table
|
||||
* walk + kernel direct map), applied all-or-nothing per cheat. See
|
||||
* "Game memory access" below for why not ptrace + kernel_mprotect.
|
||||
* - A background watcher thread polls every 3s for new game processes
|
||||
* and auto-applies "patches" (always-on) + re-applies user-toggled
|
||||
* cheats.
|
||||
* - Toggle state persisted to sidecar JSON files so it survives payload
|
||||
* redeploy.
|
||||
*
|
||||
* Ported from elf-arsenal/src/cheats.c, adapted to ps5upload's ptrace
|
||||
* infrastructure (ptrace_remote.c) and management protocol.
|
||||
* Ported from elf-arsenal/src/cheats.c, adapted to ps5upload's
|
||||
* management protocol.
|
||||
*/
|
||||
|
||||
#include "cheats.h"
|
||||
@@ -43,9 +44,17 @@
|
||||
|
||||
#include "notif.h"
|
||||
#include "proc_list.h"
|
||||
#include "ptrace_remote.h"
|
||||
#include "aes.h" /* tiny-AES, vendored — MC4 cheat decryption */
|
||||
#include "xml_encoding.h" /* UTF-16 → UTF-8 for SHN/MC4 trainer XML (#317) */
|
||||
#include "cheats_plan.h"
|
||||
#include "kernel_rw_lock.h"
|
||||
|
||||
#include <ps5/mdbg.h>
|
||||
|
||||
/* Defined by the payload's own build of the SDK startup code (third_party/sdk-crt), which every
|
||||
* payload links: the kernel direct-map base and the process page-table walk. */
|
||||
extern unsigned long KERNEL_ADDRESS_DMAP_BASE;
|
||||
int kernel_proc_getpaddr(int pid, unsigned long vaddr, unsigned long *paddr, unsigned long *plen);
|
||||
|
||||
/* ── Constants ───────────────────────────────────────────────────── */
|
||||
|
||||
@@ -64,7 +73,6 @@
|
||||
#define MAX_CHEAT_NAME 256
|
||||
#define MAX_MODS_PER_FILE 64
|
||||
#define MAX_MEM_ENTRIES 32
|
||||
#define MAX_HEX_BYTES 256
|
||||
#define MAX_JSON_BUF (256 * 1024)
|
||||
#define CHEAT_FILE_MAX (4 * 1024 * 1024)
|
||||
|
||||
@@ -72,14 +80,6 @@
|
||||
|
||||
#define KINFO_STRUCT_MINSIZE 448
|
||||
|
||||
#define PROT_READ 0x01
|
||||
#define PROT_WRITE 0x02
|
||||
#define PROT_EXEC 0x04
|
||||
|
||||
#define PAGE_SIZE_16K 0x4000
|
||||
#define ROUND_PG_DOWN(a) ((intptr_t)((a) & ~(intptr_t)(PAGE_SIZE_16K - 1)))
|
||||
#define ROUND_PG_UP(a) ((intptr_t)(((a) + PAGE_SIZE_16K - 1) & ~(intptr_t)(PAGE_SIZE_16K - 1)))
|
||||
|
||||
/* Canonical layout + title-id helpers. */
|
||||
#include "app_info.h"
|
||||
|
||||
@@ -101,47 +101,6 @@ static struct {
|
||||
struct timespec ts;
|
||||
} g_rg_cache;
|
||||
|
||||
/* ── Hex parsing ─────────────────────────────────────────────────── */
|
||||
|
||||
static int hex_val(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Parse a hex string like "90 1F 00 00" or "901F0000" into bytes.
|
||||
* Separators (space, comma, dash, colon, tab, newline) are skipped.
|
||||
* Returns byte count, or -1 on invalid hex. */
|
||||
static int parse_hex_bytes(const char *str, uint8_t *out, int max) {
|
||||
int n = 0;
|
||||
int hi = -1;
|
||||
for (const char *p = str; *p && n < max; p++) {
|
||||
if (*p == ' ' || *p == ',' || *p == '-' || *p == ':' ||
|
||||
*p == '\t' || *p == '\n' || *p == '\r') continue;
|
||||
int v = hex_val(*p);
|
||||
if (v < 0) return -1;
|
||||
if (hi < 0) {
|
||||
hi = v;
|
||||
} else {
|
||||
out[n++] = (uint8_t)(hi * 16 + v);
|
||||
hi = -1;
|
||||
}
|
||||
}
|
||||
if (hi >= 0) {
|
||||
/* dangling nibble — treat as low nibble of a byte */
|
||||
out[n++] = (uint8_t)(hi);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Parse offset string to intptr_t. Accepts bare hex or 0x-prefixed. */
|
||||
static intptr_t parse_offset(const char *s) {
|
||||
if (!s || !*s) return 0;
|
||||
if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) s += 2;
|
||||
return (intptr_t)strtoull(s, NULL, 16);
|
||||
}
|
||||
|
||||
/* ── JSON string extraction helpers (from runtime.c patterns) ────── */
|
||||
|
||||
/* Find the value of a string field in a JSON blob. Writes the unquoted,
|
||||
@@ -395,68 +354,73 @@ static void invalidate_rg_cache(void) {
|
||||
pthread_mutex_unlock(&g_rg_lock);
|
||||
}
|
||||
|
||||
/* ── Memory write via ptrace ─────────────────────────────────────── */
|
||||
/* ── Game memory access ──────────────────────────────────────────── */
|
||||
|
||||
/* Write bytes into a target process at addr. Temporarily flips page
|
||||
* protection to RWX if needed, writes, verifies by readback, restores.
|
||||
* Returns 0 on success, -1 on write failure, -2 on readback failure,
|
||||
* -3 on verify mismatch. */
|
||||
static int write_process_memory(pid_t pid, intptr_t addr,
|
||||
const uint8_t *data, size_t len) {
|
||||
if (len == 0) return 0;
|
||||
/* Read and write the game's memory through its physical pages: walk the process page tables
|
||||
* to the physical address and copy through the kernel's direct map. Nothing about the game
|
||||
* changes except the bytes: no ptrace stop, no page-protection change.
|
||||
*
|
||||
* The engine used to PT_ATTACH the game, flip the protection byte of the vm_map entry holding
|
||||
* the address to RWX with kernel_mprotect, write with PT_IO, and "restore" it to R-X. Measured
|
||||
* on FW 13.60 (Marvel's Wolverine, 2026-10-18): the game's .text is ONE 95 MiB vm_map entry
|
||||
* (0x400000-0x6324000) whose protection is execute-only (0x04). Every toggle therefore made the
|
||||
* whole executable writable and then left it R-X, a protection it never had, under a PT_IO
|
||||
* write that has to copy-on-write a page of Sony's executable mapping. kernel_mprotect edits
|
||||
* the entry byte directly (no pmap update, no entry split), and CheatRunner traced two real
|
||||
* kernel panics / console freezes to exactly that call before it moved to this physical-page
|
||||
* path, which works on the same consoles. The user report: every Wolverine cheat crashed the
|
||||
* console with ps5upload, while CheatRunner worked.
|
||||
*
|
||||
* mdbg (the debugger syscall) is the fallback for firmwares where the page walk is not
|
||||
* available; on FW 13.60 its write fails and its reads of code pages return filler, which is
|
||||
* why it is not the first choice. Every write is read back through the same physical path. */
|
||||
|
||||
intptr_t page = ROUND_PG_DOWN(addr);
|
||||
intptr_t span = ROUND_PG_UP(addr + len) - page;
|
||||
|
||||
/* Flip to RWX for the write */
|
||||
if (kernel_mprotect(pid, page, span,
|
||||
PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
|
||||
return -1;
|
||||
static int phys_copy(pid_t pid, intptr_t addr, void *buf, size_t len, int write) {
|
||||
if (!KERNEL_ADDRESS_DMAP_BASE) return -1;
|
||||
size_t done = 0;
|
||||
while (done < len) {
|
||||
unsigned long pa = 0, plen = 0;
|
||||
pthread_mutex_lock(&kernel_rw_lock);
|
||||
int rc = kernel_proc_getpaddr(pid, (unsigned long)(addr + (intptr_t)done), &pa, &plen);
|
||||
if (rc == 0 && plen > 0) {
|
||||
size_t chunk = len - done;
|
||||
if (plen < chunk) chunk = (size_t)plen;
|
||||
rc = write ? kernel_copyin((uint8_t *)buf + done, KERNEL_ADDRESS_DMAP_BASE + pa, chunk)
|
||||
: kernel_copyout(KERNEL_ADDRESS_DMAP_BASE + pa, (uint8_t *)buf + done, chunk);
|
||||
if (rc == 0) done += chunk;
|
||||
} else {
|
||||
rc = -1;
|
||||
}
|
||||
pthread_mutex_unlock(&kernel_rw_lock);
|
||||
if (rc != 0) return -1;
|
||||
}
|
||||
|
||||
/* Write */
|
||||
if (pt_copyin(pid, data, addr, len) != 0) {
|
||||
(void)kernel_mprotect(pid, page, span, PROT_READ | PROT_EXEC);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Verify by readback */
|
||||
uint8_t readback[MAX_HEX_BYTES];
|
||||
if (len > sizeof(readback)) len = sizeof(readback);
|
||||
if (pt_copyout(pid, addr, readback, len) != 0) {
|
||||
(void)kernel_mprotect(pid, page, span, PROT_READ | PROT_EXEC);
|
||||
return -2;
|
||||
}
|
||||
|
||||
int ok = (memcmp(readback, data, len) == 0);
|
||||
|
||||
/* Restore to R-X */
|
||||
(void)kernel_mprotect(pid, page, span, PROT_READ | PROT_EXEC);
|
||||
|
||||
return ok ? 0 : -3;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Read bytes from target process, handling execute-only (XOM) pages. */
|
||||
static int read_process_memory(pid_t pid, intptr_t addr,
|
||||
uint8_t *out, size_t len) {
|
||||
static int game_mem_read(pid_t pid, intptr_t addr, uint8_t *out, size_t len) {
|
||||
if (len == 0) return 0;
|
||||
if (pt_copyout(pid, addr, out, len) == 0) return 0;
|
||||
if (!cheat_addr_ok(addr, (int)len)) return -1;
|
||||
if (phys_copy(pid, addr, out, len, 0) == 0) return 0;
|
||||
/* mdbg swaps this process's authid, so it runs inside the shared swap-window lock. */
|
||||
pthread_mutex_lock(&kernel_rw_lock);
|
||||
int rc = mdbg_copyout(pid, addr, out, len);
|
||||
pthread_mutex_unlock(&kernel_rw_lock);
|
||||
return rc == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
/* pt_copyout can fail on XOM pages — temporarily add PROT_READ */
|
||||
intptr_t page = ROUND_PG_DOWN(addr);
|
||||
intptr_t span = ROUND_PG_UP(addr + len) - page;
|
||||
int orig_prot = kernel_get_vmem_protection(pid, page, span);
|
||||
if (orig_prot < 0) orig_prot = PROT_READ | PROT_EXEC;
|
||||
|
||||
if (!(orig_prot & PROT_READ)) {
|
||||
(void)kernel_mprotect(pid, page, span,
|
||||
(orig_prot | PROT_READ));
|
||||
/* Returns 0 when the bytes were written and read back equal. */
|
||||
static int game_mem_write(pid_t pid, intptr_t addr, const uint8_t *data, size_t len) {
|
||||
if (len == 0) return 0;
|
||||
if (!cheat_addr_ok(addr, (int)len) || len > CHEAT_PLAN_MAX_BYTES) return -1;
|
||||
if (phys_copy(pid, addr, (void *)data, len, 1) != 0) {
|
||||
pthread_mutex_lock(&kernel_rw_lock);
|
||||
int rc = mdbg_copyin(pid, data, addr, len);
|
||||
pthread_mutex_unlock(&kernel_rw_lock);
|
||||
if (rc != 0) return -1;
|
||||
}
|
||||
int rc = pt_copyout(pid, addr, out, len);
|
||||
if (!(orig_prot & PROT_READ)) {
|
||||
(void)kernel_mprotect(pid, page, span, orig_prot);
|
||||
}
|
||||
return rc;
|
||||
uint8_t back[CHEAT_PLAN_MAX_BYTES];
|
||||
if (game_mem_read(pid, addr, back, len) != 0) return -2;
|
||||
return memcmp(back, data, len) == 0 ? 0 : -3;
|
||||
}
|
||||
|
||||
/* ── Module base resolution ──────────────────────────────────────── */
|
||||
@@ -490,6 +454,7 @@ typedef struct {
|
||||
char type[16]; /* "checkbox" or "button" */
|
||||
char module_name[128];
|
||||
int enabled; /* persisted toggle state */
|
||||
int incomplete; /* the file had more than the parser could hold: never apply */
|
||||
mem_entry_t mem[MAX_MEM_ENTRIES];
|
||||
int mem_count;
|
||||
} cheat_mod_t;
|
||||
@@ -546,13 +511,15 @@ static int parse_json_file(const char *json, size_t json_len,
|
||||
/* Extract this mod's substring */
|
||||
size_t mod_len = (size_t)(mod_end - mod_start);
|
||||
char mod_buf[8192];
|
||||
if (mod_len >= sizeof(mod_buf)) mod_len = sizeof(mod_buf) - 1;
|
||||
int mod_cut = 0;
|
||||
if (mod_len >= sizeof(mod_buf)) { mod_len = sizeof(mod_buf) - 1; mod_cut = 1; }
|
||||
memcpy(mod_buf, mod_start, mod_len);
|
||||
mod_buf[mod_len] = '\0';
|
||||
|
||||
cheat_mod_t *m = &cf->mods[mod_idx];
|
||||
memset(m, 0, sizeof(*m));
|
||||
strcpy(m->type, "checkbox");
|
||||
m->incomplete = mod_cut;
|
||||
|
||||
json_str(mod_buf, "name", m->name, sizeof(m->name));
|
||||
json_str(mod_buf, "hint", m->hint, sizeof(m->hint));
|
||||
@@ -591,8 +558,10 @@ static int parse_json_file(const char *json, size_t json_len,
|
||||
|
||||
size_t me_len = (size_t)(me_end - me_start);
|
||||
char me_buf[2048];
|
||||
if (me_len >= sizeof(me_buf))
|
||||
if (me_len >= sizeof(me_buf)) {
|
||||
me_len = sizeof(me_buf) - 1;
|
||||
m->incomplete = 1;
|
||||
}
|
||||
memcpy(me_buf, me_start, me_len);
|
||||
me_buf[me_len] = '\0';
|
||||
|
||||
@@ -614,6 +583,8 @@ static int parse_json_file(const char *json, size_t json_len,
|
||||
if (e->offset_str[0]) mi++;
|
||||
mp = me_end;
|
||||
}
|
||||
/* Stopped at the cap with another entry still to come. */
|
||||
if (mi >= MAX_MEM_ENTRIES && strchr(mp, '{')) m->incomplete = 1;
|
||||
m->mem_count = mi;
|
||||
}
|
||||
}
|
||||
@@ -742,13 +713,15 @@ static int parse_shn_file(const char *xml, size_t xml_len,
|
||||
|
||||
size_t cheat_len = (size_t)(close_cheat - cheat_end);
|
||||
char cheat_buf[8192];
|
||||
if (cheat_len >= sizeof(cheat_buf)) cheat_len = sizeof(cheat_buf) - 1;
|
||||
int cheat_cut = 0;
|
||||
if (cheat_len >= sizeof(cheat_buf)) { cheat_len = sizeof(cheat_buf) - 1; cheat_cut = 1; }
|
||||
memcpy(cheat_buf, cheat_end, cheat_len);
|
||||
cheat_buf[cheat_len] = '\0';
|
||||
|
||||
cheat_mod_t *m = &cf->mods[mod_idx];
|
||||
memset(m, 0, sizeof(*m));
|
||||
strcpy(m->type, "checkbox");
|
||||
m->incomplete = cheat_cut;
|
||||
|
||||
xml_attr(cheat_start, "Text", m->name, sizeof(m->name));
|
||||
if (!m->name[0])
|
||||
@@ -787,7 +760,7 @@ static int parse_shn_file(const char *xml, size_t xml_len,
|
||||
}
|
||||
|
||||
char cl_buf[2048];
|
||||
if (cl_len >= sizeof(cl_buf)) cl_len = sizeof(cl_buf) - 1;
|
||||
if (cl_len >= sizeof(cl_buf)) { cl_len = sizeof(cl_buf) - 1; m->incomplete = 1; }
|
||||
memcpy(cl_buf, cl_end, cl_len);
|
||||
cl_buf[cl_len] = '\0';
|
||||
|
||||
@@ -797,7 +770,7 @@ static int parse_shn_file(const char *xml, size_t xml_len,
|
||||
/* For self-closing tags, extract attributes directly */
|
||||
char cl_full[2048];
|
||||
size_t full_len = (size_t)(cl_real_end - cl_start);
|
||||
if (full_len >= sizeof(cl_full)) full_len = sizeof(cl_full) - 1;
|
||||
if (full_len >= sizeof(cl_full)) { full_len = sizeof(cl_full) - 1; m->incomplete = 1; }
|
||||
memcpy(cl_full, cl_start, full_len);
|
||||
cl_full[full_len] = '\0';
|
||||
|
||||
@@ -838,6 +811,11 @@ static int parse_shn_file(const char *xml, size_t xml_len,
|
||||
if (e->offset_str[0]) mi++;
|
||||
cl_cursor = cl_real_end;
|
||||
}
|
||||
{
|
||||
const char *dummy_s, *dummy_e;
|
||||
if (mi >= MAX_MEM_ENTRIES && xml_find_tag(cl_cursor, "Cheatline", &dummy_s, &dummy_e))
|
||||
m->incomplete = 1;
|
||||
}
|
||||
m->mem_count = mi;
|
||||
|
||||
if (m->name[0] || mi > 0) mod_idx++;
|
||||
@@ -1041,68 +1019,63 @@ static void state_path(const char *title_id, char *out, size_t cap) {
|
||||
snprintf(out, cap, "%s/%s.json", CHEATS_STATE_DIR, title_id);
|
||||
}
|
||||
|
||||
/* Load enabled state from sidecar. Updates cf->mods[].enabled. */
|
||||
static void cheats_load_state(const char *title_id, cheat_file_t *cf) {
|
||||
/* The sidecar is {"<flat index>":true,...}: one flat index across every cheat file of the
|
||||
* title, in find_cheat_files() order — the same numbering the mod list and toggles use. */
|
||||
static int state_read(const char *title_id, char *buf, size_t cap) {
|
||||
char path[MAX_CHEAT_FILEPATH];
|
||||
state_path(title_id, path, sizeof(path));
|
||||
buf[0] = '\0';
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return;
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) != 0 || st.st_size > 4096) { close(fd); return; }
|
||||
char buf[4096];
|
||||
ssize_t rd = read(fd, buf, sizeof(buf) - 1);
|
||||
if (fd < 0) return -1;
|
||||
ssize_t rd = read(fd, buf, cap - 1);
|
||||
close(fd);
|
||||
if (rd <= 0) return;
|
||||
if (rd <= 0) { buf[0] = '\0'; return -1; }
|
||||
buf[rd] = '\0';
|
||||
|
||||
/* Parse the flat index keys: {"0":true,"3":true, ...}
|
||||
* These are flat indices across all files, but we only support
|
||||
* one file at a time here, so they map directly to cf->mods[]. */
|
||||
for (int i = 0; i < cf->mod_count; i++) {
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "\"%d\"", i);
|
||||
const char *p = strstr(buf, key);
|
||||
if (p) {
|
||||
p += strlen(key);
|
||||
while (*p && *p != ':') p++;
|
||||
if (*p == ':') {
|
||||
p++;
|
||||
while (*p == ' ' || *p == '\t' || *p == '\n') p++;
|
||||
if (strncmp(p, "true", 4) == 0) {
|
||||
cf->mods[i].enabled = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Save enabled state to sidecar. */
|
||||
static void save_state(const char *title_id, cheat_file_t *files,
|
||||
int n_files) {
|
||||
static int state_has(const char *buf, int flat) {
|
||||
char key[24];
|
||||
snprintf(key, sizeof(key), "\"%d\"", flat);
|
||||
const char *p = strstr(buf, key);
|
||||
if (!p) return 0;
|
||||
p += strlen(key);
|
||||
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == ':') p++;
|
||||
return strncmp(p, "true", 4) == 0;
|
||||
}
|
||||
|
||||
/* Load enabled state from sidecar into one file's mods, whose first mod has flat index
|
||||
* `flat_base`. */
|
||||
static void cheats_load_state(const char *title_id, cheat_file_t *cf, int flat_base) {
|
||||
char buf[4096];
|
||||
if (state_read(title_id, buf, sizeof(buf)) != 0) return;
|
||||
for (int i = 0; i < cf->mod_count; i++)
|
||||
if (state_has(buf, flat_base + i)) cf->mods[i].enabled = 1;
|
||||
}
|
||||
|
||||
/* Record one mod's state, keeping every other mod's. (This used to rewrite the file from the
|
||||
* one cheat file being toggled with per-file indices, so a toggle in a title's second file
|
||||
* marked the first file's mods instead, and the watcher re-applied those on the next launch.) */
|
||||
static void state_set(const char *title_id, int flat, int on) {
|
||||
char buf[4096];
|
||||
if (state_read(title_id, buf, sizeof(buf)) != 0) buf[0] = '\0';
|
||||
|
||||
char path[MAX_CHEAT_FILEPATH];
|
||||
state_path(title_id, path, sizeof(path));
|
||||
|
||||
char tmp[MAX_CHEAT_FILEPATH];
|
||||
char tmp[MAX_CHEAT_FILEPATH + 8];
|
||||
snprintf(tmp, sizeof(tmp), "%s.tmp", path);
|
||||
|
||||
FILE *f = fopen(tmp, "w");
|
||||
if (!f) return;
|
||||
|
||||
fprintf(f, "{");
|
||||
int first = 1;
|
||||
int flat = 0;
|
||||
for (int fi = 0; fi < n_files; fi++) {
|
||||
for (int mi = 0; mi < files[fi].mod_count; mi++) {
|
||||
if (files[fi].mods[mi].enabled) {
|
||||
if (!first) fprintf(f, ",");
|
||||
fprintf(f, "\"%d\":true", flat);
|
||||
first = 0;
|
||||
}
|
||||
flat++;
|
||||
}
|
||||
/* Keys are small: one per mod of the title (MAX_MODS_PER_FILE x 16 files at most). */
|
||||
for (int i = 0; i < MAX_MODS_PER_FILE * 16; i++) {
|
||||
int en = (i == flat) ? on : state_has(buf, i);
|
||||
if (!en) continue;
|
||||
fprintf(f, "%s\"%d\":true", first ? "" : ",", i);
|
||||
first = 0;
|
||||
}
|
||||
fprintf(f, "}");
|
||||
|
||||
if (fclose(f) == 0) {
|
||||
rename(tmp, path);
|
||||
} else {
|
||||
@@ -1112,97 +1085,151 @@ static void save_state(const char *title_id, cheat_file_t *files,
|
||||
|
||||
/* ── Core: apply a single mod ────────────────────────────────────── */
|
||||
|
||||
static int apply_mod(pid_t pid, intptr_t base, cheat_mod_t *mod,
|
||||
int turn_on, char *err, size_t err_cap) {
|
||||
/* Apply or remove one cheat, all of its locations or none of them.
|
||||
*
|
||||
* 1. Resolve and parse every location and read what it holds now. A location that holds
|
||||
* neither the cheat's bytes nor the original bytes means a different game build (or a
|
||||
* clashing cheat), and the whole cheat is refused before anything is written.
|
||||
* 2. Write in cheat_plan_order(): caves before the jumps into them when turning on, the
|
||||
* reverse when turning off. The game is not stopped, so this order is what keeps it from
|
||||
* running a jump whose target is still padding.
|
||||
* 3. If a write fails, put back what was already written, newest first.
|
||||
*
|
||||
* Returns 0 when the cheat is in the asked-for state (including "it already was"). */
|
||||
static int apply_mod_locked(pid_t pid, intptr_t base, cheat_mod_t *mod,
|
||||
int turn_on, char *err, size_t err_cap) {
|
||||
if (mod->incomplete) {
|
||||
if (err) snprintf(err, err_cap,
|
||||
"cheat has more locations than the engine can hold; not applied");
|
||||
return -1;
|
||||
}
|
||||
if (mod->mem_count == 0) {
|
||||
if (err) snprintf(err, err_cap, "mod has no memory entries");
|
||||
return -1;
|
||||
}
|
||||
/* No base yet means the module is not mapped: a relative offset would be used as an
|
||||
* absolute address. */
|
||||
if (base <= 0) {
|
||||
if (err) snprintf(err, err_cap, "the game's code is not loaded yet; try again");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* "button" type always writes "on" regardless of turn_on */
|
||||
int write_on = (strcmp(mod->type, "button") == 0) ? 1 : turn_on;
|
||||
|
||||
int wrote = 0;
|
||||
int skipped = 0;
|
||||
int n = mod->mem_count;
|
||||
cheat_loc_t *locs = (cheat_loc_t *)calloc((size_t)n, sizeof(cheat_loc_t));
|
||||
if (!locs) {
|
||||
if (err) snprintf(err, err_cap, "out of memory");
|
||||
return -1;
|
||||
}
|
||||
int rc = 0;
|
||||
|
||||
for (int i = 0; i < mod->mem_count; i++) {
|
||||
for (int i = 0; i < n && rc == 0; i++) {
|
||||
mem_entry_t *e = &mod->mem[i];
|
||||
if (!e->offset_str[0] || !e->on_str[0]) {
|
||||
skipped++;
|
||||
continue;
|
||||
cheat_loc_t *l = &locs[i];
|
||||
uint64_t off = 0;
|
||||
if (cheat_parse_offset(e->offset_str, &off) != 0) {
|
||||
if (err) snprintf(err, err_cap, "location %d: bad offset \"%s\"", i + 1, e->offset_str);
|
||||
rc = -1;
|
||||
break;
|
||||
}
|
||||
if (strlen(e->on_str) + 1 >= sizeof(e->on_str) ||
|
||||
strlen(e->off_str) + 1 >= sizeof(e->off_str)) {
|
||||
if (err) snprintf(err, err_cap, "location %d: value too long for the engine", i + 1);
|
||||
rc = -1;
|
||||
break;
|
||||
}
|
||||
int on_len = cheat_parse_hex(e->on_str, l->on, CHEAT_PLAN_MAX_BYTES);
|
||||
int off_len = cheat_parse_hex(e->off_str, l->off, CHEAT_PLAN_MAX_BYTES);
|
||||
if (on_len <= 0 || off_len <= 0) {
|
||||
if (err) snprintf(err, err_cap,
|
||||
"location %d: needs both its cheat and original bytes", i + 1);
|
||||
rc = -1;
|
||||
break;
|
||||
}
|
||||
if (on_len != off_len) {
|
||||
if (err) snprintf(err, err_cap,
|
||||
"location %d: cheat and original bytes differ in length (%d vs %d)",
|
||||
i + 1, on_len, off_len);
|
||||
rc = -1;
|
||||
break;
|
||||
}
|
||||
l->len = on_len;
|
||||
|
||||
intptr_t addr;
|
||||
if (e->absolute) {
|
||||
addr = parse_offset(e->offset_str);
|
||||
l->addr = (intptr_t)off;
|
||||
} else if (e->section > 0) {
|
||||
intptr_t sec_base = kernel_dynlib_mapbase_addr(pid, (uint32_t)e->section);
|
||||
addr = sec_base + parse_offset(e->offset_str);
|
||||
} else {
|
||||
addr = base + parse_offset(e->offset_str);
|
||||
}
|
||||
|
||||
/* Determine which bytes to write */
|
||||
const char *write_hex = write_on ? e->on_str : e->off_str;
|
||||
if (!write_hex[0]) write_hex = e->on_str;
|
||||
|
||||
uint8_t write_bytes[MAX_HEX_BYTES];
|
||||
int wlen = parse_hex_bytes(write_hex, write_bytes, sizeof(write_bytes));
|
||||
if (wlen <= 0) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Baseline guard: read current bytes and verify they match
|
||||
* either the "on" or "off" pattern. This prevents crashes from
|
||||
* wrong-game-build cheats. */
|
||||
uint8_t current[MAX_HEX_BYTES];
|
||||
memset(current, 0, sizeof(current));
|
||||
|
||||
/* Parse on/off patterns for comparison */
|
||||
uint8_t on_bytes[MAX_HEX_BYTES], off_bytes[MAX_HEX_BYTES];
|
||||
int on_len = e->on_str[0] ?
|
||||
parse_hex_bytes(e->on_str, on_bytes, sizeof(on_bytes)) : 0;
|
||||
int off_len = e->off_str[0] ?
|
||||
parse_hex_bytes(e->off_str, off_bytes, sizeof(off_bytes)) : 0;
|
||||
|
||||
/* Read current bytes at the address */
|
||||
int read_ok = (read_process_memory(pid, addr, current,
|
||||
(size_t)(on_len > 0 ? on_len : wlen)) == 0);
|
||||
if (read_ok) {
|
||||
int matches_on = (on_len > 0 &&
|
||||
memcmp(current, on_bytes, (size_t)on_len) == 0);
|
||||
int matches_off = (off_len > 0 &&
|
||||
memcmp(current, off_bytes, (size_t)off_len) == 0);
|
||||
if (!matches_on && !matches_off) {
|
||||
/* Baseline mismatch — wrong game build or wrong base */
|
||||
skipped++;
|
||||
continue;
|
||||
if (sec_base <= 0) {
|
||||
if (err) snprintf(err, err_cap, "location %d: module %d is not loaded",
|
||||
i + 1, e->section);
|
||||
rc = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Write the bytes */
|
||||
int rc = write_process_memory(pid, addr, write_bytes, (size_t)wlen);
|
||||
if (rc == 0) {
|
||||
wrote++;
|
||||
l->addr = (intptr_t)((uint64_t)sec_base + off);
|
||||
} else {
|
||||
/* Write failed — don't abort, try remaining entries */
|
||||
skipped++;
|
||||
l->addr = (intptr_t)((uint64_t)base + off);
|
||||
}
|
||||
|
||||
if (game_mem_read(pid, l->addr, l->cur, (size_t)l->len) != 0) {
|
||||
if (err) snprintf(err, err_cap, "location %d (0x%lx) cannot be read",
|
||||
i + 1, (unsigned long)l->addr);
|
||||
rc = -1;
|
||||
break;
|
||||
}
|
||||
if (cheat_loc_state(l) == CHEAT_LOC_MISMATCH) {
|
||||
if (err) snprintf(err, err_cap,
|
||||
"cheat does not match this game build "
|
||||
"(location %d at 0x%lx holds other bytes); nothing was written",
|
||||
i + 1, (unsigned long)l->addr);
|
||||
rc = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (wrote > 0) {
|
||||
return 0;
|
||||
int order[MAX_MEM_ENTRIES];
|
||||
int written[MAX_MEM_ENTRIES];
|
||||
int n_written = 0;
|
||||
if (rc == 0) {
|
||||
cheat_plan_order(locs, n, write_on, order);
|
||||
for (int k = 0; k < n; k++) {
|
||||
cheat_loc_t *l = &locs[order[k]];
|
||||
const uint8_t *want = write_on ? l->on : l->off;
|
||||
if (memcmp(l->cur, want, (size_t)l->len) == 0) continue; /* already there */
|
||||
int wrc = game_mem_write(pid, l->addr, want, (size_t)l->len);
|
||||
if (wrc != 0) {
|
||||
if (err) snprintf(err, err_cap, "write failed at 0x%lx (rc=%d)%s",
|
||||
(unsigned long)l->addr, wrc,
|
||||
n_written ? "; earlier writes were undone" : "");
|
||||
rc = -1;
|
||||
break;
|
||||
}
|
||||
written[n_written++] = order[k];
|
||||
}
|
||||
}
|
||||
|
||||
if (skipped > 0 && err) {
|
||||
snprintf(err, err_cap,
|
||||
"cheat does not match this game build "
|
||||
"(%d location(s) skipped)", skipped);
|
||||
} else if (err) {
|
||||
snprintf(err, err_cap, "no writable memory entries");
|
||||
if (rc != 0) {
|
||||
for (int k = n_written - 1; k >= 0; k--) {
|
||||
cheat_loc_t *l = &locs[written[k]];
|
||||
(void)game_mem_write(pid, l->addr, l->cur, (size_t)l->len);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
|
||||
free(locs);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* One apply at a time: the PC app, the console page and the watcher can all ask at once, and
|
||||
* two applies of overlapping cheats must not interleave their reads and writes. */
|
||||
static pthread_mutex_t g_apply_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static int apply_mod(pid_t pid, intptr_t base, cheat_mod_t *mod,
|
||||
int turn_on, char *err, size_t err_cap) {
|
||||
pthread_mutex_lock(&g_apply_lock);
|
||||
int rc = apply_mod_locked(pid, base, mod, turn_on, err, err_cap);
|
||||
pthread_mutex_unlock(&g_apply_lock);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* ── Background watcher thread ───────────────────────────────────── */
|
||||
@@ -1246,22 +1273,20 @@ static int apply_patches_for_game(pid_t pid, intptr_t base,
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pt_attach(pid) != 0) {
|
||||
free(cf);
|
||||
return total_writes;
|
||||
}
|
||||
if (name_out && name_cap && !name_out[0] &&
|
||||
is_usable_game_name(cf->game_name, title_id)) {
|
||||
snprintf(name_out, name_cap, "%s", cf->game_name);
|
||||
}
|
||||
for (int m = 0; m < cf->mod_count; m++) {
|
||||
char err[128];
|
||||
char err[160];
|
||||
/* Patches are always-on: turn_on = 1 */
|
||||
if (apply_mod(pid, base, &cf->mods[m], 1, err, sizeof(err)) == 0) {
|
||||
intptr_t mod_base = base;
|
||||
if (cf->mods[m].module_name[0])
|
||||
mod_base = resolve_module_base(pid, cf->mods[m].module_name);
|
||||
if (mod_base && apply_mod(pid, mod_base, &cf->mods[m], 1, err, sizeof(err)) == 0) {
|
||||
total_writes++;
|
||||
}
|
||||
}
|
||||
pt_detach(pid, 0);
|
||||
free(cf);
|
||||
}
|
||||
atomic_store(&g_patches_last, n);
|
||||
@@ -1277,40 +1302,36 @@ static int reapply_enabled_for_game(pid_t pid, intptr_t base,
|
||||
found_file_t files[16];
|
||||
int n = find_cheat_files(title_id, files, 16, 0 /* cheats */);
|
||||
|
||||
int applied = 0;
|
||||
int flat_base = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
cheat_file_t *cf = (cheat_file_t *)malloc(sizeof(cheat_file_t));
|
||||
if (!cf) continue;
|
||||
/* A file that fails to load lists no mods, so it adds nothing to the flat numbering
|
||||
* (cheats_list_mods skips it the same way). */
|
||||
if (load_cheat_file(files[i].path, files[i].format, cf) != 0) {
|
||||
free(cf);
|
||||
continue;
|
||||
}
|
||||
cheats_load_state(title_id, cf);
|
||||
cheats_load_state(title_id, cf, flat_base);
|
||||
flat_base += cf->mod_count;
|
||||
|
||||
int has_enabled = 0;
|
||||
for (int m = 0; m < cf->mod_count; m++) {
|
||||
if (cf->mods[m].enabled) { has_enabled = 1; break; }
|
||||
}
|
||||
if (!has_enabled) { free(cf); continue; }
|
||||
|
||||
if (pt_attach(pid) != 0) { free(cf); return 0; }
|
||||
if (name_out && name_cap && !name_out[0] &&
|
||||
is_usable_game_name(cf->game_name, title_id)) {
|
||||
snprintf(name_out, name_cap, "%s", cf->game_name);
|
||||
}
|
||||
int applied = 0;
|
||||
for (int m = 0; m < cf->mod_count; m++) {
|
||||
if (cf->mods[m].enabled) {
|
||||
char err[128];
|
||||
if (apply_mod(pid, base, &cf->mods[m], 1, err, sizeof(err)) == 0) {
|
||||
applied++;
|
||||
}
|
||||
if (!cf->mods[m].enabled) continue;
|
||||
if (name_out && name_cap && !name_out[0] &&
|
||||
is_usable_game_name(cf->game_name, title_id)) {
|
||||
snprintf(name_out, name_cap, "%s", cf->game_name);
|
||||
}
|
||||
char err[160];
|
||||
intptr_t mod_base = base;
|
||||
if (cf->mods[m].module_name[0])
|
||||
mod_base = resolve_module_base(pid, cf->mods[m].module_name);
|
||||
if (mod_base && apply_mod(pid, mod_base, &cf->mods[m], 1, err, sizeof(err)) == 0)
|
||||
applied++;
|
||||
}
|
||||
pt_detach(pid, 0);
|
||||
free(cf);
|
||||
return applied; /* Only process the first matching file */
|
||||
}
|
||||
return 0;
|
||||
return applied;
|
||||
}
|
||||
|
||||
static void *watcher_thread(void *arg) {
|
||||
@@ -1480,7 +1501,7 @@ int cheats_list_mods(const char *title_id, char *buf, size_t cap,
|
||||
free(cf);
|
||||
continue;
|
||||
}
|
||||
cheats_load_state(title_id, cf);
|
||||
cheats_load_state(title_id, cf, flat);
|
||||
|
||||
for (int mi = 0; mi < cf->mod_count; mi++) {
|
||||
cheat_mod_t *m = &cf->mods[mi];
|
||||
@@ -1593,36 +1614,23 @@ int cheats_toggle(const char *title_id, int mod_index, int turn_on,
|
||||
}
|
||||
}
|
||||
|
||||
/* Attach and apply. A failed attach is told apart, because the console page (webui_codec.c
|
||||
* webui_cheats_toggle_error matches these texts; keep them in step) says each differently:
|
||||
* - the game exited a moment ago (the 2 s running-game cache still had its pid): the same
|
||||
* text as no game at all, and the cache is dropped so the next status read sees it gone;
|
||||
* - the game is alive but another debugger is attached (EBUSY: the PC app toggling at the
|
||||
* same moment, or another cheat tool): "is attached to the game";
|
||||
* - anything else: the plain "pt_attach failed (errno=N)". */
|
||||
if (pt_attach(pid) != 0) {
|
||||
int e = errno;
|
||||
/* The game may have exited a moment ago (the 2 s running-game cache still had its pid): say
|
||||
* so with the same text as no game at all (webui_codec.c webui_cheats_toggle_error matches
|
||||
* it), and drop the cache so the next status read sees it gone. */
|
||||
if (kill(pid, 0) != 0 && errno == ESRCH) {
|
||||
free(target_cf);
|
||||
invalidate_rg_cache();
|
||||
if (kill(pid, 0) != 0 && errno == ESRCH) {
|
||||
if (err) snprintf(err, err_cap, "no game is currently running");
|
||||
} else if (e == EBUSY) {
|
||||
if (err) snprintf(err, err_cap, "pt_attach failed: another debugger or cheat tool is attached to the game (errno=%d)", e);
|
||||
} else if (err) {
|
||||
snprintf(err, err_cap, "pt_attach failed (errno=%d)", e);
|
||||
}
|
||||
if (err) snprintf(err, err_cap, "no game is currently running");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int rc = apply_mod(pid, mod_base, mod, turn_on, err, err_cap);
|
||||
|
||||
pt_detach(pid, 0);
|
||||
|
||||
if (rc == 0) {
|
||||
/* Update persisted state (only for checkbox type) */
|
||||
if (strcmp(mod->type, "button") != 0) {
|
||||
mod->enabled = turn_on;
|
||||
save_state(title_id, target_cf, 1);
|
||||
state_set(title_id, mod_index, turn_on);
|
||||
}
|
||||
|
||||
/* Confirm on the console.
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/* cheats_plan.c — see cheats_plan.h. Pure C, no console calls. */
|
||||
|
||||
#include "cheats_plan.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int hex_val(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int is_sep(char c) {
|
||||
return c == ' ' || c == ',' || c == '-' || c == ':' || c == '\t' || c == '\n' || c == '\r';
|
||||
}
|
||||
|
||||
int cheat_parse_hex(const char *s, uint8_t *out, int max) {
|
||||
if (!s || !out || max <= 0) return -1;
|
||||
int n = 0;
|
||||
int hi = -1;
|
||||
for (const char *p = s; *p; p++) {
|
||||
if (is_sep(*p)) continue;
|
||||
int v = hex_val(*p);
|
||||
if (v < 0) return -1;
|
||||
if (hi < 0) {
|
||||
hi = v;
|
||||
} else {
|
||||
if (n >= max) return -1;
|
||||
out[n++] = (uint8_t)(hi * 16 + v);
|
||||
hi = -1;
|
||||
}
|
||||
}
|
||||
if (hi >= 0) {
|
||||
if (n >= max) return -1;
|
||||
out[n++] = (uint8_t)hi;
|
||||
}
|
||||
return n > 0 ? n : -1;
|
||||
}
|
||||
|
||||
int cheat_parse_offset(const char *s, uint64_t *out) {
|
||||
if (!s || !out) return -1;
|
||||
while (*s == ' ' || *s == '\t') s++;
|
||||
if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) s += 2;
|
||||
if (!*s) return -1;
|
||||
const char *p = s;
|
||||
while (hex_val(*p) >= 0) p++;
|
||||
while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++;
|
||||
if (*p) return -1;
|
||||
errno = 0;
|
||||
unsigned long long v = strtoull(s, NULL, 16);
|
||||
if (errno != 0) return -1;
|
||||
*out = (uint64_t)v;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cheat_loc_state(const cheat_loc_t *l) {
|
||||
size_t n = (size_t)l->len;
|
||||
if (memcmp(l->cur, l->on, n) == 0) return CHEAT_LOC_ON;
|
||||
if (memcmp(l->cur, l->off, n) == 0) return CHEAT_LOC_OFF;
|
||||
return CHEAT_LOC_MISMATCH;
|
||||
}
|
||||
|
||||
int cheat_loc_is_cave(const cheat_loc_t *l) {
|
||||
if (l->len <= 0) return 0;
|
||||
uint8_t first = l->off[0];
|
||||
if (first != 0xCC && first != 0x00) return 0;
|
||||
for (int i = 1; i < l->len; i++)
|
||||
if (l->off[i] != first) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void cheat_plan_order(const cheat_loc_t *locs, int n, int turn_on, int *order) {
|
||||
int k = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
if (cheat_loc_is_cave(&locs[i])) order[k++] = i;
|
||||
for (int i = 0; i < n; i++)
|
||||
if (!cheat_loc_is_cave(&locs[i])) order[k++] = i;
|
||||
if (!turn_on) {
|
||||
for (int i = 0, j = n - 1; i < j; i++, j--) {
|
||||
int t = order[i];
|
||||
order[i] = order[j];
|
||||
order[j] = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int cheat_addr_ok(intptr_t addr, int len) {
|
||||
if (len <= 0) return 0;
|
||||
uint64_t a = (uint64_t)addr;
|
||||
uint64_t end = a + (uint64_t)len;
|
||||
return a >= 0x10000ULL && end > a && end <= 0x800000000000ULL;
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/* Host-side tests for cheats_plan.c: parsing a cheat's locations, telling which state each is
|
||||
* in, and the order the writes go in. The data is the real Marvel's Wolverine (PPSA03671,
|
||||
* 01.001.005) cheat pack the app downloads, whose cheats crashed the console with the old
|
||||
* engine (fix/cheats-wolverine). */
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../src/cheats_plan.c"
|
||||
|
||||
static int failures = 0;
|
||||
|
||||
#define CHECK(expr) \
|
||||
do { \
|
||||
if (!(expr)) { \
|
||||
fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #expr); \
|
||||
failures++; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
typedef struct { const char *off, *on, *orig; } raw_t;
|
||||
|
||||
/* "Max Resources / XP" from PPSA03671_01.001.005_50d8b46b.json, in file order: 16 code caves
|
||||
* (original bytes are int3 padding) and 4 hooks that jump into them. */
|
||||
static const raw_t MAX_RES[] = {
|
||||
{"05CC0A91", "4C8B184D8B5B30E9E4060000", "CCCCCCCCCCCCCCCCCCCCCCCC"},
|
||||
{"05CC1181", "4D85DB0F84D72D0000E932060000", "CCCCCCCCCCCCCCCCCCCCCCCCCCCC"},
|
||||
{"05CC17C1", "450FB65370E9F6050000", "CCCCCCCCCCCCCCCCCCCC"},
|
||||
{"05CC1DC1", "4584D20F8497210000E942010000", "CCCCCCCCCCCCCCCCCCCCCCCCCCCC"},
|
||||
{"05CC1F11", "837808000F8E46200000E9111E0000", "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"},
|
||||
{"05CC3D31", "C740089F860100C3", "CCCCCCCCCCCCCCCC"},
|
||||
{"05CC3F61", "C3", "CC"},
|
||||
{"05CC4091", "E8FBC9FFFF8B4808E97D7F0100", "CCCCCCCCCCCCCCCCCCCCCCCCCC"},
|
||||
{"05CDC014", "E97880FEFF", "8B4808EB02"},
|
||||
{"05CC5041", "E84BBAFFFF8B4808E99D700100", "CCCCCCCCCCCCCCCCCCCCCCCCCC"},
|
||||
{"05CDC0E4", "E9588FFEFF", "8B4808EB02"},
|
||||
{"05CC50A1", "E8EBB9FFFF8B7008E91E710100", "CCCCCCCCCCCCCCCCCCCCCCCCCC"},
|
||||
{"05CDC1C5", "E9D78EFEFF", "8B7008EB02"},
|
||||
{"05CC7A81", "4C8B5B30E907000000", "CCCCCCCCCCCCCCCCCC"},
|
||||
{"05CC7A91", "4D85DB0F84570F0000E922020000", "CCCCCCCCCCCCCCCCCCCCCCCCCCCC"},
|
||||
{"05CC7CC1", "450FB65370E926030000", "CCCCCCCCCCCCCCCCCCCC"},
|
||||
{"05CC7FF1", "4584D20F84F7090000E952000000", "CCCCCCCCCCCCCCCCCCCCCCCCCCCC"},
|
||||
{"05CC8051", "41BD9F860100E995090000", "CCCCCCCCCCCCCCCCCCCCCC"},
|
||||
{"05CC89F1", "4489EB45896F08E9F73F0100", "CCCCCCCCCCCCCCCCCCCCCCCC"},
|
||||
{"05CDC9ED", "E98FB0FEFF9090", "4489EB45896F08"},
|
||||
};
|
||||
#define N_MAX_RES ((int)(sizeof MAX_RES / sizeof MAX_RES[0]))
|
||||
|
||||
/* "Infinite Health" from PPSA03671_01.001.005_d02e93c8.mc4: the hook comes SECOND in the file,
|
||||
* the cave is zero padding at the end of .text, and the values use dash separators. */
|
||||
static const raw_t MC4_HEALTH[] = {
|
||||
{"4803568", "E9-D3-04-72-01-90-90-90", "C5-FA-11-83-D0-00-00-00"},
|
||||
{"5F23A40",
|
||||
"80-BB-4C-01-00-00-00-75-0F-C7-83-D0-00-00-00-00-00-96-43-E9-18-FB-8D-FE-C5-FA-11-83-D0-00-00-00-E9-0B-FB-8D-FE",
|
||||
"00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00"},
|
||||
};
|
||||
|
||||
static int build(const raw_t *raw, int n, intptr_t base, cheat_loc_t *locs) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
uint64_t off = 0;
|
||||
if (cheat_parse_offset(raw[i].off, &off) != 0) return -1;
|
||||
int a = cheat_parse_hex(raw[i].on, locs[i].on, CHEAT_PLAN_MAX_BYTES);
|
||||
int b = cheat_parse_hex(raw[i].orig, locs[i].off, CHEAT_PLAN_MAX_BYTES);
|
||||
if (a <= 0 || a != b) return -1;
|
||||
locs[i].len = a;
|
||||
locs[i].addr = base + (intptr_t)off;
|
||||
memcpy(locs[i].cur, locs[i].off, (size_t)a); /* the game as it boots */
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Position of location `idx` in a write order. */
|
||||
static int pos_of(const int *order, int n, int idx) {
|
||||
for (int k = 0; k < n; k++)
|
||||
if (order[k] == idx) return k;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void test_parse(void) {
|
||||
uint8_t b[8];
|
||||
CHECK(cheat_parse_hex("90 1F-00,00", b, 8) == 4 && b[0] == 0x90 && b[1] == 0x1F && b[3] == 0);
|
||||
CHECK(cheat_parse_hex("C3", b, 8) == 1 && b[0] == 0xC3);
|
||||
CHECK(cheat_parse_hex("ABC", b, 8) == 2 && b[0] == 0xAB && b[1] == 0x0C);
|
||||
CHECK(cheat_parse_hex("", b, 8) == -1);
|
||||
CHECK(cheat_parse_hex("9G", b, 8) == -1);
|
||||
CHECK(cheat_parse_hex("0102030405", b, 4) == -1); /* longer than the buffer: refused, not cut */
|
||||
|
||||
uint64_t o = 0;
|
||||
CHECK(cheat_parse_offset("01BAD908", &o) == 0 && o == 0x1BAD908);
|
||||
CHECK(cheat_parse_offset("0x1bad908", &o) == 0 && o == 0x1BAD908);
|
||||
CHECK(cheat_parse_offset(" 5F23A40\n", &o) == 0 && o == 0x5F23A40);
|
||||
CHECK(cheat_parse_offset("5C0-executable[0]-5", &o) == -1); /* MC4 <PS5Offset> form */
|
||||
CHECK(cheat_parse_offset("", &o) == -1);
|
||||
}
|
||||
|
||||
static void test_state(void) {
|
||||
cheat_loc_t l[2];
|
||||
/* God Mode, the JSON pack: base 0x400000 measured on the Pro (FW 13.60). */
|
||||
static const raw_t god[] = {
|
||||
{"01BAD908", "909090909090909090", "C4E2719D8FF0010000"},
|
||||
{"01BAD911", "C5F828C1", "C5F25DC0"},
|
||||
};
|
||||
CHECK(build(god, 2, 0x400000, l) == 0);
|
||||
CHECK(l[0].addr == 0x1FAD908 && l[0].len == 9 && l[1].len == 4);
|
||||
CHECK(cheat_loc_state(&l[0]) == CHEAT_LOC_OFF);
|
||||
memcpy(l[0].cur, l[0].on, 9);
|
||||
CHECK(cheat_loc_state(&l[0]) == CHEAT_LOC_ON);
|
||||
l[0].cur[3] ^= 0xFF; /* another build, or another cheat's jump already there */
|
||||
CHECK(cheat_loc_state(&l[0]) == CHEAT_LOC_MISMATCH);
|
||||
CHECK(!cheat_loc_is_cave(&l[0]) && !cheat_loc_is_cave(&l[1]));
|
||||
}
|
||||
|
||||
static void test_order_max_resources(void) {
|
||||
cheat_loc_t locs[N_MAX_RES];
|
||||
CHECK(build(MAX_RES, N_MAX_RES, 0x400000, locs) == 0);
|
||||
int hooks[] = {8, 10, 12, 19};
|
||||
int caves = 0;
|
||||
for (int i = 0; i < N_MAX_RES; i++) caves += cheat_loc_is_cave(&locs[i]);
|
||||
CHECK(caves == 16);
|
||||
for (int h = 0; h < 4; h++) CHECK(!cheat_loc_is_cave(&locs[hooks[h]]));
|
||||
|
||||
int on[N_MAX_RES], off[N_MAX_RES];
|
||||
cheat_plan_order(locs, N_MAX_RES, 1, on);
|
||||
cheat_plan_order(locs, N_MAX_RES, 0, off);
|
||||
/* Every location exactly once. */
|
||||
int seen[N_MAX_RES] = {0};
|
||||
for (int k = 0; k < N_MAX_RES; k++) seen[on[k]]++;
|
||||
for (int i = 0; i < N_MAX_RES; i++) CHECK(seen[i] == 1);
|
||||
/* Turning on: every cave is written before any hook. In file order, the hook at
|
||||
* 05CDC014 (index 8) came before the cave at 05CC5041 (index 9). */
|
||||
for (int h = 0; h < 4; h++)
|
||||
for (int i = 0; i < N_MAX_RES; i++)
|
||||
if (cheat_loc_is_cave(&locs[i])) CHECK(pos_of(on, N_MAX_RES, i) < pos_of(on, N_MAX_RES, hooks[h]));
|
||||
/* Turning off: every hook is restored before any cave goes back to int3. In file order
|
||||
* the cave at 05CC4091 (index 7) was cleared while the hook into it (index 8) was live. */
|
||||
for (int h = 0; h < 4; h++)
|
||||
for (int i = 0; i < N_MAX_RES; i++)
|
||||
if (cheat_loc_is_cave(&locs[i])) CHECK(pos_of(off, N_MAX_RES, hooks[h]) < pos_of(off, N_MAX_RES, i));
|
||||
for (int k = 0; k < N_MAX_RES; k++) CHECK(off[k] == on[N_MAX_RES - 1 - k]);
|
||||
}
|
||||
|
||||
static void test_order_mc4_zero_cave(void) {
|
||||
cheat_loc_t locs[2];
|
||||
CHECK(build(MC4_HEALTH, 2, 0x400000, locs) == 0);
|
||||
CHECK(locs[0].len == 8 && locs[1].len == 37);
|
||||
CHECK(!cheat_loc_is_cave(&locs[0]) && cheat_loc_is_cave(&locs[1]));
|
||||
int on[2], off[2];
|
||||
cheat_plan_order(locs, 2, 1, on);
|
||||
cheat_plan_order(locs, 2, 0, off);
|
||||
CHECK(on[0] == 1 && on[1] == 0); /* cave, then the jump into it */
|
||||
CHECK(off[0] == 0 && off[1] == 1); /* jump restored, then the cave cleared */
|
||||
/* The cave ends inside .text: 0x400000 + 0x5F23A40 + 37 < 0x6324000 (measured end). */
|
||||
CHECK(locs[1].addr + locs[1].len <= 0x6324000);
|
||||
}
|
||||
|
||||
static void test_addr_ok(void) {
|
||||
CHECK(cheat_addr_ok(0x1FAD908, 9));
|
||||
CHECK(!cheat_addr_ok(0, 4));
|
||||
CHECK(!cheat_addr_ok(0x1BAD908 - 0x1BAD908 + 0x100, 4)); /* null page */
|
||||
CHECK(!cheat_addr_ok((intptr_t)0xFFFFF04080F57368ULL, 4)); /* kernel address */
|
||||
CHECK(!cheat_addr_ok(0x7FFFFFFFFFFE, 4)); /* crosses the user limit */
|
||||
CHECK(!cheat_addr_ok(0x400000, 0));
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_parse();
|
||||
test_state();
|
||||
test_order_max_resources();
|
||||
test_order_mc4_zero_cave();
|
||||
test_addr_ok();
|
||||
if (failures) {
|
||||
fprintf(stderr, "cheats_plan_selftest: %d failure(s)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
printf("cheats_plan_selftest: ok\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user