Merge pull request #271 from Alexbeav/pr/stale-static-guards

codegen+runtime: stop executing stale static code when a game overlays its own text
This commit is contained in:
Matt Stanley
2026-08-30 20:52:11 -07:00
committed by GitHub
3 changed files with 76 additions and 14 deletions
+43 -2
View File
@@ -101,6 +101,33 @@ CodeGenerator::CodeGenerator(const PS1Executable& exe, const CodeGenConfig& conf
{ const char* e = std::getenv("PSX_CPS"); cps_enabled_ = (e == nullptr || e[0] != '0'); }
}
/* Inter-piece host transfers (fallthrough into a split piece / the next
* function, and the legacy non-CPS split-target transfers) hand execution to
* another compiled dispatch entry WITHOUT passing the dispatcher's
* stale-static validation. A game that overlays its own text at runtime
* (CMR2 streams transform-loop variants over its boot EXE) keeps executing
* the stale static translation of the target piece through such an edge.
* The guard revalidates the target entry's emitted ranges; on mismatch it
* publishes the PC and unwinds to the trampoline, whose dispatch takes the
* sanctioned dirty-RAM-interpreter fallback over the live bytes. */
static std::string emit_stale_static_guard(uint32_t target, const std::string& indent) {
return fmt::format(
"{0}if (!psx_game_text_native_ok(0x{1:08X}u)) {{ cpu->pc = 0x{1:08X}u; return; }} /* stale-static guard */\n",
indent, target);
}
/* Same guard when only the emitted function NAME is at hand (the
* fallthrough-to-next-function edges). Game functions are named func_%08X;
* anything else gets no guard (identical to the pre-guard emission). */
static std::string emit_stale_static_guard_named(const std::string& name,
const std::string& indent) {
if (name.rfind("func_", 0) != 0) return "";
char* end = nullptr;
unsigned long v = strtoul(name.c_str() + 5, &end, 16);
if (!end || *end != '\0' || v == 0) return "";
return emit_stale_static_guard((uint32_t)v, indent);
}
uint32_t CodeGenerator::partial_block_cycle_count(uint32_t addr,
const ControlFlowGraph& cfg) const {
if (cfg.blocks.count(addr)) {
@@ -2154,6 +2181,7 @@ std::string CodeGenerator::translate_basic_block(
<< fmt::format("cpu->pc = 0x{:08X}u; return; /* CPS taken: split */\n", branch_target);
} else if (known_functions_.count(branch_target)) {
ss << emit_interrupt_check(branch_target, config_.indent + config_.indent);
ss << emit_stale_static_guard(branch_target, config_.indent + config_.indent);
ss << config_.indent << config_.indent
<< fmt::format("func_{:08X}(cpu); return; /* taken: split piece */\n", branch_target);
} else {
@@ -2172,6 +2200,7 @@ std::string CodeGenerator::translate_basic_block(
<< fmt::format("cpu->pc = 0x{:08X}u; return; /* CPS not taken: split */\n", fall_through_addr);
} else if (known_functions_.count(fall_through_addr)) {
ss << emit_interrupt_check(fall_through_addr, config_.indent + config_.indent);
ss << emit_stale_static_guard(fall_through_addr, config_.indent + config_.indent);
ss << config_.indent << config_.indent
<< fmt::format("func_{:08X}(cpu); return; /* not taken: split piece */\n", fall_through_addr);
} else {
@@ -2197,6 +2226,7 @@ std::string CodeGenerator::translate_basic_block(
} else if (block.exit_instr.target != 0 && known_functions_.count(block.exit_instr.target)) {
// Jump target is out-of-function and is a known function start
ss << emit_interrupt_check(block.exit_instr.target, config_.indent);
ss << emit_stale_static_guard(block.exit_instr.target, config_.indent);
ss << config_.indent
<< fmt::format("func_{:08X}(cpu); return; /* j to split piece */\n",
block.exit_instr.target);
@@ -2368,8 +2398,13 @@ std::string CodeGenerator::translate_basic_block(
ss << config_.indent << "{ uint32_t _csp = cpu->gpr[29];\n";
ss << emit_interrupt_check(target, config_.indent);
if (known_functions_.count(target) > 0) {
ss << config_.indent << fmt::format("func_{:08X}(cpu); /* jal */\n", target);
ss << config_.indent << fmt::format("if (psx_call_contract(cpu, 0x{:08X}u, _csp)) return; }}\n", addr + 8);
ss << config_.indent << fmt::format(
"if (psx_game_text_native_ok(0x{0:08X}u)) {{ func_{0:08X}(cpu); /* jal */\n", target);
ss << config_.indent << fmt::format(
"if (psx_call_contract(cpu, 0x{:08X}u, _csp)) return;\n", addr + 8);
ss << config_.indent << fmt::format(
"}} else {{ call_by_address(cpu, 0x{:08X}u); /* jal: stale-static guard */\n", target);
ss << config_.indent << "if (g_psx_call_bail) return; (void)_csp; } }\n";
} else {
ss << config_.indent << fmt::format("call_by_address(cpu, 0x{:08X}u); /* external jal */\n", target);
/* psx_dispatch_call validated the (ra, sp) contract;
@@ -2383,6 +2418,7 @@ std::string CodeGenerator::translate_basic_block(
// Split-function: JAL continuation is outside this function piece.
// Tail-call to the continuation piece (at exit_addr + 8, past delay slot).
if (known_functions_.count(cont_addr)) {
ss << emit_stale_static_guard(cont_addr, config_.indent);
ss << config_.indent
<< fmt::format("func_{:08X}(cpu); return; /* jal cont: split piece */\n", cont_addr);
} else {
@@ -2424,6 +2460,7 @@ std::string CodeGenerator::translate_basic_block(
} else {
// Split-function: JALR continuation is outside this function piece.
if (known_functions_.count(cont_addr)) {
ss << emit_stale_static_guard(cont_addr, config_.indent);
ss << config_.indent
<< fmt::format("func_{:08X}(cpu); return; /* jalr cont: split piece */\n", cont_addr);
} else {
@@ -2449,6 +2486,7 @@ std::string CodeGenerator::translate_basic_block(
} else if (block.exit_instr.type == ControlFlowType::None) {
uint32_t next_addr = block.end_addr + 4;
if (known_functions_.count(next_addr) > 0) {
ss << emit_stale_static_guard(next_addr, config_.indent);
ss << config_.indent
<< fmt::format("func_{:08X}(cpu); return; /* fallthrough to split piece */\n",
next_addr);
@@ -2787,6 +2825,7 @@ GeneratedFunction CodeGenerator::generate_function(
const BasicBlock& last = cfg.blocks.at(cfg.block_order.back());
bool is_reachable = last.is_entry || !last.predecessors.empty();
if (is_reachable) {
body_ss << emit_stale_static_guard_named(fallthrough_name, " ");
body_ss << fmt::format(" {}(cpu); /* fallthrough to next function */\n",
fallthrough_name);
}
@@ -3035,6 +3074,7 @@ std::vector<GeneratedFunction> CodeGenerator::generate_alias_group(
last_block.exit_instr.type == ControlFlowType::Jump) &&
last_block.successors.empty());
if (needs_fallthrough) {
body << emit_stale_static_guard_named(fallthrough_name, " ");
body << fmt::format(" {}(cpu); /* fallthrough to next function */\n",
fallthrough_name);
}
@@ -3224,6 +3264,7 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const {
ss << "extern void cosim_block(uint32_t block_leader_phys);\n";
ss << "extern void cosim_instr(uint32_t pc);\n";
ss << "#endif\n";
ss << "extern int psx_game_text_native_ok(uint32_t addr); /* stale-static guard (dispatch shard) */\n";
ss << "extern int psx_datashard_enter(CPUState* cpu, uint32_t key); /* data-shard replay/capture (data_shards.c) */\n";
ss << "extern void psx_mod_function_entry(CPUState* cpu, uint32_t address); /* trusted opt-in game-mod hook */\n";
ss << "extern void psx_datashard_ret(CPUState* cpu); /* data-shard capture finalize */\n";
+28 -10
View File
@@ -482,15 +482,19 @@ int dirty_ram_text_native_ok(uint32_t phys) {
* a mismatch never poisons an unrelated 4 KB page forever: every decision is
* made from the live bytes the native body will actually execute.
*
* exec_pc is the dispatch/resume address. Ranges that end at or before that PC
* are skipped, and a range that straddles it is clipped to [exec_pc, end). A
* runtime patch of a function prologue must not block a compiled continuation
* that never fetches the patched bytes. */
* exec_pc is the dispatch/resume address, kept for observability and future
* use; ranges are validated IN FULL regardless of it. The former
* [exec_pc, end) clip assumed a continuation "never fetches the patched
* bytes" behind its resume PC — false for any entry with a backward edge:
* a post-call continuation re-enters mid-function and loops back into the
* clipped-away region, executing stale static code (CMR2 overlay
* corruption, second locus 0x80016B34). A genuinely patched prologue now
* blocks the whole entry to the interpreter — correct, merely slower. */
int dirty_ram_text_native_ok_ranges_from(const uint32_t *lo_len_pairs,
uint32_t count,
uint32_t exec_pc) {
if (!text_ref_image || !lo_len_pairs || count == 0) return 0;
uint32_t at = exec_pc & 0x1FFFFFFFu;
(void)exec_pc;
int any = 0;
for (uint32_t i = 0; i < count; i++) {
uint32_t phys = lo_len_pairs[i * 2u] & 0x1FFFFFFFu;
@@ -500,12 +504,26 @@ int dirty_ram_text_native_ok_ranges_from(const uint32_t *lo_len_pairs,
g_text_native_blocked++;
return 0;
}
if (phys + len <= at) continue;
if (phys < at) {
len -= at - phys;
phys = at;
}
any = 1;
/* Page-clean fast path: every page this range touches is neither
* guard-modified nor runtime-dirty, so its bytes still equal the
* reference image — skip the memcmp. This keeps the emitted
* stale-static guards on inter-piece host transfers near-free on
* the (overwhelmingly common) pristine pages. */
{
uint32_t p0 = phys >> DIRTY_RAM_PAGE_SHIFT;
uint32_t p1 = (phys + len - 1u) >> DIRTY_RAM_PAGE_SHIFT;
uint32_t p;
int clean = 1;
for (p = p0; p <= p1; p++) {
if ((text_modified_bitmap[p >> 5] & (1u << (p & 31u))) ||
dirty_ram_is_dirty(p << DIRTY_RAM_PAGE_SHIFT)) {
clean = 0;
break;
}
}
if (clean) continue;
}
if (memcmp(ram + phys, text_ref_image + (phys - text_ref_lo), len) != 0) {
uint32_t off = 0;
const uint8_t *live = ram + phys;
+5 -2
View File
@@ -200,8 +200,11 @@ def main():
for _ in range(max(0, n)):
cmd(sa, "step 1", timeout=1200)
cmd(sb, "step 1", timeout=1200)
da = parse_cpu(cmd(sa, "cpu"))
db = parse_cpu(cmd(sb, "cpu"))
ra = cmd(sa, "cpu"); rb = cmd(sb, "cpu")
da = parse_cpu(ra)
db = parse_cpu(rb)
print(f"A cpu dump: {ra}", flush=True)
print(f"B cpu dump: {rb}", flush=True)
print(f"CPU field-diff at cp {args.cpudiff_at_cp} (A={args.a} B={args.b}):", flush=True)
diffs = [k for k in da if da.get(k) != db.get(k)]
if not diffs: