#include "function_analysis.h" #include "ps1_exe_parser.h" #include #include #include #include #include #include namespace { constexpr uint32_t kLoad = 0x80010000u; int failures = 0; #define CHECK(cond, message) do { \ if (!(cond)) { \ std::fprintf(stderr, "FAIL: %s\n", (message)); \ failures++; \ } \ } while (0) void put32(std::vector& data, size_t offset, uint32_t value) { data[offset + 0] = static_cast(value); data[offset + 1] = static_cast(value >> 8); data[offset + 2] = static_cast(value >> 16); data[offset + 3] = static_cast(value >> 24); } uint32_t jal(uint32_t target) { return 0x0C000000u | ((target >> 2) & 0x03FFFFFFu); } std::vector make_exe_buffer(uint32_t image_size, uint32_t entry = kLoad) { std::vector data(2048u + image_size, 0); std::memcpy(data.data(), "PS-X EXE", 8); put32(data, 0x10, entry); put32(data, 0x18, kLoad); put32(data, 0x1C, image_size); return data; } PSXRecomp::PS1Executable parse(std::vector data) { std::string error; auto exe = PSXRecomp::PS1ExeParser::parse_buffer(data, error); CHECK(exe.has_value(), error.c_str()); return exe.value(); } std::set starts(const PSXRecomp::FunctionAnalysisResult& result) { std::set out; for (const auto& function : result.functions) out.insert(function.start_addr); return out; } struct ApeSwitchFixture { std::vector image; uint32_t entry = kLoad + 0x500u; uint32_t jr_pc = kLoad + 0x528u; uint32_t table = kLoad + 0xA00u; std::vector cases = { kLoad + 0x540u, kLoad + 0x550u, kLoad + 0x560u}; }; ApeSwitchFixture make_ape_switch() { ApeSwitchFixture f; f.image = make_exe_buffer(0x1000u, f.entry); const size_t text = 2048u; put32(f.image, text + 0x500, 0x3C088001u); // lui t0,0x8001 put32(f.image, text + 0x504, 0x25100A00u); // addiu s0,t0,0x0a00 put32(f.image, text + 0x508, 0x00000000u); put32(f.image, text + 0x50C, 0x2C620003u); // sltiu v0,v1,3 put32(f.image, text + 0x510, 0x1040001Bu); // beq v0,zero,+0x580 put32(f.image, text + 0x514, 0x00000000u); put32(f.image, text + 0x518, 0x00031080u); // sll v0,v1,2 put32(f.image, text + 0x51C, 0x00501021u); // addu v0,v0,s0 put32(f.image, text + 0x520, 0x8C420000u); // lw v0,0(v0) put32(f.image, text + 0x524, 0x00000000u); put32(f.image, text + 0x528, 0x00400008u); // jr v0 put32(f.image, text + 0x52C, 0x00000000u); for (size_t i = 0; i < f.cases.size(); i++) { size_t off = static_cast(f.cases[i] - kLoad); put32(f.image, text + off, 0x24020001u + static_cast(i)); put32(f.image, text + off + 4u, 0x03E00008u); put32(f.image, text + off + 8u, 0x00000000u); put32(f.image, text + 0xA00u + i * 4u, f.cases[i]); } put32(f.image, text + 0x580, 0x03E00008u); put32(f.image, text + 0x584, 0x00000000u); return f; } bool resolves_ape_switch(const ApeSwitchFixture& f, uint32_t entry, uint32_t producer_lo = kLoad, uint32_t producer_hi = kLoad + 0x1000u) { auto exe = parse(f.image); PSXRecomp::ExactJumpTable table; return PSXRecomp::resolve_exact_bounded_jump_table( exe, entry, kLoad + 0x1000u, f.jr_pc, 2u, table, nullptr, producer_lo, producer_hi); } ApeSwitchFixture make_scheduled_switch() { auto f = make_ape_switch(); const size_t text = 2048; put32(f.image, text + 0x500, 0); put32(f.image, text + 0x504, 0); put32(f.image, text + 0x508, 0x2C620003u); // sltiu v0,v1,3 put32(f.image, text + 0x50C, 0x1040001Cu); // beq v0,zero,+0x580 put32(f.image, text + 0x510, 0x3C028001u); // lui v0 (guard delay) put32(f.image, text + 0x514, 0x24420A00u); // addiu v0,v0,0xa00 put32(f.image, text + 0x518, 0x00031880u); // sll v1,v1,2 put32(f.image, text + 0x51C, 0x00621821u); // addu v1,v1,v0 put32(f.image, text + 0x520, 0x8C620000u); // lw v0,0(v1) return f; } void check_scheduled_switch() { const size_t text = 2048; auto f = make_scheduled_switch(); auto exe = parse(f.image); PSXRecomp::ExactJumpTable table; CHECK(PSXRecomp::resolve_exact_bounded_jump_table( exe, f.entry, kLoad + 0x600u, f.jr_pc, 2u, table) && table.table_base == f.table && table.table_count == 3u, "scheduled table constant resolves exactly"); std::set targets; for (auto target : table.targets) targets.insert(target.second); CHECK(targets == std::set(f.cases.begin(), f.cases.end()), "scheduled table has exactly the expected case targets"); PSXRecomp::FunctionAnalyzer analyzer(exe); auto result = analyzer.analyze_exact_entries({f.entry}); for (auto target : f.cases) CHECK(result.exact_reachable_pcs.count(target), "scheduled switch cases are reached from the function root"); auto renamed = f; put32(renamed.image, text + 0x510, 0x3C088001u); put32(renamed.image, text + 0x514, 0x25020A00u); CHECK(resolves_ape_switch(renamed, renamed.entry), "scheduled constant can rename registers"); auto no_load_delay = f; no_load_delay.jr_pc = kLoad + 0x524u; put32(no_load_delay.image, text + 0x524, 0x00400008u); CHECK(!resolves_ape_switch(no_load_delay, f.entry), "scheduled switch must respect the R3000 load delay"); auto signed_low = f; signed_low.image.resize(text + 0x9000u); put32(signed_low.image, 0x1C, 0x9000u); put32(signed_low.image, text + 0x510, 0x3C028002u); put32(signed_low.image, text + 0x514, 0x24428A00u); for (size_t i = 0; i < f.cases.size(); ++i) put32(signed_low.image, text + 0x8A00u + 4u * i, f.cases[i]); CHECK(resolves_ape_switch(signed_low, f.entry, kLoad, kLoad + 0x9000u), "scheduled constant sign-extends ADDIU low half"); CHECK(!resolves_ape_switch(f, f.entry, kLoad, kLoad + 0x900u), "scheduled table must belong to the producer"); auto jump = [](uint32_t offset) { return 0x08000000u | (((kLoad + offset) >> 2) & 0x03FFFFFFu); }; const std::vector>> mutations = { {{0x508, 0x2C630003u}, {0x50C, 0x1060001Cu}}, // bound writes index {{0x510, 0x3C038001u}, {0x514, 0x24620A00u}}, // LUI writes index {{0x514, 0x24430A00u}}, // ADDIU writes index {{0x514, 0x25020A00u}}, // wrong constant source {{0x510, 0x3C228001u}}, // reserved LUI fields {{0x50C, jump(0x580) | 0x04000000u}}, // JAL delay {{0x50C, 0x5040001Cu}}, // BEQL {{0x50C, 0x1040FFFFu}}, // self-loop with clobbered condition {{0x50C, 0x10400000u}}, // reject into LUI {{0x500, jump(0x510)}}, // earlier edge skips bound {{0x590, jump(0x510)}}, // later edge skips bound {{0x540, jump(0x510)}, {0x544, 0}}, // case skips bound {{0x504, jump(0x580)}}, // bound in delay slot {{0xA00, kLoad + 0x510u}}, // case target skips bound {{0xA04, kLoad + 0x2000u}}, // out of host }; for (const auto& changes : mutations) { auto broken = f; for (auto change : changes) put32(broken.image, text + change.first, change.second); CHECK(!resolves_ape_switch(broken, broken.entry), "unsafe scheduled-switch mutation fails closed"); } } } // namespace int main() { check_scheduled_switch(); auto image = make_exe_buffer(0x2000); const size_t text = 2048; // Root: direct call to a framed target, direct call to a frameless leaf, // unresolved jalr, direct call beyond the eventual bound, then return. put32(image, text + 0x00, jal(kLoad + 0x100)); put32(image, text + 0x04, 0x00000000u); put32(image, text + 0x08, jal(kLoad + 0x300)); put32(image, text + 0x0C, 0x00000000u); put32(image, text + 0x10, 0x0320F809u); // jalr $ra,$t9 (unresolved) put32(image, text + 0x14, 0x00000000u); put32(image, text + 0x18, jal(kLoad + 0x1100)); put32(image, text + 0x1C, 0x00000000u); put32(image, text + 0x20, 0x03E00008u); put32(image, text + 0x24, 0x00000000u); // Callable direct target. put32(image, text + 0x100, 0x27BDFFF0u); put32(image, text + 0x104, 0x03E00008u); put32(image, text + 0x108, 0x27BD0010u); // Valid-looking callback not referenced by a direct call. put32(image, text + 0x200, 0x27BDFFF0u); put32(image, text + 0x204, 0x03E00008u); put32(image, text + 0x208, 0x27BD0010u); // A direct JAL is sufficient callable-boundary evidence even when the // target is a tiny frameless leaf. put32(image, text + 0x300, 0x24020001u); // addiu $v0,$zero,1 put32(image, text + 0x304, 0x03E00008u); put32(image, text + 0x308, 0x00000000u); // Callable target outside the verified static bound. put32(image, text + 0x1100, 0x27BDFFF0u); put32(image, text + 0x1104, 0x03E00008u); put32(image, text + 0x1108, 0x27BD0010u); auto exe = parse(image); std::string error; CHECK(PSXRecomp::apply_static_analysis_bound(exe, 0x1000, error), "valid page-aligned analysis bound is accepted"); CHECK(exe.code_size() == 0x1000 && exe.code_data.size() == 0x1000, "accepted bound truncates header and analysis payload together"); PSXRecomp::FunctionAnalyzer analyzer(exe); const auto reachable = starts(analyzer.analyze_exact_entries({kLoad})); CHECK(reachable == std::set({kLoad, kLoad + 0x100, kLoad + 0x300}), "reachable analysis follows framed and frameless direct JAL targets"); CHECK(!reachable.count(kLoad + 0x200), "unseen indirect callback fails closed"); CHECK(!reachable.count(kLoad + 0x1100), "direct target beyond verified bound fails closed"); // A JAL-shaped word can target an embedded table whose first pointer also // happens to decode as a legal MIPS instruction. Three adjacent local // pointers are stronger data evidence; keep that target interpreted. auto pointer_image = make_exe_buffer(0x1000); put32(pointer_image, text + 0x00, jal(kLoad + 0x400)); put32(pointer_image, text + 0x04, 0x00000000u); put32(pointer_image, text + 0x08, 0x03E00008u); put32(pointer_image, text + 0x0C, 0x00000000u); put32(pointer_image, text + 0x400, kLoad + 0x500); put32(pointer_image, text + 0x404, kLoad + 0x520); put32(pointer_image, text + 0x408, kLoad + 0x540); auto pointer_exe = parse(pointer_image); PSXRecomp::FunctionAnalyzer pointer_analyzer(pointer_exe); const auto pointer_starts = starts( pointer_analyzer.analyze_exact_entries({kLoad})); CHECK(!pointer_starts.count(kLoad + 0x400), "reachable analysis rejects a dense local pointer table target"); // Discover call targets in stable rounds. An early root's JAL into the // middle of a later explicit root must remain absorbed by that host rather // than splitting/hard-capping it before the host is walked. auto interior_call_image = make_exe_buffer(0x1000); put32(interior_call_image, text + 0x00, jal(kLoad + 0x108)); put32(interior_call_image, text + 0x04, 0x00000000u); put32(interior_call_image, text + 0x08, 0x03E00008u); put32(interior_call_image, text + 0x0C, 0x00000000u); put32(interior_call_image, text + 0x100, 0x24020001u); put32(interior_call_image, text + 0x104, 0x24420001u); put32(interior_call_image, text + 0x108, 0x24420001u); put32(interior_call_image, text + 0x10C, 0x03E00008u); put32(interior_call_image, text + 0x110, 0x00000000u); auto interior_call_exe = parse(interior_call_image); PSXRecomp::FunctionAnalyzer interior_call_analyzer(interior_call_exe); const auto interior_call_starts = starts( interior_call_analyzer.analyze_exact_entries( {kLoad, kLoad + 0x100})); CHECK(interior_call_starts == std::set({kLoad, kLoad + 0x100}), "direct call into an owned function stays an interior alias"); // Ownership must also settle among roots discovered in the same round. // The initial root calls both A and an address B inside A. Neither address // is owned by the initial partition, but walking A without B as a cap // reaches B, so B is an alias rather than a sibling function. auto same_round_image = make_exe_buffer(0x1000); put32(same_round_image, text + 0x00, jal(kLoad + 0x100)); put32(same_round_image, text + 0x04, 0x00000000u); put32(same_round_image, text + 0x08, jal(kLoad + 0x110)); put32(same_round_image, text + 0x0C, 0x00000000u); put32(same_round_image, text + 0x10, 0x03E00008u); put32(same_round_image, text + 0x14, 0x00000000u); put32(same_round_image, text + 0x100, jal(kLoad + 0x110)); put32(same_round_image, text + 0x104, 0x00000000u); put32(same_round_image, text + 0x108, 0x24420001u); put32(same_round_image, text + 0x10C, 0x24420001u); put32(same_round_image, text + 0x110, 0x03E00008u); put32(same_round_image, text + 0x114, 0x00000000u); auto same_round_exe = parse(same_round_image); PSXRecomp::FunctionAnalyzer same_round_analyzer(same_round_exe); const auto same_round_result = same_round_analyzer.analyze_exact_entries({kLoad}); const auto same_round_starts = starts(same_round_result); CHECK(same_round_starts == std::set({kLoad, kLoad + 0x100}), "same-round call target inside a discovered host stays absorbed"); bool same_round_hosted = false; for (const auto& absorbed : same_round_result.absorbed_entries) { same_round_hosted |= (absorbed.addr == kLoad + 0x110 && absorbed.host_start == kLoad + 0x100 && absorbed.addr < absorbed.host_end && absorbed.source_addr == kLoad + 0x100 && !absorbed.resolved_indirect); } CHECK(same_round_hosted, "every analyzer-proven absorbed target names its final CFG host"); // Resolved constant-register calls carry the same reachable provenance as // direct JALs and can therefore become final hosted aliases too. auto indirect_alias_image = make_exe_buffer(0x1000); put32(indirect_alias_image, text + 0x00, jal(kLoad + 0x100)); put32(indirect_alias_image, text + 0x04, 0x00000000u); put32(indirect_alias_image, text + 0x08, 0x03E00008u); put32(indirect_alias_image, text + 0x0C, 0x00000000u); put32(indirect_alias_image, text + 0x100, 0x3C088001u); put32(indirect_alias_image, text + 0x104, 0x25080110u); put32(indirect_alias_image, text + 0x108, 0x0100F809u); put32(indirect_alias_image, text + 0x10C, 0x00000000u); put32(indirect_alias_image, text + 0x110, 0x03E00008u); put32(indirect_alias_image, text + 0x114, 0x00000000u); auto indirect_alias_exe = parse(indirect_alias_image); PSXRecomp::FunctionAnalyzer indirect_alias_analyzer(indirect_alias_exe); const auto indirect_alias_result = indirect_alias_analyzer.analyze_exact_entries({kLoad}); bool indirect_alias_hosted = false; for (const auto& absorbed : indirect_alias_result.absorbed_entries) { indirect_alias_hosted |= (absorbed.addr == kLoad + 0x110 && absorbed.host_start == kLoad + 0x100 && absorbed.source_addr == kLoad + 0x108 && absorbed.resolved_indirect); } CHECK(indirect_alias_hosted, "resolved indirect call target exports final alias provenance"); // The ordering can span rounds too: B is found first, then B discovers an // earlier A whose reachable body contains B. Once A exists, ownership must // be recomputed without retaining B as a stale hard cap. auto later_round_image = make_exe_buffer(0x1000); put32(later_round_image, text + 0x00, jal(kLoad + 0x120)); put32(later_round_image, text + 0x04, 0x00000000u); put32(later_round_image, text + 0x08, 0x03E00008u); put32(later_round_image, text + 0x0C, 0x00000000u); put32(later_round_image, text + 0x100, 0x10000007u); // b 0x80010120 put32(later_round_image, text + 0x104, 0x00000000u); put32(later_round_image, text + 0x120, jal(kLoad + 0x100)); put32(later_round_image, text + 0x124, 0x00000000u); put32(later_round_image, text + 0x128, 0x03E00008u); put32(later_round_image, text + 0x12C, 0x00000000u); auto later_round_exe = parse(later_round_image); PSXRecomp::FunctionAnalyzer later_round_analyzer(later_round_exe); const auto later_round_starts = starts( later_round_analyzer.analyze_exact_entries({kLoad})); CHECK(later_round_starts == std::set({kLoad, kLoad + 0x100}), "later-discovered host retracts its stale interior hard cap"); // R discovers A and B; A initially owns B. Walking that expanded A then // discovers X between them. X caps A before B and does not itself reach B, // so final ownership must reconsider B and restore it as a real root. auto invalidated_owner_image = make_exe_buffer(0x1000); put32(invalidated_owner_image, text + 0x00, jal(kLoad + 0x100)); put32(invalidated_owner_image, text + 0x04, 0x00000000u); put32(invalidated_owner_image, text + 0x08, jal(kLoad + 0x140)); put32(invalidated_owner_image, text + 0x0C, 0x00000000u); put32(invalidated_owner_image, text + 0x10, 0x03E00008u); put32(invalidated_owner_image, text + 0x14, 0x00000000u); put32(invalidated_owner_image, text + 0x100, 0x08000000u | (((kLoad + 0x130) >> 2) & 0x03FFFFFFu)); put32(invalidated_owner_image, text + 0x104, 0x00000000u); put32(invalidated_owner_image, text + 0x120, 0x03E00008u); // X put32(invalidated_owner_image, text + 0x124, 0x00000000u); put32(invalidated_owner_image, text + 0x130, 0x24420001u); put32(invalidated_owner_image, text + 0x134, 0x24420001u); put32(invalidated_owner_image, text + 0x138, 0x24420001u); put32(invalidated_owner_image, text + 0x13C, 0x24420001u); put32(invalidated_owner_image, text + 0x140, jal(kLoad + 0x120)); // B -> X put32(invalidated_owner_image, text + 0x144, 0x00000000u); put32(invalidated_owner_image, text + 0x148, 0x03E00008u); put32(invalidated_owner_image, text + 0x14C, 0x00000000u); auto invalidated_owner_exe = parse(invalidated_owner_image); PSXRecomp::FunctionAnalyzer invalidated_owner_analyzer( invalidated_owner_exe); const auto invalidated_owner_result = invalidated_owner_analyzer.analyze_exact_entries({kLoad}); const auto invalidated_owner_starts = starts(invalidated_owner_result); CHECK(invalidated_owner_starts.count(kLoad + 0x100) && invalidated_owner_starts.count(kLoad + 0x120) && invalidated_owner_starts.count(kLoad + 0x140), "R/A/X/B ownership invalidation restores B as a final root"); bool stale_b_alias = false; for (const auto& absorbed : invalidated_owner_result.absorbed_entries) stale_b_alias |= absorbed.addr == kLoad + 0x140; CHECK(!stale_b_alias, "R/A/X/B final ownership does not retain stale B alias"); // Producer boundaries are hard CFG ownership boundaries. A call into the // next producer is rejected without approval and, when approved, becomes // its own root rather than being absorbed by fallthrough from producer A. auto producer_owner_image = make_exe_buffer(0x1000); put32(producer_owner_image, text + 0x00, jal(kLoad + 0x100)); put32(producer_owner_image, text + 0x04, 0x00000000u); put32(producer_owner_image, text + 0x08, jal(kLoad + 0x108)); put32(producer_owner_image, text + 0x0C, 0x00000000u); put32(producer_owner_image, text + 0x10, 0x03E00008u); put32(producer_owner_image, text + 0x14, 0x00000000u); put32(producer_owner_image, text + 0x100, 0x24420001u); put32(producer_owner_image, text + 0x104, 0x24420001u); put32(producer_owner_image, text + 0x108, 0x03E00008u); put32(producer_owner_image, text + 0x10C, 0x00000000u); auto producer_owner_exe = parse(producer_owner_image); const std::vector> ownership_ranges = { {kLoad, kLoad + 0x108}, {kLoad + 0x108, kLoad + 0x1000}}; PSXRecomp::FunctionAnalyzer rejected_owner_analyzer(producer_owner_exe); const auto rejected_owner = rejected_owner_analyzer.analyze_exact_entries( {kLoad}, ownership_ranges, {}); CHECK(!starts(rejected_owner).count(kLoad + 0x108) && rejected_owner.absorbed_entries.empty(), "rejected cross-producer call is neither root nor alias"); PSXRecomp::FunctionAnalyzer allowed_owner_analyzer(producer_owner_exe); const auto allowed_owner = allowed_owner_analyzer.analyze_exact_entries( {kLoad}, ownership_ranges, {kLoad + 0x108}); CHECK(starts(allowed_owner).count(kLoad + 0x108), "approved cross-producer call remains a separate root"); bool cross_absorbed = false; for (const auto& absorbed : allowed_owner.absorbed_entries) cross_absorbed |= absorbed.addr == kLoad + 0x108; CHECK(!cross_absorbed, "approved cross-producer target is never absorbed across boundary"); // An uncovered composite gap has no producer identity. Two gap addresses // both returning producer -1 must not become a synthetic shared producer. auto producer_gap_image = make_exe_buffer(0x1000); put32(producer_gap_image, text + 0x00, 0x03E00008u); put32(producer_gap_image, text + 0x04, 0x00000000u); put32(producer_gap_image, text + 0x60, 0x24020001u); put32(producer_gap_image, text + 0x64, 0x03E00008u); put32(producer_gap_image, text + 0x68, 0x00000000u); const auto producer_gap_exe = parse(producer_gap_image); PSXRecomp::FunctionAnalyzer producer_gap_analyzer(producer_gap_exe); const std::vector> gap_ranges = { {kLoad, kLoad + 0x40}, {kLoad + 0x80, kLoad + 0x100}}; const auto producer_gap_result = producer_gap_analyzer.analyze_exact_entries( {kLoad, kLoad + 0x60}, gap_ranges, {}); CHECK(starts(producer_gap_result) == std::set({kLoad}) && !producer_gap_result.exact_reachable_pcs.count(kLoad + 0x60), "explicit roots in uncovered producer gaps fail closed"); // R reaches S, S calls B, and B calls earlier X. X then caps R before S, // invalidating B's only source edge. Recomputing evidence oscillates, so the // analyzer must fail closed to explicit R instead of retaining B forever. auto stale_source_image = make_exe_buffer(0x1000); put32(stale_source_image, text + 0x00, 0x08000000u | (((kLoad + 0x100) >> 2) & 0x03FFFFFFu)); put32(stale_source_image, text + 0x04, 0x00000000u); put32(stale_source_image, text + 0x80, 0x03E00008u); // X put32(stale_source_image, text + 0x84, 0x00000000u); put32(stale_source_image, text + 0x100, jal(kLoad + 0x140)); // S -> B put32(stale_source_image, text + 0x104, 0x00000000u); put32(stale_source_image, text + 0x108, 0x03E00008u); put32(stale_source_image, text + 0x10C, 0x00000000u); put32(stale_source_image, text + 0x140, jal(kLoad + 0x80)); // B -> X put32(stale_source_image, text + 0x144, 0x00000000u); put32(stale_source_image, text + 0x148, 0x03E00008u); put32(stale_source_image, text + 0x14C, 0x00000000u); auto stale_source_exe = parse(stale_source_image); PSXRecomp::FunctionAnalyzer stale_source_analyzer(stale_source_exe); const auto stale_source_result = stale_source_analyzer.analyze_exact_entries({kLoad}); CHECK(starts(stale_source_result) == std::set({kLoad}) && stale_source_result.absorbed_entries.empty(), "cyclic stale call evidence fails closed to explicit roots"); // beq rN,rN is unconditional: PC+8 is not reachable and a hidden JAL // there cannot manufacture a function or absorbed alias. auto always_branch_image = make_exe_buffer(0x1000); put32(always_branch_image, text + 0x00, (0x04u << 26) | (8u << 21) | (8u << 16) | 0x1Fu); // -> +0x80 put32(always_branch_image, text + 0x04, 0x00000000u); put32(always_branch_image, text + 0x08, jal(kLoad + 0x40)); put32(always_branch_image, text + 0x0C, 0x00000000u); put32(always_branch_image, text + 0x40, 0x03E00008u); put32(always_branch_image, text + 0x44, 0x00000000u); put32(always_branch_image, text + 0x80, 0x03E00008u); put32(always_branch_image, text + 0x84, 0x00000000u); auto always_branch_exe = parse(always_branch_image); PSXRecomp::FunctionAnalyzer always_branch_analyzer(always_branch_exe); const auto always_branch_result = always_branch_analyzer.analyze_exact_entries({kLoad}); CHECK(starts(always_branch_result) == std::set({kLoad}) && always_branch_result.absorbed_entries.empty(), "always-taken branch excludes unreachable fallthrough call"); // bne rN,rN is never taken: its encoded target cannot contribute a hidden // call edge, while its ordinary delay slot and PC+8 remain reachable. auto never_branch_image = make_exe_buffer(0x1000); put32(never_branch_image, text + 0x00, (0x05u << 26) | (8u << 21) | (8u << 16) | 0x1Fu); // false -> +0x80 put32(never_branch_image, text + 0x04, 0x00000000u); put32(never_branch_image, text + 0x08, 0x03E00008u); put32(never_branch_image, text + 0x0C, 0x00000000u); put32(never_branch_image, text + 0x40, 0x03E00008u); put32(never_branch_image, text + 0x44, 0x00000000u); put32(never_branch_image, text + 0x80, jal(kLoad + 0x40)); put32(never_branch_image, text + 0x84, 0x00000000u); put32(never_branch_image, text + 0x88, 0x03E00008u); put32(never_branch_image, text + 0x8C, 0x00000000u); auto never_branch_exe = parse(never_branch_image); PSXRecomp::FunctionAnalyzer never_branch_analyzer(never_branch_exe); const auto never_branch_result = never_branch_analyzer.analyze_exact_entries({kLoad}); CHECK(starts(never_branch_result) == std::set({kLoad}) && never_branch_result.absorbed_entries.empty(), "never-taken branch excludes unreachable target call"); // bgezal $zero is the BAL alias: the target is call evidence and the // return continuation at PC+8 remains reachable. auto bal_image = make_exe_buffer(0x1000); put32(bal_image, text + 0x00, (0x01u << 26) | (0x11u << 16) | 0x1Fu); // bal +0x80 put32(bal_image, text + 0x04, 0x00000000u); put32(bal_image, text + 0x08, jal(kLoad + 0x40)); put32(bal_image, text + 0x0C, 0x00000000u); put32(bal_image, text + 0x10, 0x03E00008u); put32(bal_image, text + 0x14, 0x00000000u); put32(bal_image, text + 0x40, 0x03E00008u); put32(bal_image, text + 0x44, 0x00000000u); put32(bal_image, text + 0x80, 0x03E00008u); put32(bal_image, text + 0x84, 0x00000000u); auto bal_exe = parse(bal_image); PSXRecomp::FunctionAnalyzer bal_analyzer(bal_exe); const auto bal_starts = starts(bal_analyzer.analyze_exact_entries({kLoad})); CHECK(bal_starts.count(kLoad + 0x40) && bal_starts.count(kLoad + 0x80), "BAL preserves call target and return-continuation discovery"); // A synthetic adjacent-producer envelope is not one linked image. The // caller supplies the cross-boundary targets that passed its independent // callable-CFG proof; other valid-looking targets stay with the interpreter. PSXRecomp::FunctionAnalyzer composite_analyzer(exe); const std::vector> producer_ranges = { {kLoad, kLoad + 0x80}, {kLoad + 0x80, kLoad + 0x1000}}; const auto composite = starts(composite_analyzer.analyze_exact_entries( {kLoad}, producer_ranges, {kLoad + 0x100})); CHECK(composite.count(kLoad) && composite.count(kLoad + 0x100), "approved cross-producer call target is discovered"); CHECK(!composite.count(kLoad + 0x300), "unapproved cross-producer JAL target fails closed"); // A constant-register JR out of the current function is a statically // proven tail call. Resolve the common lui/addiu/jr sequence without an // execution-derived seed. auto indirect_image = make_exe_buffer(0x1000, kLoad + 0x500); put32(indirect_image, text + 0x400, 0x24020001u); put32(indirect_image, text + 0x404, 0x03E00008u); put32(indirect_image, text + 0x408, 0x00000000u); put32(indirect_image, text + 0x500, 0x3C088001u); // lui $t0,0x8001 put32(indirect_image, text + 0x504, 0x25080400u); // addiu $t0,$t0,0x400 put32(indirect_image, text + 0x508, 0x01000008u); // jr $t0 put32(indirect_image, text + 0x50C, 0x00000000u); auto indirect_exe = parse(indirect_image); PSXRecomp::FunctionAnalyzer indirect_analyzer(indirect_exe); const auto indirect = starts( indirect_analyzer.analyze_exact_entries({kLoad + 0x500})); CHECK(indirect.count(kLoad + 0x400) && indirect.count(kLoad + 0x500), "constant-register JR discovers its frameless tail-call target"); // A jump enters at the low-half definition and skips the preceding LUI. // A raw backward scan would combine them; reaching-definition proof must // reject that synthetic indirect target. auto split_constant_image = make_exe_buffer(0x1000, kLoad + 0x500); put32(split_constant_image, text + 0x400, 0x03E00008u); put32(split_constant_image, text + 0x404, 0x00000000u); put32(split_constant_image, text + 0x500, 0x08000000u | (((kLoad + 0x518) >> 2) & 0x03FFFFFFu)); put32(split_constant_image, text + 0x504, 0x00000000u); put32(split_constant_image, text + 0x514, 0x3C088001u); put32(split_constant_image, text + 0x518, 0x25080400u); put32(split_constant_image, text + 0x51C, 0x01000008u); put32(split_constant_image, text + 0x520, 0x00000000u); auto split_constant_exe = parse(split_constant_image); PSXRecomp::FunctionAnalyzer split_constant_analyzer(split_constant_exe); const auto split_constant = starts( split_constant_analyzer.analyze_exact_entries({kLoad + 0x500})); CHECK(!split_constant.count(kLoad + 0x400), "indirect resolver rejects a definition skipped by an inbound edge"); // The LUI executes in a JAL delay slot, then the callee may clobber t0 // before returning to the low half. It is not a reaching definition for // the later JALR even though the raw words are adjacent. auto call_delay_image = make_exe_buffer(0x1000, kLoad + 0x500); put32(call_delay_image, text + 0x300, 0x03E00008u); put32(call_delay_image, text + 0x304, 0x00000000u); put32(call_delay_image, text + 0x400, 0x03E00008u); put32(call_delay_image, text + 0x404, 0x00000000u); put32(call_delay_image, text + 0x500, jal(kLoad + 0x300)); put32(call_delay_image, text + 0x504, 0x3C088001u); // call delay LUI put32(call_delay_image, text + 0x508, 0x25080400u); put32(call_delay_image, text + 0x50C, 0x0100F809u); put32(call_delay_image, text + 0x510, 0x00000000u); put32(call_delay_image, text + 0x514, 0x03E00008u); put32(call_delay_image, text + 0x518, 0x00000000u); auto call_delay_exe = parse(call_delay_image); PSXRecomp::FunctionAnalyzer call_delay_analyzer(call_delay_exe); const auto call_delay = starts( call_delay_analyzer.analyze_exact_entries({kLoad + 0x500})); CHECK(call_delay.count(kLoad + 0x300) && !call_delay.count(kLoad + 0x400), "indirect resolver rejects a LUI in a call delay slot"); // Ape Escape's save-screen dispatcher builds its table base across // registers (lui t0; addiu s0,t0,lo). Prove the complete bounded switch, // then make every adversarial mutation fail closed as one whole table. auto ape_switch = make_ape_switch(); auto ape_switch_exe = parse(ape_switch.image); PSXRecomp::ExactJumpTable ape_table; CHECK(PSXRecomp::resolve_exact_bounded_jump_table( ape_switch_exe, ape_switch.entry, kLoad + 0x1000u, ape_switch.jr_pc, 2u, ape_table) && ape_table.table_base == ape_switch.table && ape_table.table_count == ape_switch.cases.size() && ape_table.targets.size() == ape_switch.cases.size(), "Ape cross-register bounded switch resolves exactly"); PSXRecomp::FunctionAnalyzer ape_switch_analyzer(ape_switch_exe); const auto ape_result = ape_switch_analyzer.analyze_exact_entries( {ape_switch.entry}); bool all_ape_cases_reachable = true; for (uint32_t target : ape_switch.cases) all_ape_cases_reachable &= ape_result.exact_reachable_pcs.count(target) != 0; CHECK(all_ape_cases_reachable, "exact reachable discovery follows every validated Ape case"); auto ori_switch = make_ape_switch(); put32(ori_switch.image, text + 0x504, 0x35100A00u); // ori s0,t0,0x0a00 CHECK(!resolves_ape_switch(ori_switch, ori_switch.entry), "unproven ORI table constants fail closed"); auto no_lw_nop_switch = make_ape_switch(); no_lw_nop_switch.jr_pc = kLoad + 0x524u; put32(no_lw_nop_switch.image, text + 0x524, 0x00400008u); put32(no_lw_nop_switch.image, text + 0x528, 0x00000000u); CHECK(resolves_ape_switch(no_lw_nop_switch, no_lw_nop_switch.entry), "zero-NOP LW-to-JR schedule matches Python parity"); auto two_lw_nop_switch = make_ape_switch(); two_lw_nop_switch.jr_pc = kLoad + 0x52Cu; put32(two_lw_nop_switch.image, text + 0x528, 0x00000000u); put32(two_lw_nop_switch.image, text + 0x52C, 0x00400008u); CHECK(!resolves_ape_switch(two_lw_nop_switch, two_lw_nop_switch.entry), "two-NOP LW-to-JR schedule fails closed"); auto wrong_jr_switch = make_ape_switch(); put32(wrong_jr_switch.image, text + 0x528, 0x00400808u); // reserved rd field makes this noncanonical JR CHECK(!resolves_ape_switch(wrong_jr_switch, wrong_jr_switch.entry), "jump-table proof checks the exact JR encoding"); auto noncanonical_lui_switch = make_ape_switch(); put32(noncanonical_lui_switch.image, text + 0x500, 0x3C288001u); // LUI's reserved rs field is nonzero CHECK(!resolves_ape_switch(noncanonical_lui_switch, noncanonical_lui_switch.entry), "jump-table proof rejects noncanonical LUI reserved fields"); auto cross_producer_switch = make_ape_switch(); CHECK(!resolves_ape_switch(cross_producer_switch, cross_producer_switch.entry, kLoad + 0x400u, kLoad + 0x900u), "producer A cannot borrow table slots from adjacent producer B"); auto producer_case_escape = make_ape_switch(); put32(producer_case_escape.image, text + 0x540, 0x08000000u | (((kLoad + 0xB20u) >> 2) & 0x03FFFFFFu)); put32(producer_case_escape.image, text + 0x544, 0x00000000u); put32(producer_case_escape.image, text + 0xB20, 0x08000000u | (((kLoad + 0x518u) >> 2) & 0x03FFFFFFu)); put32(producer_case_escape.image, text + 0xB24, 0x00000000u); CHECK(!resolves_ape_switch(producer_case_escape, producer_case_escape.entry, kLoad + 0x400u, kLoad + 0xA20u), "case walk cannot escape through an adjacent producer"); auto cross_producer_exe = parse(cross_producer_switch.image); PSXRecomp::FunctionAnalyzer cross_producer_analyzer(cross_producer_exe); const auto cross_producer_result = cross_producer_analyzer.analyze_exact_entries( {cross_producer_switch.entry}, {{kLoad + 0x400u, kLoad + 0x900u}, {kLoad + 0x900u, kLoad + 0x1000u}}); CHECK(!cross_producer_result.exact_reachable_pcs.count(kLoad + 0x540u), "reachable discovery rejects a cross-producer table"); CHECK(!cross_producer_result.functions.empty() && cross_producer_result.functions.front().producer_lo == kLoad + 0x400u && cross_producer_result.functions.front().producer_hi == kLoad + 0x900u, "exact functions retain producer bounds for code generation"); auto invalid_target_switch = make_ape_switch(); put32(invalid_target_switch.image, text + 0x550, 0xFFFFFFFFu); CHECK(!resolves_ape_switch(invalid_target_switch, invalid_target_switch.entry), "one invalid target instruction rejects the entire table"); auto clobbered_switch = make_ape_switch(); put32(clobbered_switch.image, text + 0x508, 0x24100000u); // addiu s0,zero,0 CHECK(!resolves_ape_switch(clobbered_switch, clobbered_switch.entry), "table-base clobber rejects the bounded switch"); auto skipped_def_switch = make_ape_switch(); put32(skipped_def_switch.image, text + 0x700, 0x08000000u | (((kLoad + 0x504u) >> 2) & 0x03FFFFFFu)); put32(skipped_def_switch.image, text + 0x704, 0x00000000u); CHECK(!resolves_ape_switch(skipped_def_switch, skipped_def_switch.entry), "later host bytes cannot jump into and skip a protected definition"); auto bad_bound_switch = make_ape_switch(); put32(bad_bound_switch.image, text + 0x50C, 0x2C820003u); // sltiu v0,a0,3: wrong index dependency CHECK(!resolves_ape_switch(bad_bound_switch, bad_bound_switch.entry), "unrelated SLTIU bound cannot validate a jump table"); auto outside_target_switch = make_ape_switch(); put32(outside_target_switch.image, text + 0xA04, kLoad + 0x2000u); CHECK(!resolves_ape_switch(outside_target_switch, outside_target_switch.entry), "one out-of-image table target rejects the entire table"); // A packed BIOS thunk is also a backward-scan boundary. The frameless // leaf after its delay slot must not be swallowed into the thunk run. auto thunk_image = make_exe_buffer(0x1000); put32(thunk_image, text + 0x600, 0x240A00B0u); put32(thunk_image, text + 0x604, 0x01400008u); put32(thunk_image, text + 0x608, 0x24090008u); put32(thunk_image, text + 0x60C, 0x00000000u); put32(thunk_image, text + 0x610, 0x24020001u); put32(thunk_image, text + 0x614, 0x03E00008u); put32(thunk_image, text + 0x618, 0x00000000u); auto thunk_exe = parse(thunk_image); PSXRecomp::FunctionAnalyzer thunk_analyzer(thunk_exe); const auto thunk_starts = starts(thunk_analyzer.analyze()); CHECK(thunk_starts.count(kLoad + 0x600) && thunk_starts.count(kLoad + 0x610), "packed BIOS thunk bounds the following frameless leaf"); // ---- No-split guard on EXPLICIT entries (overlay mode) ---------------- // A root is a hard cap, so an explicit entry that the entry below it // reaches by fallthrough splits that function (beads-eio.3.177: Tomba // X00 root 0x8011B1CC, a subu mid-way through host 0x8011AC10). { auto split_image = make_exe_buffer(0x1000); put32(split_image, text + 0x100, 0x27BDFFE0u); // addiu sp,sp,-32 put32(split_image, text + 0x104, 0x24420001u); put32(split_image, text + 0x108, 0x00431023u); // subu v0,v0,v1 put32(split_image, text + 0x10C, 0x03E00008u); put32(split_image, text + 0x110, 0x27BD0020u); auto split_exe = parse(split_image); PSXRecomp::FunctionAnalyzer unguarded(split_exe); CHECK(starts(unguarded.analyze_exact_entries( {kLoad + 0x100, kLoad + 0x108})) == std::set({kLoad + 0x100, kLoad + 0x108}), "without the guard an explicit mid-function entry still caps"); PSXRecomp::FunctionAnalyzer guarded(split_exe); const auto guarded_result = guarded.analyze_exact_entries( {kLoad + 0x100, kLoad + 0x108}, {}, {}, true); CHECK(starts(guarded_result) == std::set({kLoad + 0x100}), "no-split guard absorbs an explicit entry reached by fallthrough"); bool aliased = false; for (const auto& absorbed : guarded_result.absorbed_entries) aliased |= absorbed.addr == kLoad + 0x108 && absorbed.host_start == kLoad + 0x100 && absorbed.addr < absorbed.host_end; CHECK(aliased, "absorbed explicit entry is exported as a hosted alias"); PSXRecomp::FunctionAnalyzer trusted(split_exe); CHECK(starts(trusted.analyze_exact_entries( {kLoad + 0x100, kLoad + 0x108}, {}, {}, true, {kLoad + 0x108})) == std::set({kLoad + 0x100, kLoad + 0x108}), "trusted (promoted dispatch) entries are exempt from the guard"); } { // Every case label of a switch is an explicit entry. Each one caps // the host below its siblings, so the table (which resolves only when // all of its targets are inside the walk) is only visible when the // whole run of case labels is absorbed together. auto sw = make_ape_switch(); auto sw_exe = parse(sw.image); std::vector entries = {sw.entry}; entries.insert(entries.end(), sw.cases.begin(), sw.cases.end()); PSXRecomp::FunctionAnalyzer guarded(sw_exe); const auto result = guarded.analyze_exact_entries(entries, {}, {}, true); CHECK(starts(result) == std::set({sw.entry}), "no-split guard absorbs every case-label entry of a switch host"); size_t hosted = 0; for (const auto& absorbed : result.absorbed_entries) hosted += absorbed.host_start == sw.entry; CHECK(hosted == sw.cases.size(), "every absorbed case label is hosted by the switch function"); } { // A forward tail call over an unrelated function must not demote the // tail-call target: the intervening root would leave it hostless. auto tail_image = make_exe_buffer(0x1000); put32(tail_image, text + 0x700, 0x08000000u | (((kLoad + 0x740) >> 2) & 0x03FFFFFFu)); put32(tail_image, text + 0x704, 0x00000000u); put32(tail_image, text + 0x720, 0x03E00008u); put32(tail_image, text + 0x724, 0x00000000u); put32(tail_image, text + 0x740, 0x24020001u); put32(tail_image, text + 0x744, 0x03E00008u); put32(tail_image, text + 0x748, 0x00000000u); auto tail_exe = parse(tail_image); PSXRecomp::FunctionAnalyzer guarded(tail_exe); CHECK(starts(guarded.analyze_exact_entries( {kLoad + 0x700, kLoad + 0x720, kLoad + 0x740}, {}, {}, true)) == std::set( {kLoad + 0x700, kLoad + 0x720, kLoad + 0x740}), "tail call over an unrelated root keeps its target a root"); } PSXRecomp::FunctionAnalyzer seeded_analyzer(exe); const auto seeded = starts( seeded_analyzer.analyze_exact_entries({kLoad, kLoad + 0x200})); CHECK(seeded.count(kLoad + 0x200), "evidence-backed explicit callback seed is compiled"); auto bad = parse(image); const auto original_size = bad.code_data.size(); CHECK(!PSXRecomp::apply_static_analysis_bound(bad, 0x1800, error), "non-page-aligned truncation is rejected"); CHECK(bad.code_size() == 0x2000 && bad.code_data.size() == original_size, "invalid bound leaves parsed image unchanged"); CHECK(!PSXRecomp::apply_static_analysis_bound(bad, 0x3000, error), "oversized noncanonical bound is rejected"); CHECK(!PSXRecomp::apply_static_analysis_bound(bad, 0, error), "zero bound is rejected"); auto excludes_entry = parse(image); excludes_entry.header.initial_pc = kLoad + 0x1000; CHECK(!PSXRecomp::apply_static_analysis_bound(excludes_entry, 0x1000, error), "bound excluding entry point is rejected"); auto rounded = parse(make_exe_buffer(0x1800)); CHECK(PSXRecomp::apply_static_analysis_bound(rounded, 0x2000, error), "canonical generated final-page reservation remains compatible"); CHECK(rounded.code_size() == 0x1800, "page reservation does not widen static analysis payload"); // Retail PS-X EXEs may declare BSS inside a sector-rounded payload. The // overlap is safe only when every overlapped file byte is padding zero. auto zero_padded_bss = make_exe_buffer(0x1000); put32(zero_padded_bss, 0x28, kLoad + 0xDBC); // 0x244-byte overlap put32(zero_padded_bss, 0x2C, 0x400); std::string zero_bss_error; const auto zero_bss_exe = PSXRecomp::PS1ExeParser::parse_buffer( zero_padded_bss, zero_bss_error); CHECK(zero_bss_exe.has_value(), "zero-only BSS/payload overlap is accepted"); auto nonzero_bss = zero_padded_bss; nonzero_bss[text + 0xE00] = 1; std::string nonzero_bss_error; const auto nonzero_bss_exe = PSXRecomp::PS1ExeParser::parse_buffer( nonzero_bss, nonzero_bss_error); CHECK(!nonzero_bss_exe.has_value(), "non-zero BSS/code overlap remains rejected"); CHECK(nonzero_bss_error.find("overlaps non-zero code/data") != std::string::npos, "non-zero overlap reports the unsafe bytes"); auto before_code_bss = zero_padded_bss; put32(before_code_bss, 0x28, kLoad - 4); std::string before_code_error; CHECK(!PSXRecomp::PS1ExeParser::parse_buffer( before_code_bss, before_code_error).has_value(), "BSS beginning before the payload remains rejected"); // The KSEG0 main-RAM decode window is 8 MiB: an image placed in a retail // mirror (a mod engine copied to 0x80780000) is valid, one crossing the // window end is not. auto mirror_image = make_exe_buffer(0x1000); put32(mirror_image, 0x10, 0x80780000u); put32(mirror_image, 0x18, 0x80780000u); std::string mirror_error; CHECK(PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error).has_value(), "image in the 4th RAM mirror is accepted"); put32(mirror_image, 0x10, 0x807FF800u); put32(mirror_image, 0x18, 0x807FF800u); CHECK(!PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error).has_value() && mirror_error.find("beyond 0x80800000") != std::string::npos, "image crossing the RAM decode window end is rejected"); // A KUSEG header names the same RAM as KSEG0 but keeps its segment: the // program runs at KUSEG PCs, so it compiles there // (docs/SEGMENT_AWARE_CODE.md ยง5.3). put32(mirror_image, 0x10, 0x00780000u); put32(mirror_image, 0x18, 0x00780000u); { auto kuseg = PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error); CHECK(kuseg.has_value() && kuseg->load_address() == 0x00780000u && kuseg->header.initial_pc == 0x00780000u && kuseg->link_segment() == PSXRecomp::kSegKUSEG && kuseg->phys_load_address() == 0x00780000u && kuseg->contains_phys(0x80780FFCu) && !kuseg->contains_phys(0x00781000u) && kuseg->read_word(0x00780000u).has_value() && !kuseg->read_word(0x80780000u).has_value(), "a KUSEG header keeps its segment; bytes are addressed physically"); } put32(mirror_image, 0x10, 0x007FF800u); put32(mirror_image, 0x18, 0x007FF800u); CHECK(!PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error).has_value() && mirror_error.find("beyond 0x00800000") != std::string::npos, "a KUSEG image crossing its RAM window end is rejected"); // KSEG1 is accepted as an uncached home segment. put32(mirror_image, 0x10, 0xA0010000u); put32(mirror_image, 0x18, 0xA0010000u); { auto kseg1 = PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error); CHECK(kseg1.has_value() && kseg1->link_segment() == PSXRecomp::kSegKSEG1, "a KSEG1-linked header is accepted in its own segment"); } // One image, one link segment. put32(mirror_image, 0x10, 0x80010000u); put32(mirror_image, 0x18, 0x00010000u); CHECK(!PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error).has_value() && mirror_error.find("different segments") != std::string::npos, "an entry in another segment than the load address is rejected"); // Outside main RAM (KSEG2, the BIOS ROM) is not a PS-X EXE target. put32(mirror_image, 0x10, 0xBFC00000u); put32(mirror_image, 0x18, 0xBFC00000u); CHECK(!PSXRecomp::PS1ExeParser::parse_buffer(mirror_image, mirror_error).has_value() && mirror_error.find("Must be in main RAM") != std::string::npos, "a load address outside the main-RAM windows is rejected"); if (failures) { std::fprintf(stderr, "FAILED (%d)\n", failures); return 1; } std::puts("ALL PASS"); return 0; }