Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/internal/upstream/shaneomac-pr16-overlay-init-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Shaneomac PR #16: overlay-loader initialization guard

| Field | Value |
|---|---|
| Upstream PR | <https://github.com/mstan/psxrecomp/pull/16> |
| Source commit | [`e6809ccf7d778a4c2f32d9e27c0ec31a44cbd2ba`](https://github.com/mstan/psxrecomp/commit/e6809ccf7d778a4c2f32d9e27c0ec31a44cbd2ba) |
| Source author | Martin Penkava (`shaneomac1337`) `<mpenkava1337@gmail.com>` |
| Source co-author | Claude Fable 5 `<noreply@anthropic.com>` |
| Port base | `dde268dc0fb9daf8fe6529f4aebfe80995350334` |
| Evaluated/ported | 2026-07-13 (America/Los_Angeles) |

## Included

Only the overlay-loader lifecycle guards from the source commit were adapted.
`overlay_loader_dispatch()`, `overlay_loader_is_candidate()`, and
`overlay_loader_call_native()` now return without consulting overlay indexes
until `overlay_loader_init()` finishes constructing them and sets `s_active`.

The focused regression test is
`runtime/tests/test_overlay_init_guard.py`.

## Explicitly excluded

- The source commit's decodable-word/high-RAM dirty-interpreter fallback in
`runtime/src/dirty_ram_interp.c`.
- All SmackDown-specific behavior and data.
- Every other PR #16 change, including Vulkan, audio/SPU, GPU primitive-size,
MSVC portability, and helper-script work.

The source commit combined the lifecycle guard with a separate discovery
fallback. This port intentionally does not cherry-pick that commit; it adapts
only the three fail-closed `s_active` checks so the framework change remains
game-agnostic and reviewable in isolation.
5 changes: 4 additions & 1 deletion runtime/src/overlay_loader.c
Original file line number Diff line number Diff line change
Expand Up @@ -2390,6 +2390,7 @@ static int overlay_find_by_range(uint32_t phys) {

int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) {
uint32_t phys = addr & 0x1FFFFFFFu;
if (!s_active) return 0;
int lazy_loaded = 0;
retry_candidates:
int head = idx_head(phys);
Expand Down Expand Up @@ -2883,6 +2884,7 @@ static FpEnt s_fp[FP_CAP];
static uint64_t s_fp_seq = 0;

int overlay_loader_is_candidate(uint32_t phys) {
if (!s_active) return 0;
phys &= 0x1FFFFFFFu;
return idx_head(phys) >= 0 || lazy_has_exact_entry(phys);
}
Expand Down Expand Up @@ -3229,7 +3231,8 @@ void overlay_fp_log(uint32_t addr, const uint32_t *in_regs,
* leaks (root cause of the dwarf->overworld native blue screen).
* Returns 1 iff a native candidate ran. */
int overlay_loader_call_native(CPUState *cpu, uint32_t addr) {
if (!s_native_exec) return 0; /* interp mode: keep the legacy inline path */
if (!s_active || !s_native_exec)
return 0; /* inactive/interp mode: keep the legacy inline path */
uint32_t phys = addr & 0x1FFFFFFFu;
if (idx_head(phys) < 0 && !lazy_has_exact_entry(phys))
return 0; /* neither a registered nor an exact cached entry */
Expand Down
85 changes: 85 additions & 0 deletions runtime/tests/test_overlay_init_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Structural regression test for pre-init overlay-loader entry points.

The public dispatch/candidate/native-call entry points can be reached during
early BIOS initialization, before overlay_loader_init() constructs their index
tables. They must fail closed on s_active before consulting those tables.

Usage: python runtime/tests/test_overlay_init_guard.py
Exit 0 = PASS.
"""

from pathlib import Path
import re
import sys


ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "runtime" / "src" / "overlay_loader.c"


def function_body(source: str, name: str) -> str:
match = re.search(rf"\b(?:int|void)\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 require_before(body: str, guard_pattern: str, state_pattern: str, name: str) -> None:
guard = re.search(guard_pattern, body)
state = re.search(state_pattern, body)
if not guard:
raise AssertionError(f"{name}: missing pre-init s_active guard")
if not state:
raise AssertionError(f"{name}: test could not find protected index access")
if guard.start() > state.start():
raise AssertionError(f"{name}: s_active guard occurs after index access")


def main() -> int:
source = SOURCE.read_text(encoding="utf-8")

dispatch = function_body(source, "overlay_loader_dispatch")
require_before(
dispatch,
r"if\s*\(\s*!s_active\s*\)\s*return\s+0\s*;",
r"idx_head\s*\(",
"overlay_loader_dispatch",
)

candidate = function_body(source, "overlay_loader_is_candidate")
require_before(
candidate,
r"if\s*\(\s*!s_active\s*\)\s*return\s+0\s*;",
r"idx_head\s*\(",
"overlay_loader_is_candidate",
)

call_native = function_body(source, "overlay_loader_call_native")
require_before(
call_native,
r"if\s*\(\s*!s_active\s*\|\|\s*!s_native_exec\s*\)\s*return\s+0\s*;",
r"idx_head\s*\(",
"overlay_loader_call_native",
)

print("PASS: overlay loader entry points fail closed before initialization")
return 0


if __name__ == "__main__":
try:
sys.exit(main())
except AssertionError as exc:
print(f"FAIL: {exc}")
sys.exit(1)