From 685f38dacb2b1ed0c9a6a57a3bfe336fb16f42d6 Mon Sep 17 00:00:00 2001 From: ai-tdd-labs Date: Sat, 4 Jul 2026 16:41:29 +0200 Subject: [PATCH] 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 --- runtime/include/dirty_ram_interp.h | 5 ++ runtime/src/dirty_ram_interp.c | 23 ++++----- runtime/src/main.cpp | 31 ++++++++++++ runtime/src/memory.c | 77 ++++++++++++++++++++++++++++++ runtime/src/overlay_loader.c | 7 ++- 5 files changed, 124 insertions(+), 19 deletions(-) diff --git a/runtime/include/dirty_ram_interp.h b/runtime/include/dirty_ram_interp.h index 94e8f532..c9b729a1 100644 --- a/runtime/include/dirty_ram_interp.h +++ b/runtime/include/dirty_ram_interp.h @@ -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. */ diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 4af43f81..ab952c28 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -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); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 4360a4b0..5209b704 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -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); diff --git a/runtime/src/memory.c b/runtime/src/memory.c index 7091b114..eba5999f 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -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); } diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index 80f4ddb8..b215b1c3 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -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)) {