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.
This commit is contained in:
Alexandros Mandravillis
2026-08-20 13:34:25 +03:00
committed by Codex (Pegasus)
parent c5488ebcb8
commit 73eb746479
6 changed files with 74 additions and 6 deletions
+9
View File
@@ -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";
@@ -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:
+13
View File
@@ -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()
+9 -4
View File
@@ -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;
+25
View File
@@ -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 */
@@ -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