cheats: review fixes — rollback, private writes, FW split, shared caves

- Rollback restores the failing location too (a write torn across two
  pages, or a read-back mismatch), then the earlier ones; undo failures
  are reported in the error. The write loop, checks and rollback move to
  cheat_plan_apply() with a memory-ops backend.
- FW split as in CheatRunner: <= 8.20 mdbg only (copy-on-write, private);
  > 8.20 physical page only, no mdbg write fallback.
- A physical write must land inside the game's own module (eboot, handle
  0, or a module under /app0); Section / module_name / absolute addresses
  in a system library are refused.
- Turning a cheat off leaves alone a code cave that overlaps another
  enabled cheat of the same title.
- The game is held with SIGSTOP (not ptrace) while the writes land;
  SIGCONT on every path. kill(pid, 0) before every write.
- state_set runs under g_apply_lock; the sidecar is read whole (no 4 KiB
  cut) and an unreadable one is not rewritten.
- Self-test: fake memory backend failing on every Nth write, on the second
  page of a page-straddling write (turning on and off), on the undo
  itself, a mismatch, the game exiting mid-apply, and a shared cave.

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:47:20 -07:00
1 parent 02469c725b
commit fd9dce6479
4 files changed
+508 -115

No files matched your search

+32
View File
@@ -13,6 +13,7 @@
#include <stdint.h>
#define CHEAT_PLAN_MAX_BYTES 256
#define CHEAT_PLAN_MAX_LOCS 32 /* MAX_MEM_ENTRIES in cheats.c */
/* One location of a cheat, with the bytes read from the game before anything is written. */
typedef struct {
@@ -21,8 +22,25 @@ typedef struct {
uint8_t on[CHEAT_PLAN_MAX_BYTES];
uint8_t off[CHEAT_PLAN_MAX_BYTES];
uint8_t cur[CHEAT_PLAN_MAX_BYTES];
int keep; /* set by cheat_plan_apply: left as it is */
} cheat_loc_t;
/* A byte range another enabled cheat of the same title also writes. */
typedef struct {
intptr_t addr;
int len;
} cheat_range_t;
/* Where the writes go. On the console this is the game's memory; the self-test uses a fake. */
typedef struct {
void *ctx;
/* Write len bytes at addr and read them back. 0 = the bytes are there. A failure may leave
* the range partly written (a write split across two pages can fail on the second). */
int (*write)(void *ctx, intptr_t addr, const uint8_t *data, size_t len);
/* 1 while the target process still exists. Checked before every write. */
int (*alive)(void *ctx);
} cheat_mem_ops_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 */
@@ -51,6 +69,20 @@ int cheat_loc_is_cave(const cheat_loc_t *l);
* 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);
/* Apply or remove one cheat whose locations have `cur` filled in (read from the game before
* anything is written). All or nothing:
* - turning off, a code cave that overlaps one of `keep` (another cheat that is still on and
* shares the cave) is left alone;
* - any other location holding neither its cheat nor its original bytes refuses the whole
* cheat before anything is written;
* - writes go in cheat_plan_order(), skipping locations already in the wanted state;
* - if a write fails (or the game is gone), the failing location and every one written
* before it are put back to `cur`, newest first, and err says whether that worked.
* Returns 0 when the cheat is in the asked-for state. */
int cheat_plan_apply(cheat_loc_t *locs, int n, int turn_on,
const cheat_range_t *keep, int n_keep,
const cheat_mem_ops_t *ops, char *err, size_t err_cap);
/* 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);
+236 -115
View File
@@ -26,6 +26,7 @@
#include <ps5/kernel.h>
#include <ctype.h>
#include <stddef.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
@@ -371,9 +372,20 @@ static void invalidate_rg_cache(void) {
* 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. */
* Which path, by firmware (the same split CheatRunner uses):
* - FW <= 8.20: mdbg (the debugger syscall). It writes copy-on-write, so the change stays
* private to the game.
* - FW > 8.20: the physical page. mdbg's write fails there (measured on 13.60), so there is
* no fallback: a failed physical write is a failed write. A physical write changes the page
* for EVERY process that maps it, so it is only allowed inside the game's own code: its
* eboot (dynlib handle 0) or a module loaded from its /app0. A Section, module_name or
* absolute address that lands in a system library is refused. */
static int use_mdbg(void) {
static int fw = 0;
if (!fw) fw = (int)(kernel_get_fw_version() >> 16);
return fw <= 0x820;
}
static int phys_copy(pid_t pid, intptr_t addr, void *buf, size_t len, int write) {
if (!KERNEL_ADDRESS_DMAP_BASE) return -1;
@@ -397,32 +409,90 @@ static int phys_copy(pid_t pid, intptr_t addr, void *buf, size_t len, int write)
return 0;
}
static int game_mem_read(pid_t pid, intptr_t addr, uint8_t *out, size_t len) {
if (len == 0) return 0;
if (!cheat_addr_ok(addr, (int)len)) return -1;
if (phys_copy(pid, addr, out, len, 0) == 0) return 0;
static int mdbg_copy(pid_t pid, intptr_t addr, void *buf, size_t len, int write) {
/* 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);
int rc = write ? mdbg_copyin(pid, buf, addr, len) : mdbg_copyout(pid, addr, buf, len);
pthread_mutex_unlock(&kernel_rw_lock);
return rc == 0 ? 0 : -1;
}
/* 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) {
static int game_mem_read(pid_t pid, intptr_t addr, uint8_t *out, size_t len) {
if (len == 0) return 0;
if (!cheat_addr_ok(addr, (int)len)) return -1;
if (use_mdbg()) return mdbg_copy(pid, addr, out, len, 0);
if (phys_copy(pid, addr, out, len, 0) == 0) return 0;
return mdbg_copy(pid, addr, out, len, 0); /* a read changes nothing */
}
/* The first fields of the kernel's dynlib object (third_party/sdk-crt/kernel.h dynlib_obj_t). */
typedef struct {
uint64_t next;
uint64_t path;
uint64_t unknown0[2];
uint32_t refcount;
uint64_t handle;
uint64_t mapbase;
uint64_t mapsize;
} dynlib_head_t;
_Static_assert(offsetof(dynlib_head_t, handle) == 0x28, "dynlib_obj_t.handle");
_Static_assert(offsetof(dynlib_head_t, mapbase) == 0x30, "dynlib_obj_t.mapbase");
/* 1 when [addr, addr+len) lies inside one module of the game that belongs to the game itself:
* the eboot (handle 0) or a module whose path is under its app0. */
static int range_in_game_module(pid_t pid, intptr_t addr, size_t len, char *why, size_t why_cap) {
int ok = 0;
char path[256] = "";
pthread_mutex_lock(&kernel_rw_lock);
unsigned long kproc = (unsigned long)kernel_get_proc(pid);
unsigned long obj = 0;
if (kproc && kernel_copyout(kproc + 0x3e8, &obj, sizeof(obj)) == 0) {
/* The list head holds the first object's address; each object starts with `next`. */
for (int guard = 0; guard < 1024; guard++) {
if (kernel_copyout(obj, &obj, sizeof(obj)) != 0 || !obj) break;
dynlib_head_t h;
if (kernel_copyout(obj, &h, sizeof(h)) != 0) break;
uint64_t a = (uint64_t)addr;
if (a >= h.mapbase && a + len <= h.mapbase + h.mapsize) {
path[0] = '\0';
if (h.path) (void)kernel_copyout(h.path, path, sizeof(path) - 1);
path[sizeof(path) - 1] = '\0';
ok = (h.handle == 0) || strstr(path, "/app0/") != NULL;
if (why && !ok) snprintf(why, why_cap, "%s", path[0] ? path : "a system module");
break;
}
}
}
pthread_mutex_unlock(&kernel_rw_lock);
if (why && !ok && !why[0]) snprintf(why, why_cap, "no module of the game");
return ok;
}
typedef struct {
pid_t pid;
} game_mem_ctx_t;
/* cheat_mem_ops_t.write: 0 when the bytes were written and read back equal. */
static int game_mem_write(void *opaque, intptr_t addr, const uint8_t *data, size_t len) {
pid_t pid = ((game_mem_ctx_t *)opaque)->pid;
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;
}
uint8_t back[CHEAT_PLAN_MAX_BYTES];
if (game_mem_read(pid, addr, back, len) != 0) return -2;
if (use_mdbg()) {
if (mdbg_copy(pid, addr, (void *)data, len, 1) != 0) return -1;
if (mdbg_copy(pid, addr, back, len, 0) != 0) return -2;
} else {
if (phys_copy(pid, addr, (void *)data, len, 1) != 0) return -1;
if (phys_copy(pid, addr, back, len, 0) != 0) return -2;
}
return memcmp(back, data, len) == 0 ? 0 : -3;
}
static int game_alive(void *opaque) {
pid_t pid = ((game_mem_ctx_t *)opaque)->pid;
return !(kill(pid, 0) != 0 && errno == ESRCH);
}
/* ── Module base resolution ──────────────────────────────────────── */
/* Resolve the base address of a named module in the target process.
@@ -1020,18 +1090,34 @@ static void state_path(const char *title_id, char *out, size_t cap) {
}
/* 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) {
* title, in find_cheat_files() order — the same numbering the mod list and toggles use.
* Every key fits: 1024 mods at most (64 per file x 16 files), about 11 KiB. */
#define STATE_FILE_MAX (64 * 1024)
/* The whole sidecar as a NUL-terminated heap string ("" when there is none), or NULL when it
* exists but cannot be read in full. Caller frees. */
static char *state_read(const char *title_id) {
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 -1;
ssize_t rd = read(fd, buf, cap - 1);
if (fd < 0) return errno == ENOENT ? strdup("") : NULL;
struct stat st;
if (fstat(fd, &st) != 0 || st.st_size < 0 || st.st_size > STATE_FILE_MAX) {
close(fd);
return NULL;
}
size_t size = (size_t)st.st_size;
char *buf = (char *)malloc(size + 1);
if (!buf) { close(fd); return NULL; }
size_t got = 0;
while (got < size) {
ssize_t rd = read(fd, buf + got, size - got);
if (rd <= 0) break;
got += (size_t)rd;
}
close(fd);
if (rd <= 0) { buf[0] = '\0'; return -1; }
buf[rd] = '\0';
return 0;
buf[got] = '\0';
return buf;
}
static int state_has(const char *buf, int flat) {
@@ -1047,28 +1133,30 @@ static int state_has(const char *buf, int flat) {
/* 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;
char *buf = state_read(title_id);
if (!buf) return;
for (int i = 0; i < cf->mod_count; i++)
if (state_has(buf, flat_base + i)) cf->mods[i].enabled = 1;
free(buf);
}
/* 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';
* marked the first file's mods instead, and the watcher re-applied those on the next launch.)
* Call with g_apply_lock held. Returns 0 on success; a sidecar it cannot read is left alone
* rather than rewritten without its other keys. */
static int state_set(const char *title_id, int flat, int on) {
char *buf = state_read(title_id);
if (!buf) return -1;
char path[MAX_CHEAT_FILEPATH];
state_path(title_id, path, sizeof(path));
char tmp[MAX_CHEAT_FILEPATH + 8];
snprintf(tmp, sizeof(tmp), "%s.tmp", path);
FILE *f = fopen(tmp, "w");
if (!f) return;
if (!f) { free(buf); return -1; }
fprintf(f, "{");
int first = 1;
/* 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;
@@ -1076,28 +1164,20 @@ static void state_set(const char *title_id, int flat, int on) {
first = 0;
}
fprintf(f, "}");
if (fclose(f) == 0) {
rename(tmp, path);
} else {
free(buf);
if (fclose(f) != 0) {
unlink(tmp);
return -1;
}
return rename(tmp, path) == 0 ? 0 : -1;
}
/* ── Core: apply a single mod ────────────────────────────────────── */
/* 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) {
/* Parse a mod's locations and work out their addresses (no memory access). Returns the number
* of locations, or -1 with err set. */
static int resolve_mod_locs(pid_t pid, intptr_t base, const cheat_mod_t *mod, cheat_loc_t *locs,
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");
@@ -1113,50 +1193,34 @@ static int apply_mod_locked(pid_t pid, intptr_t base, cheat_mod_t *mod,
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 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 < n && rc == 0; i++) {
mem_entry_t *e = &mod->mem[i];
for (int i = 0; i < n; i++) {
const mem_entry_t *e = &mod->mem[i];
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;
return -1;
}
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;
return -1;
}
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;
return -1;
}
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;
return -1;
}
l->len = on_len;
if (e->absolute) {
l->addr = (intptr_t)off;
} else if (e->section > 0) {
@@ -1164,56 +1228,68 @@ static int apply_mod_locked(pid_t pid, intptr_t base, cheat_mod_t *mod,
if (sec_base <= 0) {
if (err) snprintf(err, err_cap, "location %d: module %d is not loaded",
i + 1, e->section);
rc = -1;
break;
return -1;
}
l->addr = (intptr_t)((uint64_t)sec_base + off);
} else {
l->addr = (intptr_t)((uint64_t)base + off);
}
if (!cheat_addr_ok(l->addr, l->len)) {
if (err) snprintf(err, err_cap, "location %d: address 0x%lx is not a game address",
i + 1, (unsigned long)l->addr);
return -1;
}
}
return n;
}
/* Apply or remove one cheat, all of its locations or none of them (cheat_plan_apply does the
* checking, ordering and rollback). `keep` lists the locations of the title's other enabled
* cheats, so turning this one off leaves a code cave they share alone.
*
* The game is held with SIGSTOP while the writes land (not ptrace: no attach, no protection
* change), so no thread runs an instruction half overwritten. SIGCONT follows on every path.
*
* 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,
const cheat_range_t *keep, int n_keep, char *err, size_t err_cap) {
/* "button" type always writes "on" regardless of turn_on */
int write_on = (strcmp(mod->type, "button") == 0) ? 1 : turn_on;
cheat_loc_t *locs = (cheat_loc_t *)calloc(MAX_MEM_ENTRIES, sizeof(cheat_loc_t));
if (!locs) {
if (err) snprintf(err, err_cap, "out of memory");
return -1;
}
int n = resolve_mod_locs(pid, base, mod, locs, err, err_cap);
int rc = n > 0 ? 0 : -1;
for (int i = 0; i < n && rc == 0; i++) {
cheat_loc_t *l = &locs[i];
if (!use_mdbg()) {
char why[160] = "";
if (!range_in_game_module(pid, l->addr, (size_t)l->len, why, sizeof(why))) {
if (err) snprintf(err, err_cap,
"location %d (0x%lx) is outside the game's own code (%s); "
"not written",
i + 1, (unsigned long)l->addr, why);
rc = -1;
break;
}
}
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;
}
}
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 (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);
}
game_mem_ctx_t ctx = { .pid = pid };
cheat_mem_ops_t ops = { .ctx = &ctx, .write = game_mem_write, .alive = game_alive };
int stopped = (kill(pid, SIGSTOP) == 0);
rc = cheat_plan_apply(locs, n, write_on, keep, n_keep, &ops, err, err_cap);
if (stopped) (void)kill(pid, SIGCONT);
}
free(locs);
@@ -1221,13 +1297,14 @@ static int apply_mod_locked(pid_t pid, intptr_t base, cheat_mod_t *mod,
}
/* 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. */
* two applies of overlapping cheats must not interleave their reads and writes. The toggle
* state file is written under the same lock. */
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);
int rc = apply_mod_locked(pid, base, mod, turn_on, NULL, 0, err, err_cap);
pthread_mutex_unlock(&g_apply_lock);
return rc;
}
@@ -1526,6 +1603,36 @@ int cheats_list_mods(const char *title_id, char *buf, size_t cap,
return 0;
}
#define KEEP_RANGES_MAX 1024
/* Every location of the title's enabled cheats other than `skip_flat`, as byte ranges. */
static int collect_enabled_ranges(pid_t pid, intptr_t base, const char *title_id, int skip_flat,
cheat_range_t *out, int max) {
found_file_t files[16];
int n = find_cheat_files(title_id, files, 16, 0);
cheat_file_t *cf = (cheat_file_t *)malloc(sizeof(cheat_file_t));
cheat_loc_t *locs = (cheat_loc_t *)calloc(MAX_MEM_ENTRIES, sizeof(cheat_loc_t));
int count = 0, flat = 0;
for (int fi = 0; cf && locs && fi < n; fi++) {
if (load_cheat_file(files[fi].path, files[fi].format, cf) != 0) continue;
cheats_load_state(title_id, cf, flat);
for (int mi = 0; mi < cf->mod_count; mi++, flat++) {
if (flat == skip_flat || !cf->mods[mi].enabled) continue;
intptr_t mb = base;
if (cf->mods[mi].module_name[0]) mb = resolve_module_base(pid, cf->mods[mi].module_name);
int nl = resolve_mod_locs(pid, mb, &cf->mods[mi], locs, NULL, 0);
for (int k = 0; k < nl && count < max; k++) {
out[count].addr = locs[k].addr;
out[count].len = locs[k].len;
count++;
}
}
}
free(locs);
free(cf);
return count;
}
/* Toggle a mod by flat index. */
int cheats_toggle(const char *title_id, int mod_index, int turn_on,
char *err, size_t err_cap) {
@@ -1624,14 +1731,28 @@ int cheats_toggle(const char *title_id, int mod_index, int turn_on,
return -1;
}
int rc = apply_mod(pid, mod_base, mod, turn_on, err, err_cap);
/* Turning off: the title's other enabled cheats may share a code cave with this one, and
* restoring the cave's padding would leave their jumps running into int3. */
cheat_range_t *keep = NULL;
int n_keep = 0;
if (!turn_on) {
keep = (cheat_range_t *)calloc(KEEP_RANGES_MAX, sizeof(cheat_range_t));
if (keep) n_keep = collect_enabled_ranges(pid, base, title_id, mod_index, keep, KEEP_RANGES_MAX);
}
pthread_mutex_lock(&g_apply_lock);
int rc = apply_mod_locked(pid, mod_base, mod, turn_on, keep, n_keep, err, err_cap);
if (rc == 0 && strcmp(mod->type, "button") != 0) {
/* Update persisted state (only for checkbox type) */
mod->enabled = turn_on;
if (state_set(title_id, mod_index, turn_on) != 0)
fprintf(stderr, "[cheats] could not record %s #%d as %s\n", title_id, mod_index,
turn_on ? "on" : "off");
}
pthread_mutex_unlock(&g_apply_lock);
free(keep);
if (rc == 0) {
/* Update persisted state (only for checkbox type) */
if (strcmp(mod->type, "button") != 0) {
mod->enabled = turn_on;
state_set(title_id, mod_index, turn_on);
}
/* Confirm on the console.
*
+77
View File
@@ -3,6 +3,7 @@
#include "cheats_plan.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -93,3 +94,79 @@ int cheat_addr_ok(intptr_t addr, int len) {
uint64_t end = a + (uint64_t)len;
return a >= 0x10000ULL && end > a && end <= 0x800000000000ULL;
}
static int overlaps(intptr_t a, int alen, intptr_t b, int blen) {
return a < b + (intptr_t)blen && b < a + (intptr_t)alen;
}
int cheat_plan_apply(cheat_loc_t *locs, int n, int turn_on,
const cheat_range_t *keep, int n_keep,
const cheat_mem_ops_t *ops, char *err, size_t err_cap) {
if (err && err_cap) err[0] = '\0';
if (n <= 0 || n > CHEAT_PLAN_MAX_LOCS) {
if (err) snprintf(err, err_cap, "cheat has no usable locations");
return -1;
}
for (int i = 0; i < n; i++) {
cheat_loc_t *l = &locs[i];
l->keep = 0;
if (!turn_on && cheat_loc_is_cave(l)) {
for (int k = 0; k < n_keep; k++)
if (overlaps(l->addr, l->len, keep[k].addr, keep[k].len)) { l->keep = 1; break; }
}
if (l->keep) continue;
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);
return -1;
}
}
int order[CHEAT_PLAN_MAX_LOCS];
int done[CHEAT_PLAN_MAX_LOCS];
int n_done = 0;
cheat_plan_order(locs, n, turn_on, order);
int failed = -1, why = 0;
for (int k = 0; k < n; k++) {
cheat_loc_t *l = &locs[order[k]];
if (l->keep) continue;
const uint8_t *want = turn_on ? l->on : l->off;
if (memcmp(l->cur, want, (size_t)l->len) == 0) continue; /* already there */
if (!ops->alive(ops->ctx)) { why = -100; failed = -2; break; }
int wrc = ops->write(ops->ctx, l->addr, want, (size_t)l->len);
if (wrc != 0) { failed = order[k]; why = wrc; break; }
done[n_done++] = order[k];
}
if (failed == -1) return 0;
if (failed == -2) {
/* The game exited mid-apply: nothing left to put back. */
if (err) snprintf(err, err_cap, "no game is currently running");
return -1;
}
/* The failing location may be half written (a split write that failed on its second page,
* or a read-back mismatch): put it back too, then the earlier ones, newest first. */
int undo_fail = 0;
if (ops->write(ops->ctx, locs[failed].addr, locs[failed].cur, (size_t)locs[failed].len) != 0)
undo_fail++;
for (int k = n_done - 1; k >= 0; k--) {
cheat_loc_t *l = &locs[done[k]];
if (ops->write(ops->ctx, l->addr, l->cur, (size_t)l->len) != 0) undo_fail++;
}
if (err) {
if (undo_fail)
snprintf(err, err_cap,
"write failed at 0x%lx (rc=%d); %d of %d location(s) could not be put back "
"- restart the game",
(unsigned long)locs[failed].addr, why, undo_fail, n_done + 1);
else
snprintf(err, err_cap, "write failed at 0x%lx (rc=%d); the game was left as it was",
(unsigned long)locs[failed].addr, why);
}
return -1;
}
+163
View File
@@ -161,12 +161,175 @@ static void test_addr_ok(void) {
CHECK(!cheat_addr_ok(0x400000, 0));
}
/* ── A fake game memory for cheat_plan_apply ───────────────────────── */
#define MOCK_BASE 0x400000
#define MOCK_SIZE 0x8000
#define MOCK_PAGE 0x1000
typedef struct {
uint8_t mem[MOCK_SIZE];
int calls; /* write calls so far */
int fail_call; /* this call (1-based) fails; 0 = never */
int fail_from; /* this call and every later one fails; 0 = never */
int split_fail; /* the failing call writes its first page, then fails */
int alive_until; /* alive() turns 0 after this many write calls; 0 = always alive */
} mock_t;
static int mock_write(void *opaque, intptr_t addr, const uint8_t *data, size_t len) {
mock_t *m = (mock_t *)opaque;
m->calls++;
int fail = (m->fail_call && m->calls == m->fail_call) || (m->fail_from && m->calls >= m->fail_from);
size_t off = (size_t)(addr - MOCK_BASE);
if (off + len > MOCK_SIZE) return -1;
size_t done = 0;
while (done < len) { /* page by page, like the physical-page path */
size_t page_end = ((off + done) / MOCK_PAGE + 1) * MOCK_PAGE;
size_t chunk = page_end - (off + done);
if (chunk > len - done) chunk = len - done;
if (fail && (!m->split_fail || done > 0)) return -1;
memcpy(m->mem + off + done, data + done, chunk);
done += chunk;
}
return fail ? -1 : 0;
}
static int mock_alive(void *opaque) {
mock_t *m = (mock_t *)opaque;
return !m->alive_until || m->calls < m->alive_until;
}
/* A synthetic hook-and-cave cheat in the fake memory: 3 caves (int3 padding), 2 hooks, and one
* cave that straddles the page boundary at 0x402000. */
static const raw_t SYN[] = {
{"0100", "4C8B184D8B5B30E9E4060000", "CCCCCCCCCCCCCCCCCCCCCCCC"},
{"5000", "E97880FEFF", "8B4808EB02"},
{"1FF8", "450FB65370E9F6050000AABBCCDD", "CCCCCCCCCCCCCCCCCCCCCCCCCCCC"}, /* crosses 0x2000 */
{"5100", "E9588FFEFF", "8B4808EB02"},
{"0200", "C3", "CC"},
};
#define N_SYN ((int)(sizeof SYN / sizeof SYN[0]))
static void mock_boot(mock_t *m, cheat_loc_t *locs) {
memset(m, 0, sizeof *m);
memset(m->mem, 0x90, sizeof m->mem);
build(SYN, N_SYN, MOCK_BASE, locs);
for (int i = 0; i < N_SYN; i++)
memcpy(m->mem + (locs[i].addr - MOCK_BASE), locs[i].off, (size_t)locs[i].len);
}
static void mock_read_cur(const mock_t *m, cheat_loc_t *locs, int n) {
for (int i = 0; i < n; i++)
memcpy(locs[i].cur, m->mem + (locs[i].addr - MOCK_BASE), (size_t)locs[i].len);
}
static int all_in_state(const mock_t *m, const cheat_loc_t *locs, int n, int on) {
for (int i = 0; i < n; i++)
if (memcmp(m->mem + (locs[i].addr - MOCK_BASE), on ? locs[i].on : locs[i].off,
(size_t)locs[i].len) != 0)
return 0;
return 1;
}
static void test_apply_mock(void) {
static mock_t m, snap;
cheat_loc_t locs[N_SYN];
cheat_mem_ops_t ops = {&m, mock_write, mock_alive};
char err[256];
/* On, then off. */
mock_boot(&m, locs);
mock_read_cur(&m, locs, N_SYN);
CHECK(cheat_plan_apply(locs, N_SYN, 1, NULL, 0, &ops, err, sizeof err) == 0);
CHECK(all_in_state(&m, locs, N_SYN, 1));
mock_read_cur(&m, locs, N_SYN);
CHECK(cheat_plan_apply(locs, N_SYN, 0, NULL, 0, &ops, err, sizeof err) == 0);
CHECK(all_in_state(&m, locs, N_SYN, 0));
/* The Nth write fails, for every N: everything is put back, including what was half done. */
for (int nth = 1; nth <= N_SYN; nth++) {
for (int split = 0; split <= 1; split++) {
mock_boot(&m, locs);
snap = m;
m.fail_call = nth;
m.split_fail = split;
mock_read_cur(&m, locs, N_SYN);
CHECK(cheat_plan_apply(locs, N_SYN, 1, NULL, 0, &ops, err, sizeof err) != 0);
CHECK(memcmp(m.mem, snap.mem, MOCK_SIZE) == 0);
CHECK(strstr(err, "left as it was") != NULL);
}
}
/* The page-straddling cave fails on its second page: torn, then restored. Caves go first in
* file order, so it is the 2nd write. */
mock_boot(&m, locs);
snap = m;
m.fail_call = 2;
m.split_fail = 1;
mock_read_cur(&m, locs, N_SYN);
CHECK(cheat_plan_apply(locs, N_SYN, 1, NULL, 0, &ops, err, sizeof err) != 0);
CHECK(memcmp(m.mem, snap.mem, MOCK_SIZE) == 0);
/* Same while turning off. */
mock_boot(&m, locs);
mock_read_cur(&m, locs, N_SYN);
CHECK(cheat_plan_apply(locs, N_SYN, 1, NULL, 0, &ops, err, sizeof err) == 0);
snap = m;
m.calls = 0;
m.fail_call = 4;
m.split_fail = 1;
mock_read_cur(&m, locs, N_SYN);
CHECK(cheat_plan_apply(locs, N_SYN, 0, NULL, 0, &ops, err, sizeof err) != 0);
CHECK(memcmp(m.mem, snap.mem, MOCK_SIZE) == 0);
/* The undo itself fails: said so, not "left as it was". */
mock_boot(&m, locs);
m.fail_from = 3;
mock_read_cur(&m, locs, N_SYN);
CHECK(cheat_plan_apply(locs, N_SYN, 1, NULL, 0, &ops, err, sizeof err) != 0);
CHECK(strstr(err, "could not be put back") != NULL);
/* A mismatch anywhere: nothing written at all. */
mock_boot(&m, locs);
m.mem[locs[3].addr - MOCK_BASE] = 0x00;
snap = m;
mock_read_cur(&m, locs, N_SYN);
CHECK(cheat_plan_apply(locs, N_SYN, 1, NULL, 0, &ops, err, sizeof err) != 0);
CHECK(m.calls == 0 && memcmp(m.mem, snap.mem, MOCK_SIZE) == 0);
/* The game exits mid-apply: stops writing. */
mock_boot(&m, locs);
m.alive_until = 2;
mock_read_cur(&m, locs, N_SYN);
CHECK(cheat_plan_apply(locs, N_SYN, 1, NULL, 0, &ops, err, sizeof err) != 0);
CHECK(m.calls == 2);
CHECK(strstr(err, "no game") != NULL);
/* Turning off with another enabled cheat sharing the first cave: that cave keeps its code,
* everything else goes back. Even when the other cheat's bytes differ from ours there. */
mock_boot(&m, locs);
mock_read_cur(&m, locs, N_SYN);
CHECK(cheat_plan_apply(locs, N_SYN, 1, NULL, 0, &ops, err, sizeof err) == 0);
m.mem[locs[0].addr - MOCK_BASE + 2] = 0x77; /* the other cheat's version of the cave */
uint8_t cave_before[32];
memcpy(cave_before, m.mem + (locs[0].addr - MOCK_BASE), (size_t)locs[0].len);
cheat_range_t keep[1] = {{locs[0].addr + 4, 2}};
mock_read_cur(&m, locs, N_SYN);
CHECK(cheat_plan_apply(locs, N_SYN, 0, keep, 1, &ops, err, sizeof err) == 0);
CHECK(memcmp(m.mem + (locs[0].addr - MOCK_BASE), cave_before, (size_t)locs[0].len) == 0);
CHECK(all_in_state(&m, locs + 1, N_SYN - 1, 0));
/* A hook overlapping `keep` is still restored (only caves are shared). */
CHECK(locs[1].keep == 0);
}
int main(void) {
test_parse();
test_state();
test_order_max_resources();
test_order_mc4_zero_cave();
test_addr_ok();
test_apply_mock();
if (failures) {
fprintf(stderr, "cheats_plan_selftest: %d failure(s)\n", failures);
return 1;