diff --git a/docs/internal/upstream/martin-pr16-overlay-decodable-fallback.md b/docs/internal/upstream/martin-pr16-overlay-decodable-fallback.md new file mode 100644 index 000000000..f9b4908be --- /dev/null +++ b/docs/internal/upstream/martin-pr16-overlay-decodable-fallback.md @@ -0,0 +1,33 @@ +# PR #16 decodable overlay fallback provenance + +This branch isolates the reusable discovery fallback from Martin Penkava's +[`mstan/psxrecomp` PR #16](https://github.com/mstan/psxrecomp/pull/16), exact +source commit +[`e6809ccf7d778a4c2f32d9e27c0ec31a44cbd2ba`](https://github.com/mstan/psxrecomp/commit/e6809ccf7d778a4c2f32d9e27c0ec31a44cbd2ba). + +Last evaluated: 2026-07-14 + +| Item | Value | +| --- | --- | +| Head repository/branch | `shaneomac1337/psxrecomp`, `smackdown2-fixes` | +| Pull request/source commit | `mstan/psxrecomp` PR #16, `e6809ccf7d778a4c2f32d9e27c0ec31a44cbd2ba` | +| Local base | `7085721afe338a03cb114321a3576cdff420b732` (`origin/master`) | +| Local branch | `feat/pr16-overlay-decodable-fallback-mpenkava` | + +The fallback handles executable RAM populated by bulk host transfers that do +not traverse ordinary RAM write hooks. A control transfer above the configured +boot-EXE text end is admitted only when its first word passes the existing MIPS +decoder check; the interpreter then marks that word executable. Invalid, data, +out-of-RAM, and below-floor targets remain rejected. + +The source commit also added pre-initialization guards to the overlay loader. +Those guards were reviewed and merged separately in PR #23 and are explicitly +excluded here. The source commit's game name, disc ID, addresses, and validation +claims are evidence only and are not embedded in framework code. + +Source authorship is retained with: + +```text +Co-authored-by: Martin Penkava +Co-authored-by: Claude Fable 5 +``` diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 5c94e2da0..6e6889d43 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -280,6 +280,7 @@ extern void psx_dispatch_call(CPUState* cpu, uint32_t addr, uint32_t return_addr /* Forward decls from memory.c — used to read instruction bytes. */ extern uint8_t *memory_get_ram_ptr(void); +extern void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len); /* MIPS instruction field decoders. */ static inline uint32_t op_field (uint32_t i) { return (i >> 26) & 0x3Fu; } @@ -2211,7 +2212,20 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ } #define OV_FPLOG_RET1() do { if (_ovfp) overlay_fp_log(addr, _in_regs, cpu, 0); return 1; } while (0) - if (!dirty_ram_is_dirty(phys) && !clean_game_text_miss) return 0; + if (!dirty_ram_is_dirty(phys) && !clean_game_text_miss) { + /* Bulk host transfers can populate post-EXE executable RAM without + * passing through the write hooks that mark dirty pages. A real + * control transfer to a decodable word above the configured boot-EXE + * text end is enough evidence to admit that word to the interpreter. + * Data and invalid targets still fail closed. */ + if (phys < (2u * 1024u * 1024u) && + phys >= g_overlay_region_floor && + dirty_ram_word_looks_decodable(fetch_word(phys))) { + dirty_ram_mark_executable_range(phys, 4u); + } else { + return 0; + } + } /* Interp-pressure signal for variant-capture automation (step 2.8): * counts dispatches the interpreter actually handles inside a capture diff --git a/runtime/tests/test_overlay_decodable_fallback.py b/runtime/tests/test_overlay_decodable_fallback.py new file mode 100644 index 000000000..c307cb827 --- /dev/null +++ b/runtime/tests/test_overlay_decodable_fallback.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Structural regression test for unmarked post-EXE code discovery.""" + +from pathlib import Path +import re +import sys + + +ROOT = Path(__file__).resolve().parents[2] +SOURCE = ROOT / "runtime" / "src" / "dirty_ram_interp.c" + + +def function_body(source: str, name: str) -> str: + match = re.search(rf"\bstatic\s+int\s+{re.escape(name)}\s*\([^;]*?\)\s*\{{", source, re.S) + if not match: + raise AssertionError(f"missing function definition: {name}") + start = match.end() + depth = 1 + for pos in range(start, len(source)): + if source[pos] == "{": + depth += 1 + elif source[pos] == "}": + depth -= 1 + if depth == 0: + return source[start:pos] + raise AssertionError(f"unterminated function definition: {name}") + + +def main() -> int: + source = SOURCE.read_text(encoding="utf-8") + body = function_body(source, "dirty_ram_dispatch_inner") + + dirty_gate = body.find("!dirty_ram_is_dirty(phys) && !clean_game_text_miss") + floor_gate = body.find("phys >= g_overlay_region_floor", dirty_gate) + ram_gate = body.find("phys < (2u * 1024u * 1024u)", dirty_gate) + decode_gate = body.find("dirty_ram_word_looks_decodable(fetch_word(phys))", dirty_gate) + mark = body.find("dirty_ram_mark_executable_range(phys, 4u)", dirty_gate) + if min(dirty_gate, floor_gate, ram_gate, decode_gate, mark) < 0: + raise AssertionError("missing dirty/floor/RAM/decode/mark fallback chain") + if not (dirty_gate < floor_gate < decode_gate < mark and dirty_gate < ram_gate < mark): + raise AssertionError("fallback checks or executable marking are out of order") + + fallback = body[dirty_gate:mark + 200] + if "else {\n return 0;" not in fallback: + raise AssertionError("invalid/data targets do not fail closed") + + print("PASS: decodable post-EXE dispatch fallback is guarded and fail-closed") + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except AssertionError as exc: + print(f"FAIL: {exc}") + sys.exit(1)