mirror of
https://github.com/phantomptr/ps5upload.git
synced 2026-10-19 02:20:02 -05:00
cheats: drop SIGSTOP, lock fixes, private-memory rule
- No SIGSTOP/SIGCONT: stopping the game froze games for CheatRunner and SIGSTOP also reaches SceSysCore as SIGCHLD. The game keeps running; the write order (caves before hooks on, the reverse off) is what keeps it from running half a cheat. - Physical writes (FW > 8.20), as CheatRunner does them: a range inside a loaded module must lie wholly inside the game's eboot or an /app0 module (system libraries and module edges are refused); a range in no module (heap, direct memory: absolute/data cheats) is gated only by a canonical user address and a present, readable page, with no vm_map entry walk (that walk panicked kernels on PS4-BC and special entries). - The other enabled cheats' ranges are collected under g_apply_lock, and an incomplete list (alloc, unreadable file or state, unresolvable mod, too many ranges) refuses the turn-off with nothing written. - A short read of the state file is a failed read, so state_set never rewrites it with keys missing. - Self-test: the keep range now really overlaps the hook, and the hook is checked restored. 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
fd9dce6479
commit
c149a6ffe1
2 files changed
+98
-45
No files matched your search
+90
-41
@@ -438,34 +438,60 @@ typedef struct {
|
||||
_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;
|
||||
/* Where a physical write (FW > 8.20) may go.
|
||||
*
|
||||
* A physical write lands in the page every process mapping it shares, so a range inside a
|
||||
* loaded module must be inside one of the GAME's modules: its eboot (handle 0) or a module
|
||||
* whose path is under its /app0. A Section, module_name or absolute address that lands in a
|
||||
* system library (/system/..., /system_ex/...) is refused.
|
||||
*
|
||||
* A range in no module at all (heap, flexible/direct memory, the stack: what absolute and
|
||||
* data cheats point at) is written like CheatRunner writes it: the only gates are a canonical
|
||||
* user address (cheat_addr_ok) and a page that is present and readable through the same
|
||||
* page-table walk. CheatRunner does not inspect the vm_map entry for those either, on purpose:
|
||||
* walking vm_map entries (kernel_get_vmem_protection) panicked the kernel on PS4-BC games and on
|
||||
* PS5 games with special vmem entry types, so we do not either.
|
||||
*
|
||||
* Returns 1 when the write may go ahead; 0 with `why` set when it may not. */
|
||||
static int phys_write_allowed(pid_t pid, intptr_t addr, size_t len, char *why, size_t why_cap) {
|
||||
int verdict = 1; /* no module overlaps: allowed (see above) */
|
||||
int walked = 0;
|
||||
char path[256] = "";
|
||||
uint64_t a = (uint64_t)addr, e = a + len;
|
||||
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) {
|
||||
walked = 1;
|
||||
/* 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;
|
||||
if (kernel_copyout(obj, &obj, sizeof(obj)) != 0) { walked = 0; break; }
|
||||
if (!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;
|
||||
if (kernel_copyout(obj, &h, sizeof(h)) != 0) { walked = 0; break; }
|
||||
uint64_t ms = h.mapbase, me = h.mapbase + h.mapsize;
|
||||
if (!(a < me && ms < e)) continue; /* no overlap with this module */
|
||||
path[0] = '\0';
|
||||
if (h.path) (void)kernel_copyout(h.path, path, sizeof(path) - 1);
|
||||
path[sizeof(path) - 1] = '\0';
|
||||
int game_module = (h.handle == 0) || strstr(path, "/app0/") != NULL;
|
||||
if (!game_module) {
|
||||
if (why) snprintf(why, why_cap, "inside %s", path[0] ? path : "a system module");
|
||||
verdict = 0;
|
||||
} else if (!(a >= ms && e <= me)) {
|
||||
if (why) snprintf(why, why_cap, "crosses the edge of %s",
|
||||
path[0] ? path : "the game's module");
|
||||
verdict = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&kernel_rw_lock);
|
||||
if (why && !ok && !why[0]) snprintf(why, why_cap, "no module of the game");
|
||||
return ok;
|
||||
if (!walked) {
|
||||
if (why) snprintf(why, why_cap, "the game's module list could not be read");
|
||||
return 0;
|
||||
}
|
||||
return verdict;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
@@ -1116,6 +1142,10 @@ static char *state_read(const char *title_id) {
|
||||
got += (size_t)rd;
|
||||
}
|
||||
close(fd);
|
||||
if (got < size) { /* a short read: never hand back a state with keys missing */
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
buf[got] = '\0';
|
||||
return buf;
|
||||
}
|
||||
@@ -1247,8 +1277,10 @@ static int resolve_mod_locs(pid_t pid, intptr_t base, const cheat_mod_t *mod, ch
|
||||
* 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.
|
||||
* The game keeps running while the writes land, as with CheatRunner: stopping it (ptrace, or
|
||||
* SIGSTOP, which also tells SceSysCore through SIGCHLD) froze games. What keeps a thread from
|
||||
* running half a cheat is the write order: caves before the jumps into them when turning on,
|
||||
* the jumps restored before their caves when turning off.
|
||||
*
|
||||
* 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,
|
||||
@@ -1268,10 +1300,10 @@ static int apply_mod_locked(pid_t pid, intptr_t base, cheat_mod_t *mod, int turn
|
||||
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 (!phys_write_allowed(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",
|
||||
"location %d (0x%lx) is not the game's own memory (%s); "
|
||||
"nothing was written",
|
||||
i + 1, (unsigned long)l->addr, why);
|
||||
rc = -1;
|
||||
break;
|
||||
@@ -1287,9 +1319,7 @@ static int apply_mod_locked(pid_t pid, intptr_t base, cheat_mod_t *mod, int turn
|
||||
if (rc == 0) {
|
||||
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);
|
||||
@@ -1605,31 +1635,42 @@ int cheats_list_mods(const char *title_id, char *buf, size_t cap,
|
||||
|
||||
#define KEEP_RANGES_MAX 1024
|
||||
|
||||
/* Every location of the title's enabled cheats other than `skip_flat`, as byte ranges. */
|
||||
/* Every location of the title's enabled cheats other than `skip_flat`, as byte ranges.
|
||||
* Returns the count, or -1 when the list could not be made complete (out of memory, a cheat
|
||||
* file or the state that cannot be read, an enabled cheat whose locations cannot be resolved,
|
||||
* more ranges than fit): the caller then refuses, because a missing range could be a shared
|
||||
* cave that would be wiped under another cheat's jump. Call with g_apply_lock held. */
|
||||
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);
|
||||
char *state = state_read(title_id);
|
||||
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++;
|
||||
int count = -1;
|
||||
if (state && cf && locs) {
|
||||
count = 0;
|
||||
int flat = 0;
|
||||
for (int fi = 0; fi < n && count >= 0; fi++) {
|
||||
if (load_cheat_file(files[fi].path, files[fi].format, cf) != 0) { count = -1; break; }
|
||||
for (int mi = 0; mi < cf->mod_count && count >= 0; mi++, flat++) {
|
||||
if (flat == skip_flat || !state_has(state, flat)) 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);
|
||||
if (nl <= 0 || count + nl > max) { count = -1; break; }
|
||||
for (int k = 0; k < nl; k++) {
|
||||
out[count].addr = locs[k].addr;
|
||||
out[count].len = locs[k].len;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
free(locs);
|
||||
free(cf);
|
||||
free(state);
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -1735,13 +1776,21 @@ int cheats_toggle(const char *title_id, int mod_index, int turn_on,
|
||||
* restoring the cave's padding would leave their jumps running into int3. */
|
||||
cheat_range_t *keep = NULL;
|
||||
int n_keep = 0;
|
||||
int rc = 0;
|
||||
pthread_mutex_lock(&g_apply_lock);
|
||||
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);
|
||||
n_keep = keep ? collect_enabled_ranges(pid, base, title_id, mod_index, keep,
|
||||
KEEP_RANGES_MAX)
|
||||
: -1;
|
||||
if (n_keep < 0) {
|
||||
if (err) snprintf(err, err_cap,
|
||||
"could not check this title's other enabled cheats for shared "
|
||||
"code; nothing was written");
|
||||
rc = -1;
|
||||
}
|
||||
}
|
||||
|
||||
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) 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;
|
||||
|
||||
@@ -314,13 +314,17 @@ static void test_apply_mock(void) {
|
||||
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}};
|
||||
/* The second range overlaps the hook at locs[1]: only caves are shared, so the hook must
|
||||
* still be restored. */
|
||||
cheat_range_t keep[2] = {{locs[0].addr + 4, 2}, {locs[1].addr + 1, 2}};
|
||||
CHECK(keep[1].addr < locs[1].addr + locs[1].len && locs[1].addr < keep[1].addr + keep[1].len);
|
||||
mock_read_cur(&m, locs, N_SYN);
|
||||
CHECK(cheat_plan_apply(locs, N_SYN, 0, keep, 1, &ops, err, sizeof err) == 0);
|
||||
CHECK(cheat_plan_apply(locs, N_SYN, 0, keep, 2, &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[0].keep == 1);
|
||||
CHECK(locs[1].keep == 0);
|
||||
CHECK(memcmp(m.mem + (locs[1].addr - MOCK_BASE), locs[1].off, (size_t)locs[1].len) == 0);
|
||||
CHECK(all_in_state(&m, locs + 1, N_SYN - 1, 0));
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
|
||||
Reference in new issue
Block a user