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:
phantomptrandClaude Opus 5.5 committed 2026-10-18 11:27:08 -07:00
1 parent a90ea16b4b
commit 02469c725b
6 files changed
+620 -277

No files matched your search

+6
View File
@@ -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
View File
@@ -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 \
+58
View File
@@ -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
View File
@@ -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.
+95
View File
@@ -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;
}
+176
View File
@@ -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;
}