Guard static text dispatch with live RAM divergence

Packed games can rewrite code over their own loaded EXE text after static recompilation. A dirty-page bit alone is too coarse for dispatch decisions: it prevents stale native execution, but it also cannot distinguish data writes in a text page from instruction bytes that actually diverged from the compiled image.\n\nRegister the boot EXE image as a reference and let memory writes mark text pages whose bytes differ from that image. Static game dispatch now asks whether the target remains native-safe; diverged pages fall through to execute live RAM through the dirty interpreter, while matching code bytes can still use the compiled path.\n\nConstraint: Runtime-installed or rewritten RAM code must execute the program's own instructions, not HLE or stale static code\nRejected: Keep the existing dirty-page-only gate | too coarse for packed/self-modifying main text and data writes sharing code pages\nConfidence: medium\nScope-risk: moderate\nDirective: Do not bypass dirty_ram_text_native_ok() at game compiled dispatch sites without proving RAM still matches the compiled image\nTested: Local Tekken 3 branch using this mechanism reaches playable fights; clean upstream worktree compiles changed main/memory/dirty files before hitting an unrelated existing macOS overlay_loader LONG/InterlockedExchange build error\nNot-tested: Full upstream macOS build completion; non-macOS build matrix
This commit is contained in:
ai-tdd-labs
2026-07-04 16:41:29 +02:00
parent 010a281b0c
commit 685f38dacb
5 changed files with 124 additions and 19 deletions
+5
View File
@@ -87,6 +87,11 @@ uint32_t dirty_ram_get_bitmap(void);
uint32_t dirty_ram_get_bitmap_word(uint32_t word_index);
uint32_t dirty_ram_get_bitmap_word_count(void);
void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len);
void dirty_ram_register_text_image(uint32_t phys_lo, const uint8_t *bytes,
uint32_t len);
int dirty_ram_text_native_ok(uint32_t phys);
uint64_t dirty_ram_text_native_blocked(void);
uint32_t dirty_ram_text_diverged_pages(void);
/* Counters for visibility / TCP debug. Increment in interpreter; expose
* via debug_server.c if helpful. */
+8 -15
View File
@@ -1010,12 +1010,10 @@ int dirty_ram_xprobe_json(char *out, int cap) {
#ifdef PSX_HAS_GAME_DISPATCH
static int interp_enter_compiled(CPUState *cpu, uint32_t target) {
if (target == 0x8001A954u) site_note(&g_site_interp);
/* Decline if the target page is dirty: an overlay overwrote it after the
* game-start baseline, so the compiled function is STALE. Returning 0 lets the
* JAL/JALR handler fall through to is_local_dirty_target -> local-flow interp of
* the live overlay, instead of running stale compiled code (the same staleness
* the dirty_ram_dispatch_inner gate guards). Clean targets run compiled. */
if (dirty_ram_is_dirty(target & 0x1FFFFFFFu)) return 0;
/* Decline when the target page no longer matches the static game image.
* Returning 0 lets the JAL/JALR handler fall through to local-flow interp
* of the live RAM bytes instead of running stale compiled code. */
if (!dirty_ram_text_native_ok(target & 0x1FFFFFFFu)) return 0;
if (psx_mixed_owner_enabled()
&& interp_host_stack_used() > psx_mixed_stack_watermark()) {
cpu->pc = target;
@@ -2062,15 +2060,10 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
#ifdef PSX_HAS_GAME_DISPATCH
xprobe_event(cpu->gpr[31], XOP_DD, XSITE_DD, addr, 0u, cpu->gpr[29], cpu->gpr[31], 0);
/* Run the statically-compiled game function ONLY for a CLEAN page (RAM matches
* the compiled image). A dirty page in the game-text region means an overlay
* overwrote it after the game-start baseline (dirty_ram_clear_image_baseline),
* so the compiled function is STALE and we must fall through to interpret the
* live overlay. Without this gate the loader thread's psx_dispatch(0x8001DB38)
* ran the stale compiled func_8001DB38 instead of the START.BIN-loader overlay
* (Tomba 2 Whoopee-Camp splash). Clean text still runs compiled (the fast path,
* unchanged for non-overlaying games once their text is baselined). */
if (!dirty_ram_is_dirty(phys)) {
/* Run the statically-compiled game function only while the target is still
* native-safe. Dirty overlay pages and pages whose text bytes diverged from
* the original EXE image fall through to interpret the live RAM bytes. */
if (dirty_ram_text_native_ok(phys)) {
g_mixed_depth++;
{
ls_func_enter(addr, cpu);
+31
View File
@@ -83,6 +83,9 @@ extern "C" uint64_t gte_get_exec_count(void);
extern "C" void memory_init(const char* bios_path);
extern "C" void memory_set_sr_ptr(const uint32_t *p);
extern "C" uint32_t memory_get_bios_checksum(void);
extern "C" void dirty_ram_register_text_image(uint32_t phys_lo,
const uint8_t *bytes,
uint32_t len);
/* dma.c */
extern "C" void dma_init(void);
@@ -2075,6 +2078,34 @@ int main(int argc, char** argv) {
fast_boot = gc.runtime.fast_boot;
bios_hle = gc.runtime.bios_hle;
bios_hle_keep_intro = gc.runtime.bios_hle_keep_intro;
/* Let the dispatch layer distinguish "dirty because text was
* loaded" from "diverged because runtime wrote different code over
* the original EXE image". Packed/self-modifying games can rewrite
* their own text; those pages must execute from live RAM, not stale
* static native code. Best effort: without the local EXE file, the
* guard remains on the existing dirty-page behavior. */
if (!gc.exe_path.empty()) {
std::ifstream ef(gc.exe_path, std::ios::binary | std::ios::ate);
if (ef) {
std::streamsize sz = ef.tellg();
if (sz > 2048) {
uint32_t img_len = (uint32_t)(sz - 2048);
uint8_t *img = (uint8_t *)std::malloc(img_len);
if (img) {
ef.seekg(2048, std::ios::beg);
if (ef.read((char *)img, img_len)) {
dirty_ram_register_text_image(
gc.load_address & 0x1FFFFFFFu, img, img_len);
std::fprintf(stdout,
"psxrecomp: text image guard armed (0x%08X..0x%08X)\n",
gc.load_address, gc.load_address + img_len);
} else {
std::free(img);
}
}
}
}
}
/* HLE-tier scheduler subsystem replacement default (env
* PSX_HLE_SCHEDULER still wins; latched at first dispatch). */
psx_hle_scheduler_set_default(gc.runtime.hle_scheduler ? 1 : 0);
+77
View File
@@ -87,6 +87,8 @@ static inline void dirty_ram_mark_kernel_write(uint32_t phys) {
dirty_ram_mark_page(phys);
}
int dirty_ram_is_dirty(uint32_t phys);
/* Establish the clean compiled-image baseline for the game-EXE text region.
* Called ONCE when the game entry is first reached (fntrace game-start): by then
* the BIOS has fully loaded the boot EXE into [0x10000, FLOOR) — which IS the
@@ -109,6 +111,78 @@ void dirty_ram_clear_image_baseline(void) {
dirty_ram_bitmap[page >> 5] &= ~(1u << (page & 31u));
}
/* Text-image divergence guard.
*
* dirty_ram_is_dirty() is intentionally coarse: it says a page was touched by
* runtime code loading. For deciding whether the original static game dispatch
* is still safe, a dirty page is not enough information: data in a code page can
* be written without changing the instructions at a target, while packed games
* can also rewrite their own text with completely different instructions.
*
* main.cpp registers the boot EXE bytes as the reference image. Writes inside
* that range mark pages whose bytes differ from the reference. At dispatch time,
* a modified page is still allowed to use the static native entry if the code
* bytes at the target match the original image; otherwise it is sticky-diverged
* and must execute from live RAM through the dirty interpreter. */
static const uint8_t *text_ref_image = NULL;
static uint32_t text_ref_lo = 0, text_ref_hi = 0;
static uint32_t text_modified_bitmap[DIRTY_RAM_BITMAP_WORDS];
static uint32_t text_diverged_bitmap[DIRTY_RAM_BITMAP_WORDS];
static uint64_t g_text_native_blocked = 0;
static uint32_t g_text_diverged_pages = 0;
void dirty_ram_register_text_image(uint32_t phys_lo, const uint8_t *bytes,
uint32_t len) {
if (!bytes || len == 0 || phys_lo >= RAM_SIZE) return;
if (len > RAM_SIZE - phys_lo) len = RAM_SIZE - phys_lo;
text_ref_image = bytes;
text_ref_lo = phys_lo;
text_ref_hi = phys_lo + len;
memset(text_modified_bitmap, 0, sizeof(text_modified_bitmap));
memset(text_diverged_bitmap, 0, sizeof(text_diverged_bitmap));
g_text_native_blocked = 0;
g_text_diverged_pages = 0;
}
static inline void text_guard_note_write(uint32_t phys, uint32_t val, int size) {
if (!text_ref_image) return;
if (phys < text_ref_lo || phys + (uint32_t)size > text_ref_hi) return;
const uint8_t *ref = text_ref_image + (phys - text_ref_lo);
uint8_t buf[4] = { (uint8_t)val, (uint8_t)(val >> 8),
(uint8_t)(val >> 16), (uint8_t)(val >> 24) };
if (memcmp(ref, buf, (size_t)size) != 0) {
uint32_t page = phys >> DIRTY_RAM_PAGE_SHIFT;
text_modified_bitmap[page >> 5] |= (1u << (page & 31u));
}
}
int dirty_ram_text_native_ok(uint32_t phys) {
if (!text_ref_image || phys < text_ref_lo || phys >= text_ref_hi)
return !dirty_ram_is_dirty(phys);
uint32_t page = phys >> DIRTY_RAM_PAGE_SHIFT;
uint32_t bit = 1u << (page & 31u);
if (text_diverged_bitmap[page >> 5] & bit) {
g_text_native_blocked++;
return 0;
}
if (!(text_modified_bitmap[page >> 5] & bit))
return !dirty_ram_is_dirty(phys);
uint32_t n = 256;
if (n > text_ref_hi - phys) n = text_ref_hi - phys;
if (memcmp(ram + phys, text_ref_image + (phys - text_ref_lo), n) == 0)
return 1;
text_diverged_bitmap[page >> 5] |= bit;
g_text_diverged_pages++;
g_text_native_blocked++;
return 0;
}
uint64_t dirty_ram_text_native_blocked(void) { return g_text_native_blocked; }
uint32_t dirty_ram_text_diverged_pages(void) { return g_text_diverged_pages; }
void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len) {
if (len == 0 || phys >= RAM_SIZE) return;
uint32_t end = phys + len - 1u;
@@ -938,6 +1012,7 @@ static void psx_write_word_raw(uint32_t addr, uint32_t val) {
parity_trace_note_write(phys, 4, g_debug_last_store_pc);
card_data_writes_check(phys, val, 4);
dirty_ram_mark_kernel_write(phys);
text_guard_note_write(phys, val, 4);
overlay_watch_note_write(phys, 4);
#ifdef PSX_COSIM
{ extern void cosim_note_ram_write(uint32_t,uint32_t); cosim_note_ram_write(phys, 4); }
@@ -1030,6 +1105,7 @@ static void psx_write_half_raw(uint32_t addr, uint16_t val) {
parity_trace_note_write(phys, 2, g_debug_last_store_pc);
card_data_writes_check(phys, (uint32_t)val, 2);
dirty_ram_mark_kernel_write(phys);
text_guard_note_write(phys, (uint32_t)val, 2);
overlay_watch_note_write(phys, 2);
#ifdef PSX_COSIM
{ extern void cosim_note_ram_write(uint32_t,uint32_t); cosim_note_ram_write(phys, 2); }
@@ -1257,6 +1333,7 @@ static void psx_write_byte_raw(uint32_t addr, uint8_t val) {
parity_trace_note_write(phys, 1, g_debug_last_store_pc);
card_data_writes_check(phys, (uint32_t)val, 1);
dirty_ram_mark_kernel_write(phys);
text_guard_note_write(phys, (uint32_t)val, 1);
overlay_watch_note_write(phys, 1);
#ifdef PSX_COSIM
{ extern void cosim_note_ram_write(uint32_t,uint32_t); cosim_note_ram_write(phys, 1); }
+3 -4
View File
@@ -2282,10 +2282,9 @@ static int psx_sljit_call_inner(CPUState *cpu, uint32_t target, uint32_t return_
int check_contract) {
uint32_t site_sp = cpu->gpr[29]; /* sp at the call (after the delay slot) */
#ifdef PSX_HAS_GAME_DISPATCH
/* Skip the compiled game function if its page is dirty (an overlay overwrote it
* after the game-start baseline -> compiled code is stale); fall through to the
* native/interp paths which run the live overlay. Clean pages run compiled. */
if (!dirty_ram_is_dirty(target & 0x1FFFFFFFu)) {
/* Skip the compiled game function when its target is no longer native-safe;
* fall through to the native/interp paths that run the live RAM bytes. */
if (dirty_ram_text_native_ok(target & 0x1FFFFFFFu)) {
extern int psx_dispatch_game_compiled(CPUState *cpu, uint32_t addr);
cpu->pc = 0;
if (psx_dispatch_game_compiled(cpu, target)) {