From 73eb7464794b3385aeb0b1ba74db0822d7b3b7c9 Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Thu, 20 Aug 2026 13:34:25 +0300 Subject: [PATCH] fix(runtime): validate full AOT entry before straight-line handoff The dirty-RAM interpreter reused a suffix-aware validity check when resuming generated code after a patched call site. If an entry prologue had been overwritten but its resume suffix still matched, execution could enter stale AOT code and fall through to another generated piece without redispatch. Emit a whole-entry predicate for straight-line handoffs, retain suffix checks for explicit guest transfers, and fail closed for older generated dispatchers until regeneration. Validated with the MGS PAL 6,875-frame route, manual codec-to-heliport gameplay, focused continuation-guard tests, and a current-upstream recompiler build. --- recompiler/src/main_psx.cpp | 9 +++++++ .../tests/test_reachable_discovery_codegen.py | 4 +++ runtime/runtime.cmake | 13 ++++++++++ runtime/src/dirty_ram_interp.c | 13 +++++++--- runtime/src/game_dispatch_compat.c | 25 +++++++++++++++++++ .../test_dirty_text_continuation_guards.py | 16 ++++++++++-- 6 files changed, 74 insertions(+), 6 deletions(-) diff --git a/recompiler/src/main_psx.cpp b/recompiler/src/main_psx.cpp index 5d6e45df..3f6e36f9 100644 --- a/recompiler/src/main_psx.cpp +++ b/recompiler/src/main_psx.cpp @@ -1458,6 +1458,7 @@ int main(int argc, char** argv) { ds << "#include \"psx_runtime.h\"\n\n"; ds << "extern void psx_check_interrupts_dispatch_entry(CPUState* cpu, uint32_t resume_pc);\n\n"; ds << "extern int dirty_ram_text_native_ok_ranges_from(const uint32_t* lo_len_pairs, uint32_t count, uint32_t exec_pc);\n\n"; + ds << "extern int dirty_ram_text_native_ok_ranges(const uint32_t* lo_len_pairs, uint32_t count);\n\n"; // Forward declarations ds << "/* Forward declarations for all recompiled functions */\n"; @@ -1619,6 +1620,14 @@ int main(int argc, char** argv) { ds << " &k_psx_game_code_ranges[entry->range_index].lo, entry->range_count, addr);\n"; ds << "}\n\n"; + ds << "/* Full-range validity for straight-line interpreter-to-AOT handoff. */\n"; + ds << "int psx_game_text_native_ok_full(uint32_t addr) {\n"; + ds << " const PsxGameDispatchEntry* entry = psx_game_find_entry(addr);\n"; + ds << " if (!entry || entry->range_count == 0) return 0;\n"; + ds << " return dirty_ram_text_native_ok_ranges(\n"; + ds << " &k_psx_game_code_ranges[entry->range_index].lo, entry->range_count);\n"; + ds << "}\n\n"; + ds << "/* Maps PS1 address to compiled game code. Returns 1 if dispatched, 0 if unknown. */\n"; ds << "int psx_dispatch_game_compiled(CPUState* cpu, uint32_t addr) {\n"; ds << " const PsxGameDispatchEntry* entry = psx_game_find_entry(addr);\n"; diff --git a/recompiler/tests/test_reachable_discovery_codegen.py b/recompiler/tests/test_reachable_discovery_codegen.py index db87e2be..744249f2 100644 --- a/recompiler/tests/test_reachable_discovery_codegen.py +++ b/recompiler/tests/test_reachable_discovery_codegen.py @@ -139,6 +139,10 @@ def main(): failures.append("dispatch guard does not accept a continuation PC") if dispatch.count("entry->range_count, addr)") != 2: failures.append("dispatch guards do not validate from their resume PC") + if "int psx_game_text_native_ok_full(uint32_t addr)" not in dispatch: + failures.append("full-range straight-line handoff guard was not emitted") + if "dirty_ram_text_native_ok_ranges(" not in dispatch: + failures.append("full-range handoff guard lost exact range validation") if "psx_native_bad_entry(" in full: failures.append("reachable main image incorrectly used overlay codegen") if "=== Exact-Entry Function Analysis ===" not in output: diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 5946a3a9..a8520439 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -877,6 +877,13 @@ function(psxrecomp_add_runtime_target target) if(game_dispatch_native_ok_decl) set(has_game_dispatch_native_ok TRUE) endif() + file(STRINGS "${PSXRT_GAME_GENERATED_DISPATCH_C}" + game_dispatch_native_ok_full_decl + REGEX "int[ \t]+psx_game_text_native_ok_full\\(" + LIMIT_COUNT 1) + if(game_dispatch_native_ok_full_decl) + set(has_game_dispatch_native_ok_full TRUE) + endif() endif() endif() # Layer B: statically-compiled overlay dispatch. Inert unless a game @@ -1296,6 +1303,12 @@ function(psxrecomp_add_runtime_target target) APPEND PROPERTY COMPILE_DEFINITIONS PSX_GAME_DISPATCH_HAS_NATIVE_OK=1) endif() + if(has_game_dispatch_native_ok_full) + set_property(SOURCE + ${PSXRECOMP_ROOT}/runtime/src/game_dispatch_compat.c + APPEND PROPERTY COMPILE_DEFINITIONS + PSX_GAME_DISPATCH_HAS_NATIVE_OK_FULL=1) + endif() if(has_overlay_dispatch) target_compile_definitions(${target} PRIVATE PSX_HAS_OVERLAY_DISPATCH=1) endif() diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 377f35aa..5ec7fd01 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -474,6 +474,7 @@ extern int psx_dispatch_game_compiled(CPUState* cpu, uint32_t addr); extern int psx_game_address_in_text(uint32_t addr); extern int psx_game_is_function_entry(uint32_t addr); /* non-destructive entry test */ extern int psx_game_text_native_ok(uint32_t addr); +extern int psx_game_text_native_ok_full(uint32_t addr); #endif extern void psx_dispatch_call(CPUState* cpu, uint32_t addr, uint32_t return_addr); @@ -3152,10 +3153,14 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ } pc = next_pc; #ifdef PSX_HAS_GAME_DISPATCH - /* Guest call returns advance without transferred set. Re-check the - * resume PC so a patched entry can hand its unchanged tail back to - * compiled code without adding probes to ordinary dirty overlay runs. */ - if (clean_game_text_miss && interp_enter_compiled(cpu, pc)) { + /* Guest call returns advance without transferred set. A suffix-only + * continuation check is insufficient here: a split compiled piece can + * fall through by a direct host call into another piece without a new + * RAM-byte check. Require the continuation's complete emitted range to + * match before this straight-line handoff. Control-transfer handoffs + * retain suffix validation because they are explicit guest entries. */ + if (clean_game_text_miss && psx_game_text_native_ok_full(pc) && + interp_enter_compiled(cpu, pc)) { g_dirty_ram_native_handoffs++; g_dirty_ram_blocks_run++; if (pc_entry) pc_entry->insns += (uint64_t)insns_executed; diff --git a/runtime/src/game_dispatch_compat.c b/runtime/src/game_dispatch_compat.c index 315b535a..00530faf 100644 --- a/runtime/src/game_dispatch_compat.c +++ b/runtime/src/game_dispatch_compat.c @@ -21,6 +21,25 @@ int psx_game_text_native_ok(uint32_t addr) return psx_game_address_in_text(addr) && dirty_ram_text_native_ok(phys); } +int psx_game_text_native_ok_full(uint32_t addr) +{ + (void)addr; + return 0; +} + +#elif defined(PSX_HAS_GAME_DISPATCH) && \ + !defined(PSX_GAME_DISPATCH_HAS_NATIVE_OK_FULL) + +/* A suffix-aware dispatcher cannot prove that a later direct host fallthrough + * in the same compiled entry still matches guest RAM. Decline straight-line + * handoff until regeneration provides exact whole-entry validation. */ + +int psx_game_text_native_ok_full(uint32_t addr) +{ + (void)addr; + return 0; +} + #elif !defined(PSX_HAS_GAME_DISPATCH) /* The BIOS-only runtime still links shared dispatch observability. There is no @@ -37,4 +56,10 @@ int psx_game_text_native_ok(uint32_t addr) return 0; } +int psx_game_text_native_ok_full(uint32_t addr) +{ + (void)addr; + return 0; +} + #endif /* PSX_HAS_GAME_DISPATCH */ diff --git a/runtime/tests/test_dirty_text_continuation_guards.py b/runtime/tests/test_dirty_text_continuation_guards.py index 4cf1a24e..56aa21ea 100644 --- a/runtime/tests/test_dirty_text_continuation_guards.py +++ b/runtime/tests/test_dirty_text_continuation_guards.py @@ -9,6 +9,8 @@ def main() -> int: root = Path(__file__).resolve().parents[2] memory = (root / "runtime/src/memory.c").read_text(encoding="utf-8") interp = (root / "runtime/src/dirty_ram_interp.c").read_text(encoding="utf-8") + compat = (root / "runtime/src/game_dispatch_compat.c").read_text(encoding="utf-8") + runtime_cmake = (root / "runtime/runtime.cmake").read_text(encoding="utf-8") start = memory.index("int dirty_ram_text_native_ok_ranges_from(") end = memory.index("\n/* Preserve the generated-code ABI", start) @@ -29,12 +31,22 @@ def main() -> int: raise AssertionError("legacy generated-code ABI is not preserved") handoff = "clean_game_text_miss && interp_enter_compiled(cpu, " - if interp.count(handoff) != 2: - raise AssertionError("expected transfer and call-return continuation handoffs") + if interp.count(handoff) != 1: + raise AssertionError("expected one suffix-validated transfer handoff") if "interp_enter_compiled(cpu, target)" not in interp: raise AssertionError("missing transfer-boundary continuation handoff") + if "psx_game_text_native_ok_full(pc) &&" not in interp: + raise AssertionError("straight-line handoff lacks full-range validation") if "interp_enter_compiled(cpu, pc)" not in interp: raise AssertionError("missing call-return continuation handoff") + if "PSX_GAME_DISPATCH_HAS_NATIVE_OK_FULL" not in compat: + raise AssertionError("older generated dispatchers lost full-guard compatibility") + if compat.count("int psx_game_text_native_ok_full(uint32_t addr)") != 3: + raise AssertionError("full-guard compatibility branches are incomplete") + if compat.count("int psx_game_text_native_ok_full(uint32_t addr)\n{\n (void)addr;\n return 0;\n}") != 3: + raise AssertionError("a dispatcher without the full ABI may accept an unsafe handoff") + if "has_game_dispatch_native_ok_full" not in runtime_cmake: + raise AssertionError("runtime does not detect the generated full-range ABI") print("dirty-text continuation guards: ok") return 0