From fe2c0046ba35b3f2741ec2d1903decd08effe3d9 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 11 Sep 2026 13:38:06 -0700 Subject: [PATCH] Recognize bounded MIPS switches with scheduled table constants --- recompiler/include/function_analysis.h | 5 +- recompiler/src/function_analysis.cpp | 51 ++++++++--- recompiler/tests/reachable_discovery_test.cpp | 86 +++++++++++++++++++ recompiler/tests/recompiler_patch_test.cpp | 23 +++++ .../tests/test_aot_overlay_discovery.py | 76 ++++++++++++++++ tools/aot_overlay_spike/README.md | 21 +++-- tools/compile_overlays.py | 46 ++++++++-- 7 files changed, 281 insertions(+), 27 deletions(-) diff --git a/recompiler/include/function_analysis.h b/recompiler/include/function_analysis.h index b3260caf..ea887462 100644 --- a/recompiler/include/function_analysis.h +++ b/recompiler/include/function_analysis.h @@ -79,7 +79,10 @@ using ExactAddressMapper = uint32_t (*)(uint32_t, const PS1Executable&); // Recognize only the canonical bounded-switch dependency chain: // sltiu/beq guard -> sll index,2 -> addu table address -> lw target -> jr. // The table constant may use either same-register or cross-register -// `lui source; addiu base,source,lo`. `producer_lo/producer_hi` bound both +// `lui source; addiu base,source,lo`, either before the guard or scheduled +// exactly as `sltiu; beq; lui (delay slot); addiu; sll` without clobbering +// the checked index or allowing direct edges to bypass the guard. +// `producer_lo/producer_hi` bound both // table storage and case code for composite images; zero/zero means the full // executable. Every dependency, table word, and target must pass the hard // safety checks; otherwise the whole table is rejected. diff --git a/recompiler/src/function_analysis.cpp b/recompiler/src/function_analysis.cpp index ede40acc..ef721a62 100644 --- a/recompiler/src/function_analysis.cpp +++ b/recompiler/src/function_analysis.cpp @@ -894,16 +894,36 @@ bool resolve_exact_bounded_jump_table( return false; } - // Exact canonical guard: sltiu; beq; nop; sll. This is the same accepted - // suffix as the Python capture verifier and excludes unrelated bounds. + // Exact guards: sltiu; beq; nop; sll, or sltiu; beq; lui; addiu; sll. + // The latter schedules the table's LUI in the bounds branch's delay slot. + // Keep both forms in parity with the Python capture verifier. if (sll_pc < entry + 12u) return false; uint32_t guard_pc = sll_pc - 8u; uint32_t bound_pc = sll_pc - 12u; + auto before_scale = read(sll_pc - 4u); + if (!before_scale.has_value()) return false; + bool scheduled_base = *before_scale != 0u; + if (scheduled_base) { + // R3000 JR would see the old target register without this load delay. + if (lw_pc != jr_pc - 8u) return false; + if (sll_pc < entry + 16u) return false; + guard_pc -= 4u; + bound_pc -= 4u; + auto upper_word = read(sll_pc - 8u); + if (!upper_word.has_value()) return false; + uint32_t upper_reg = (*upper_word >> 16) & 0x1Fu; + if ((*upper_word >> 26) != 0x0Fu || + ((*upper_word >> 21) & 0x1Fu) != 0u || upper_reg == 0u || + (*before_scale >> 26) != 0x09u || + ((*before_scale >> 21) & 0x1Fu) != upper_reg || + ((*before_scale >> 16) & 0x1Fu) != base_reg || + upper_reg == index_reg || base_reg == index_reg) { + return false; + } + } auto guard_word_opt = read(guard_pc); auto bound_word = read(bound_pc); - auto guard_delay = read(sll_pc - 4u); - if (!guard_word_opt.has_value() || !bound_word.has_value() || - !guard_delay.has_value() || *guard_delay != 0u) { + if (!guard_word_opt.has_value() || !bound_word.has_value()) { return false; } uint32_t guard_word = *guard_word_opt; @@ -920,20 +940,23 @@ bool resolve_exact_bounded_jump_table( uint32_t bound_rt = (*bound_word >> 16) & 0x1Fu; uint32_t count = *bound_word & 0xFFFFu; if (bound_op != 0x0Bu || bound_rs != index_reg || - bound_rt != bound_reg || count == 0u || count >= 512u || + bound_rt != bound_reg || bound_reg == index_reg || + count == 0u || count >= 512u || in_delay_slot(bound_pc)) { return false; } uint32_t guard_target = exact_branch_target(guard_pc, guard_word); if ((guard_target & 3u) != 0u || guard_target < entry || guard_target >= hard_cap || - (guard_target >= sll_pc && guard_target < jr_pc + 8u)) { + (guard_target >= sll_pc && guard_target < jr_pc + 8u) || + (scheduled_base && guard_target > bound_pc && guard_target < jr_pc + 8u)) { return false; } // Resolve the table-base reaching definition. Cross-register constants // (`lui rA; addiu rB,rA,lo`) are valid, but both definitions must be local, - // unskippable, outside delay slots, and unclobbered before use. + // unskippable and unclobbered before use. Only the validated scheduled + // form permits LUI in a delay slot, exactly that of the bounds BEQ. uint32_t low_pc = 0, source_reg = base_reg; int16_t low = 0; uint32_t lui_pc = 0, upper = 0; @@ -974,7 +997,9 @@ bool resolve_exact_bounded_jump_table( break; } } - if (lui_pc == 0u || in_delay_slot(lui_pc) || + if (scheduled_base && (lui_pc != guard_pc + 4u || low_pc != sll_pc - 4u)) + return false; + if (lui_pc == 0u || (in_delay_slot(lui_pc) && !scheduled_base) || (low_pc != 0u && in_delay_slot(low_pc)) || inbound_skips(lui_pc, addu_pc) || (low_pc != 0u && inbound_skips(low_pc, addu_pc)) || @@ -1026,6 +1051,7 @@ bool resolve_exact_bounded_jump_table( uint32_t runtime_target = *target_word; uint32_t image_target = mapped(runtime_target); if (image_target < entry || image_target >= hard_cap || + (scheduled_base && image_target > bound_pc && image_target < jr_pc + 8u) || image_target < producer_lo || image_target >= producer_hi || (image_target & 3u) != 0u) { return false; @@ -1089,6 +1115,7 @@ bool resolve_exact_bounded_jump_table( break; } } + uint32_t protected_pc = scheduled_base ? bound_pc : lui_pc; for (uint32_t source = entry; source < hard_cap; source += 4u) { auto word = read(source); if (!word.has_value()) return false; @@ -1096,9 +1123,9 @@ bool resolve_exact_bounded_jump_table( bool direct = cf.kind == ExactCfKind::Branch || cf.kind == ExactCfKind::Jump || cf.kind == ExactCfKind::Jal; - if (direct && cf.target > lui_pc && cf.target <= jr_pc && - (source < lui_pc || source > jr_pc) && - !case_reachable.count(source)) { + if (direct && cf.target > protected_pc && cf.target <= jr_pc && + (source < protected_pc || source > jr_pc) && + (scheduled_base || !case_reachable.count(source))) { return false; } } diff --git a/recompiler/tests/reachable_discovery_test.cpp b/recompiler/tests/reachable_discovery_test.cpp index 5052ef59..972a629c 100644 --- a/recompiler/tests/reachable_discovery_test.cpp +++ b/recompiler/tests/reachable_discovery_test.cpp @@ -102,9 +102,95 @@ bool resolves_ape_switch(const ApeSwitchFixture& f, 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; diff --git a/recompiler/tests/recompiler_patch_test.cpp b/recompiler/tests/recompiler_patch_test.cpp index da713103..bb458265 100644 --- a/recompiler/tests/recompiler_patch_test.cpp +++ b/recompiler/tests/recompiler_patch_test.cpp @@ -1068,6 +1068,29 @@ void jump_table_producer_codegen_test() { unbounded_alias_generated.front().full_code.find("/* jump table") != std::string::npos, "alias regression fixture reaches the table when ownership is absent"); + + // The scheduled variant must feed code generation too, not merely the + // discovery report. Its table base overwrites the guard in the BEQ delay + // slot, so the existing branch emitter must preserve the tested condition. + write_word(exe, base + 0x500u, 0u); + write_word(exe, base + 0x504u, 0u); + write_word(exe, base + 0x508u, 0x2C620003u); + write_word(exe, base + 0x50Cu, 0x1040001Cu); + write_word(exe, base + 0x510u, 0x3C028001u); + write_word(exe, base + 0x514u, 0x24420A00u); + write_word(exe, base + 0x518u, 0x00031880u); + write_word(exe, base + 0x51Cu, 0x00621821u); + write_word(exe, base + 0x520u, 0x8C620000u); + PSXRecomp::ControlFlowAnalyzer scheduled_analyzer(exe); + const auto scheduled_cfg = scheduled_analyzer.analyze_function(function); + PSXRecomp::CodeGenerator scheduled_generator(exe); + const auto scheduled = scheduled_generator.generate_function( + function, scheduled_cfg).full_code; + check(scheduled.find("/* jump table") != std::string::npos, + "codegen emits a switch with its table LUI in the bounds delay slot"); + for (uint32_t target : cases) + check(scheduled.find(fmt::format("goto block_{:08X}", target)) != std::string::npos, + "every scheduled case has an emitted native control-flow edge"); } void cfg_codegen_load_delay_test() { diff --git a/recompiler/tests/test_aot_overlay_discovery.py b/recompiler/tests/test_aot_overlay_discovery.py index 77d97acb..77070cfe 100644 --- a/recompiler/tests/test_aot_overlay_discovery.py +++ b/recompiler/tests/test_aot_overlay_discovery.py @@ -223,6 +223,81 @@ def check_bounded_jump_table_discovery(): LOAD + 0x60, 2, (LOAD, LOAD + 0x180)) == set() +def check_scheduled_jump_table_discovery(): + # Independent synthetic function: a checked input in v1, the table base + # in v0, and three cases returning distinct values. No captured game bytes. + data = bytearray(0x1000) + for offset, word in ( + (0x508, 0x2C620003), # sltiu v0,v1,3 + (0x50C, 0x1040001C), # beq v0,zero,+0x580 + (0x510, 0x3C028001), # lui v0,0x8001 (guard delay slot) + (0x514, 0x24420A00), # addiu v0,v0,0xa00 + (0x518, 0x00031880), # sll v1,v1,2 + (0x51C, 0x00621821), # addu v1,v1,v0 + (0x520, 0x8C620000), # lw v0,0(v1) + (0x528, 0x00400008), # jr v0 + (0x580, 0x03E00008), + ): + put(data, offset, word) + cases = {LOAD + off for off in (0x540, 0x550, 0x560)} + for i, target in enumerate(sorted(cases)): + put(data, 0xA00 + i * 4, target) + put(data, target - LOAD, 0x24020001 + i) + put(data, target - LOAD + 4, 0x03E00008) + + def resolve(image, producer=None): + return MOD._find_jump_table_targets( + image, LOAD, len(image), LOAD + 0x500, LOAD + 0x600, + LOAD + 0x528, 2, producer) + + assert resolve(data) == cases + no_load_delay = bytearray(data) + put(no_load_delay, 0x524, 0x00400008) + assert MOD._find_jump_table_targets( + no_load_delay, LOAD, len(data), LOAD + 0x500, LOAD + 0x600, + LOAD + 0x524, 2) == set() + walk = MOD._walk_overlay_function( + data, LOAD, len(data), LOAD + 0x500, LOAD + 0x600) + assert walk['jump_table_targets'] == cases + assert cases <= walk['visited'] + renamed = bytearray(data) + put(renamed, 0x510, 0x3C088001) # lui t0 + put(renamed, 0x514, 0x25020A00) # addiu v0,t0 + assert resolve(renamed) == cases + signed_low = bytearray(0x9000) + signed_low[:len(data)] = data + signed_low[0x8A00:0x8A0C] = data[0xA00:0xA0C] + put(signed_low, 0x510, 0x3C028002) + put(signed_low, 0x514, 0x24428A00) + assert resolve(signed_low) == cases + assert resolve(data, (LOAD, LOAD + 0x900)) == set() + + def jump(offset): + return 0x08000000 | (((LOAD + offset) >> 2) & 0x03FFFFFF) + + for name, mutations in ( + ('bound overwrites index', ((0x508, 0x2C630003), (0x50C, 0x1060001C))), + ('LUI overwrites index', ((0x510, 0x3C038001), (0x514, 0x24620A00))), + ('ADDIU overwrites index', ((0x514, 0x24430A00),)), + ('wrong constant source', ((0x514, 0x25020A00),)), + ('reserved LUI fields', ((0x510, 0x3C228001),)), + ('call delay slot', ((0x50C, jal(LOAD + 0x580)),)), + ('branch likely', ((0x50C, 0x5040001C),)), + ('guard loop clobbers condition', ((0x50C, 0x1040FFFF),)), + ('reject into constant', ((0x50C, 0x10400000),)), + ('skip bound from before', ((0x500, jump(0x510)),)), + ('skip bound from after', ((0x590, jump(0x510)),)), + ('case skips bound', ((0x540, jump(0x510)), (0x544, 0))), + ('bound in delay slot', ((0x504, jump(0x580)),)), + ('table case skips bound', ((0xA00, LOAD + 0x510),)), + ('out of host target', ((0xA04, LOAD + 0x700),)), + ): + broken = bytearray(data) + for offset, word in mutations: + put(broken, offset, word) + assert resolve(broken) == set(), name + + def check_composite_call_boundaries(): data = bytearray(0x100) cross_target = LOAD + 0x50 @@ -3649,6 +3724,7 @@ def main(): check_composite_call_boundaries() check_bounded_jump_table_discovery() + check_scheduled_jump_table_discovery() check_static_discovery_provenance() check_static_dispatch_provenance() check_owned_direct_call_is_interior() diff --git a/tools/aot_overlay_spike/README.md b/tools/aot_overlay_spike/README.md index d524b27c..dbe280b6 100644 --- a/tools/aot_overlay_spike/README.md +++ b/tools/aot_overlay_spike/README.md @@ -5,7 +5,7 @@ disc image at build time, so a fresh install ships with overlay coverage instead of relying entirely on runtime (tcc/gcc) autocompile. See `docs/AOT_OVERLAY_PLAN.md` for the full design, findings, and honest coverage numbers. -**Status: ENHANCEMENT SPIKE. Provably-correct for clean producers, not exhaustive.** +**Status: ENHANCEMENT SPIKE. Partial static discovery; not a correctness proof.** `extract_generic.py` is path-parameterized and suitable for the current build pipeline. Disc-container discovery remains engine-specific; unsupported producers fail closed instead of being guessed as code. @@ -41,13 +41,22 @@ the named metric. All 22 A*.BIN overlays converge on 0x80108F9C (region 0x80108000 +3996), DEMO/GAME converge on 0x80106228, and OPN resolves to 0x8018A000. -## Correctness guarantee (why partial coverage is safe) +## What byte guards establish Every shard is content-addressed by per-function `code_crc`; the runtime loader -only executes a shard when live RAM byte-matches (`overlay_loader.c` lazy_man_matches). -A mis-positioned / data-as-code shard either fails audit at compile time or never -fires at runtime → coverage loss, never incorrect execution. Whatever static -misses, production autocompile (tcc/gcc) self-heals on first visit. +checks live RAM against its recorded byte ranges (`overlay_loader.c` +`lazy_man_matches`). That protects against selecting a different overlay at the +same address. It does not prove that bytes were correctly classified as code, +that all dependencies were guarded, or that the emitted native code is correct. +Captured shards are investigative references, not correctness or completeness +oracles. Production fallback can cover additional executed paths; it cannot +establish that a static extraction is complete. + +The shared switch recognizer accepts a bounded `sltiu; beq; lui; addiu; sll; +addu; lw; nop; jr` pattern, including LUI in the BEQ delay slot. Register +dependencies, producer bounds, table targets, and direct edges must pass the +proof checks. This recovers a compiler convention across titles without game +addresses in the recognizer. Unknown dispatch forms still require investigation. ## Durable runtime capture history diff --git a/tools/compile_overlays.py b/tools/compile_overlays.py index c84ef67c..f6acfb3a 100644 --- a/tools/compile_overlays.py +++ b/tools/compile_overlays.py @@ -844,6 +844,11 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int, sll scaled,index,2; addu address,scaled,table_base lw target,offset(address); [nop x 0..1]; jr target + Also accept ``sltiu; beq; lui; addiu; sll``: the LUI executes in the + guard's delay slot, then ADDIU completes the base on the in-range path. + That exact pair may not overwrite the checked index, and direct edges + may not bypass the bound (including edges from switch cases). + ``table_base`` must be a nearest-definition LUI, optionally followed by a nearest-definition ADDIU. The lower instruction may rename the register (``lui v0; addiu s0,v0,lo``), as used by Ape Escape. Any other @@ -853,7 +858,7 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int, """ lo = load_addr hi = load_addr + size - if not (entry <= jr_pc < hard_cap and 0 < jr_rs < 32): + if not (entry <= jr_pc < hard_cap and 0 < jr_rs < 31): return set() jr_word = _word_at(data, load_addr, jr_pc) if jr_word != ((jr_rs << 21) | 0x08): @@ -904,7 +909,24 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int, # The bound must guard this exact index on the sole fallthrough into SLL. sltiu_pc = sll_pc - 12 branch_pc = sll_pc - 8 - if _word_at(data, load_addr, sll_pc - 4) != 0: + scheduled_base = _word_at(data, load_addr, sll_pc - 4) != 0 + if scheduled_base: + # R3000 load delay: JR must not consume the immediately preceding LW. + if lw_pc != jr_pc - 8: + return set() + sltiu_pc -= 4 + branch_pc -= 4 + upper = _word_at(data, load_addr, sll_pc - 8) + lower = _word_at(data, load_addr, sll_pc - 4) + if (upper is None or lower is None or + (upper >> 26) != 0x0F or ((upper >> 21) & 31) != 0 or + (lower >> 26) != 0x09 or ((lower >> 16) & 31) != table_reg or + ((upper >> 16) & 31) == 0 or + ((upper >> 16) & 31) != ((lower >> 21) & 31) or + index_reg in (table_reg, (upper >> 16) & 31)): + return set() + if sltiu_pc < entry or (sltiu_pc >= entry + 4 and + _is_control_flow(_word_at(data, load_addr, sltiu_pc - 4))): return set() sltiu_word = _word_at(data, load_addr, sltiu_pc) branch_word = _word_at(data, load_addr, branch_pc) @@ -913,7 +935,7 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int, return set() guard_reg = (sltiu_word >> 16) & 0x1F table_count = sltiu_word & 0xFFFF - if not guard_reg or not (1 <= table_count < 512): + if not guard_reg or guard_reg == index_reg or not (1 <= table_count < 512): return set() if branch_word is None or ((branch_word >> 26) & 0x3F) != 0x04: return set() @@ -923,7 +945,8 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int, return set() reject_target = _branch_target(branch_pc, branch_word) if (not (entry <= reject_target < hard_cap) or - sll_pc <= reject_target < jr_pc + 8): + sll_pc <= reject_target < jr_pc + 8 or + (scheduled_base and sltiu_pc < reject_target < jr_pc + 8)): return set() def nearest_writer(reg: int, before: int): @@ -962,6 +985,10 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int, else: return set() + if scheduled_base and (constant_pc != branch_pc + 4 or + table_def_pc != sll_pc - 4): + return set() + # Calls could clobber the proven constant. Other branches could enter the # suffix through an unproved path. The one exact bounds branch is allowed. for pc in range(constant_pc + 4, sll_pc, 4): @@ -971,7 +998,8 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int, if _is_control_flow(word) and pc != branch_pc: return set() predecessor = _word_at(data, load_addr, constant_pc - 4) - if constant_pc >= entry + 4 and _is_control_flow(predecessor): + if (constant_pc >= entry + 4 and _is_control_flow(predecessor) and + not scheduled_base): return set() table_base = (table_base + lw_offset) & 0xFFFFFFFF table_end = table_base + table_count * 4 @@ -984,6 +1012,7 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int, target = _word_at(data, load_addr, table_base + index * 4) if (target is None or (target & 3) or not (entry <= target < hard_cap) or + (scheduled_base and sltiu_pc < target < jr_pc + 8) or not (range_lo <= target < range_hi) or not _is_valid_mips_word(_word_at(data, load_addr, target))): return set() @@ -1025,15 +1054,16 @@ def _find_jump_table_targets(data: bytes, load_addr: int, size: int, elif kind in ('jr', 'jr_ra'): case_reachable.add(delay) + protected_pc = sltiu_pc if scheduled_base else constant_pc for source in range(entry, hard_cap, 4): source_word = _word_at(data, load_addr, source) if source_word is None: return set() kind, target = _classify_cf(source, source_word) if (kind in ('branch', 'j', 'jal') and - constant_pc < target <= jr_pc and - not (constant_pc <= source <= jr_pc) and - source not in case_reachable): + protected_pc < target <= jr_pc and + not (protected_pc <= source <= jr_pc) and + (scheduled_base or source not in case_reachable)): return set() if proof_out is not None: proof_out.append({