From 495bf2de53402cbdc0f17eb854f672b157972f0b Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 18 Jul 2026 02:56:42 -0700 Subject: [PATCH] aot: constrain calls across composite producers --- recompiler/include/function_analysis.h | 7 +- recompiler/src/function_analysis.cpp | 33 +++- recompiler/src/main_psx.cpp | 58 +++++- recompiler/tests/reachable_discovery_test.cpp | 13 ++ .../tests/test_aot_overlay_discovery.py | 74 ++++++++ tools/aot_overlay_spike/extract_generic.py | 22 ++- tools/compile_overlays.py | 166 +++++++++++++++++- 7 files changed, 348 insertions(+), 25 deletions(-) diff --git a/recompiler/include/function_analysis.h b/recompiler/include/function_analysis.h index bdb87486..d5a914db 100644 --- a/recompiler/include/function_analysis.h +++ b/recompiler/include/function_analysis.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include #include "ps1_exe_parser.h" @@ -52,7 +54,10 @@ public: // reachable from them. Used by runtime-loaded overlays and opt-in main-EXE // reachable discovery. Unresolved jalr/indirect targets do not mint // functions; evidence-backed entries must be supplied explicitly. - FunctionAnalysisResult analyze_exact_entries(const std::vector& entries); + FunctionAnalysisResult analyze_exact_entries( + const std::vector& entries, + const std::vector>& producer_ranges = {}, + const std::set& cross_call_allow = {}); // Add a forced entry point address that is treated as a function start // even if it has no standard ADDIU $sp prologue. The function will be diff --git a/recompiler/src/function_analysis.cpp b/recompiler/src/function_analysis.cpp index 3ac817c0..570e5ead 100644 --- a/recompiler/src/function_analysis.cpp +++ b/recompiler/src/function_analysis.cpp @@ -644,7 +644,10 @@ static ExactCf exact_classify_cf(uint32_t pc, uint32_t instr) { } // namespace -FunctionAnalysisResult FunctionAnalyzer::analyze_exact_entries(const std::vector& entries) { +FunctionAnalysisResult FunctionAnalyzer::analyze_exact_entries( + const std::vector& entries, + const std::vector>& producer_ranges, + const std::set& cross_call_allow) { FunctionAnalysisResult result; result.total_instructions = 0; result.jr_ra_count = 0; @@ -658,13 +661,28 @@ FunctionAnalysisResult FunctionAnalyzer::analyze_exact_entries(const std::vector return addr >= exe_.header.load_address && addr < exe_.end_address() && (addr & 3u) == 0; }; - auto callable_direct_jal_target = [&](uint32_t addr) { + auto producer_for = [&](uint32_t addr) -> int { + for (size_t i = 0; i < producer_ranges.size(); i++) { + if (addr >= producer_ranges[i].first && addr < producer_ranges[i].second) + return static_cast(i); + } + return -1; + }; + + auto callable_direct_jal_target = [&](uint32_t source, uint32_t addr) { auto word_opt = exe_.read_word(addr); // A JAL encountered by the reachable CFG walk is direct call evidence. // Requiring a stack prologue here discarded legitimate frameless leaf // functions and tiny library wrappers. The target still has to decode // as a valid PS1 instruction and lie inside the verified image bound. - return word_opt.has_value() && exact_is_valid_mips_word(*word_opt); + if (!word_opt.has_value() || !exact_is_valid_mips_word(*word_opt)) + return false; + if (producer_ranges.empty()) return true; + int source_producer = producer_for(source); + int target_producer = producer_for(addr); + if (source_producer >= 0 && source_producer == target_producer) + return true; + return cross_call_allow.count(addr) != 0; }; auto find_jump_table_targets = [&](uint32_t entry, uint32_t hard_cap, @@ -788,7 +806,8 @@ FunctionAnalysisResult FunctionAnalyzer::analyze_exact_entries(const std::vector case ExactCfKind::Jal: if (in_function(delay)) wr.visited.insert(delay); if (in_function(pc + 8u)) work.push(pc + 8u); - if (in_exe(cf.target) && callable_direct_jal_target(cf.target)) { + if (in_exe(cf.target) && + callable_direct_jal_target(pc, cf.target)) { wr.direct_jal_targets.insert(cf.target); } break; @@ -800,7 +819,8 @@ FunctionAnalysisResult FunctionAnalyzer::analyze_exact_entries(const std::vector uint32_t target_reg = (instr >> 21) & 0x1Fu; if (exact_resolve_constant_transfer(exe_, entry, pc, target_reg, target) && - in_exe(target)) { + in_exe(target) && + callable_direct_jal_target(pc, target)) { wr.direct_jal_targets.insert(target); } } @@ -819,7 +839,8 @@ FunctionAnalysisResult FunctionAnalyzer::analyze_exact_entries(const std::vector if (in_function(target)) { wr.jump_table_targets.insert(target); work.push(target); - } else if (in_exe(target)) { + } else if (in_exe(target) && + callable_direct_jal_target(pc, target)) { // An absolute JR out of the current function is a // statically proven tail-call entry. wr.direct_jal_targets.insert(target); diff --git a/recompiler/src/main_psx.cpp b/recompiler/src/main_psx.cpp index 68268caa..bc9472af 100644 --- a/recompiler/src/main_psx.cpp +++ b/recompiler/src/main_psx.cpp @@ -432,6 +432,8 @@ int main(int argc, char** argv) { std::vector interior_seeds; std::set trusted_root_seeds; std::set trusted_call_root_seeds; + std::vector> producer_ranges; + std::set cross_call_allow; if (extra_funcs_path) { std::ifstream ef(extra_funcs_path); if (ef.is_open()) { @@ -440,6 +442,33 @@ int main(int argc, char** argv) { std::string line; while (std::getline(ef, line)) { if (line.empty() || line[0] == '#') continue; + if (line.rfind("producer_range", 0) == 0) { + std::istringstream in(line); + std::string tag, lo_text, hi_text; + in >> tag >> lo_text >> hi_text; + uint32_t lo = static_cast( + std::strtoul(lo_text.c_str(), nullptr, 16)); + uint32_t hi = static_cast( + std::strtoul(hi_text.c_str(), nullptr, 16)); + if (lo_text.empty() || hi_text.empty() || + lo < seed_lo || lo >= hi || hi > seed_hi) { + fmt::print(stderr, "ERROR: invalid producer_range: {}\n", line); + return 1; + } + producer_ranges.emplace_back(lo, hi); + continue; + } + if (line.rfind("cross_call_allow", 0) == 0) { + const char* p = line.c_str() + 16; + uint32_t addr = static_cast( + std::strtoul(p, nullptr, 16)); + if (addr < seed_lo || addr >= seed_hi) { + fmt::print(stderr, "ERROR: invalid cross_call_allow: {}\n", line); + return 1; + } + cross_call_allow.insert(addr); + continue; + } bool interior = false; bool trusted_root = false; bool trusted_call_root = false; @@ -466,11 +495,35 @@ int main(int argc, char** argv) { } } } + std::sort(producer_ranges.begin(), producer_ranges.end()); + for (size_t i = 1; i < producer_ranges.size(); i++) { + if (producer_ranges[i - 1].second > producer_ranges[i].first) { + fmt::print(stderr, "ERROR: overlapping producer_range entries\n"); + return 1; + } + } + for (uint32_t addr : cross_call_allow) { + bool contained = false; + for (const auto& range : producer_ranges) { + if (addr >= range.first && addr < range.second) { + contained = true; + break; + } + } + if (!contained) { + fmt::print(stderr, + "ERROR: cross_call_allow 0x{:08X} is outside " + "producer ranges\n", addr); + return 1; + } + } fmt::print("Loaded {} extra function addresses ({} interior, " - "{} dispatch-root, {} call-root) from {}\n", + "{} dispatch-root, {} call-root, {} producer ranges, " + "{} cross-call allows) from {}\n", file_seeds.size() + interior_seeds.size(), interior_seeds.size(), trusted_root_seeds.size(), trusted_call_root_seeds.size(), + producer_ranges.size(), cross_call_allow.size(), extra_funcs_path); } else { fmt::print("WARNING: Cannot open extra-funcs file: {}\n", extra_funcs_path); @@ -560,7 +613,8 @@ int main(int argc, char** argv) { { PSXRecomp::FunctionAnalyzer analyzer(*exe); std::vector roots_vec(roots.begin(), roots.end()); - analysis_result = analyzer.analyze_exact_entries(roots_vec); + analysis_result = analyzer.analyze_exact_entries( + roots_vec, producer_ranges, cross_call_allow); } std::vector alias_entries; diff --git a/recompiler/tests/reachable_discovery_test.cpp b/recompiler/tests/reachable_discovery_test.cpp index 5ea74b2b..6f635f73 100644 --- a/recompiler/tests/reachable_discovery_test.cpp +++ b/recompiler/tests/reachable_discovery_test.cpp @@ -111,6 +111,19 @@ int main() { CHECK(!reachable.count(kLoad + 0x1100), "direct target beyond verified bound fails closed"); + // 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. diff --git a/recompiler/tests/test_aot_overlay_discovery.py b/recompiler/tests/test_aot_overlay_discovery.py index df679e72..66707d70 100644 --- a/recompiler/tests/test_aot_overlay_discovery.py +++ b/recompiler/tests/test_aot_overlay_discovery.py @@ -37,6 +37,42 @@ def make_psxexe(entry, data): return bytes(header) + data +def check_composite_call_boundaries(): + data = bytearray(0x100) + cross_target = LOAD + 0x50 + put(data, 0x00, jal(cross_target)) + put(data, 0x04, 0x00000000) + put(data, 0x08, 0x03E00008) + put(data, 0x0C, 0x00000000) + put(data, 0x50, 0x24020001) # valid MIPS, but no callable boundary + ranges = ((LOAD, LOAD + 0x40), (LOAD + 0x40, LOAD + 0x100)) + + # A direct JAL remains sufficient for frameless leaves inside one producer, + # but not when an adjacent-producer variant merely puts pointer/data-shaped + # bytes at the encoded address. + unscoped = MOD._walk_overlay_function(data, LOAD, len(data), LOAD, LOAD + 0x100) + assert cross_target in unscoped['direct_jals'] + scoped = MOD._walk_overlay_function( + data, LOAD, len(data), LOAD, LOAD + 0x100, ranges) + assert cross_target not in scoped['direct_jals'] + assert cross_target in scoped['rejected_cross_producer_calls'] + + # A bounded frameless body with a real return is independently code-like + # and remains discoverable across the producer boundary. + put(data, 0x54, 0x03E00008) + put(data, 0x58, 0x00000000) + scoped = MOD._walk_overlay_function( + data, LOAD, len(data), LOAD, LOAD + 0x100, ranges) + assert cross_target in scoped['direct_jals'] + + # Independent target-local boundary evidence makes a real cross-producer + # export safe to retain. + put(data, 0x50, 0x27BDFFE0) + scoped = MOD._walk_overlay_function( + data, LOAD, len(data), LOAD, LOAD + 0x100, ranges) + assert cross_target in scoped['direct_jals'] + + def check_padded_return_boundary(recompiler): # Exact-entry mode re-verifies every untrusted seed. Psy-Q aligns frameless # leaves with NOPs after the prior function's JR delay slot; those NOPs must @@ -67,6 +103,41 @@ def check_padded_return_boundary(recompiler): assert f"F {LOAD + 0x14:08X}" in ranges +def check_recompiler_composite_contract(recompiler): + data = bytearray(0x80) + target = LOAD + 0x50 + put(data, 0x00, jal(target)) + put(data, 0x04, 0x00000000) + put(data, 0x08, 0x03E00008) + put(data, 0x0C, 0x00000000) + put(data, 0x50, 0x24020001) + put(data, 0x54, 0x03E00008) + put(data, 0x58, 0x00000000) + + with tempfile.TemporaryDirectory() as tmp: + psx = os.path.join(tmp, "composite.psx") + with open(psx, "wb") as f: + f.write(make_psxexe(LOAD, data)) + + def generated(allow): + seeds = os.path.join(tmp, f"seeds_{allow}.txt") + out = os.path.join(tmp, f"out_{allow}") + with open(seeds, "w", encoding="utf-8") as f: + f.write(f"producer_range 0x{LOAD:08X} 0x{LOAD + 0x40:08X}\n") + f.write(f"producer_range 0x{LOAD + 0x40:08X} 0x{LOAD + 0x80:08X}\n") + if allow: + f.write(f"cross_call_allow 0x{target:08X}\n") + f.write(f"0x{LOAD:08X}\n") + result = subprocess.run( + [recompiler, psx, "--seeds", seeds, "--out-dir", out, "--overlay"], + capture_output=True, text=True) + assert result.returncode == 0, result.stderr or result.stdout + return ''.join(path.read_text() for path in pathlib.Path(out).glob("*_full*.c")) + + assert f"void func_{target:08X}" not in generated(False) + assert f"void func_{target:08X}" in generated(True) + + def main(): default_recompiler = ROOT / "recompiler" / "build" / "psxrecomp-game.exe" parser = argparse.ArgumentParser() @@ -75,6 +146,9 @@ def main(): if not os.path.isfile(args.recompiler): raise SystemExit(f"recompiler not found: {args.recompiler}") + check_composite_call_boundaries() + check_recompiler_composite_contract(args.recompiler) + data = bytearray(0x200) # A framed root directly calls a frameless leaf. JAL is the boundary diff --git a/tools/aot_overlay_spike/extract_generic.py b/tools/aot_overlay_spike/extract_generic.py index 001265ac..9f558699 100644 --- a/tools/aot_overlay_spike/extract_generic.py +++ b/tools/aot_overlay_spike/extract_generic.py @@ -245,7 +245,7 @@ def full_discovery_seeds(data, recompiler, tmp): a=int(m.group(1),16); seeds.add(a if a>=0x80000000 else (0x80000000|a)) return sorted(seeds) or None -def rec(load_addr, data, seeds, dispatch_extra=None): +def rec(load_addr, data, seeds, dispatch_extra=None, producer_ranges=None): # function_entry_pcs = prologue-scanned function starts (walk roots). # dispatch_entry_pcs = those PLUS any extra dispatch targets (e.g. the # overlay's own export table). compile_overlays promotes clean starts to @@ -253,11 +253,16 @@ def rec(load_addr, data, seeds, dispatch_extra=None): # (impossible_entry_start guards garbage), so passing the game's authoritative # dispatch table here recovers indirect-only entries a prologue scan misses. disp = sorted(set(seeds) | set(dispatch_extra or ())) - return {"schema":"psxrecomp overlay capture v2","load_addr":f"0x{load_addr:08X}", - "size":len(data),"bytes_b64":base64.b64encode(data).decode(), - "executed_pcs":[],"dispatch_entry_pcs":[f"0x{a:08X}" for a in disp], - "function_entry_pcs":[f"0x{a:08X}" for a in seeds], - "seeds":[f"0x{a:08X}" for a in seeds]} + out={"schema":"psxrecomp overlay capture v2","load_addr":f"0x{load_addr:08X}", + "size":len(data),"bytes_b64":base64.b64encode(data).decode(), + "executed_pcs":[],"dispatch_entry_pcs":[f"0x{a:08X}" for a in disp], + "function_entry_pcs":[f"0x{a:08X}" for a in seeds], + "seeds":[f"0x{a:08X}" for a in seeds]} + if producer_ranges: + out["producer_ranges"]=[ + {"start":f"0x{lo:08X}","end":f"0x{hi:08X}"} + for lo,hi in producer_ranges] + return out def main(): ap=argparse.ArgumentParser() @@ -417,7 +422,10 @@ def main(): composite_keys.add(key) seeds=sorted(set(lseeds)|set(rseeds)) dispatch=sorted(set(ldispatch)|set(rdispatch)) - records.append(rec(page_base,region,seeds,dispatch_extra=dispatch)) + records.append(rec( + page_base,region,seeds,dispatch_extra=dispatch, + producer_ranges=((lbase,lbase+len(ldata)), + (rbase,rbase+len(rdata))))) nc+=1 print(f" [composite] {lp} + {rp}: {len(region)}B " f"region@0x{page_base:08X} (exact adjacent producers), " diff --git a/tools/compile_overlays.py b/tools/compile_overlays.py index a39f0000..0f2e4aac 100644 --- a/tools/compile_overlays.py +++ b/tools/compile_overlays.py @@ -563,8 +563,81 @@ def _resolve_constant_transfer(data: bytes, load_addr: int, return None +def _plausible_callable_target(data: bytes, load_addr: int, size: int, + target: int, producer_hi: int) -> bool: + """CFG proof for a cross-producer call target without a classic prologue. + + Direct calls are sufficient boundary evidence inside one linked producer. + Across a synthetic adjacent-producer envelope, additionally require a + bounded, valid CFG with a reachable return and no sequential fallthrough. + This retains real frameless exports while rejecting pointer tables/NOP + runways that only happen to decode as instructions. + """ + first = _word_at(data, load_addr, target) + if first in (None, 0) or not _is_valid_mips_word(first): + return False + cap = min(load_addr + size, producer_hi, target + 0x4000) + work = deque([target]) + visited = set() + saw_return = False + bad = False + + def inside(addr: int) -> bool: + return target <= addr < cap and (addr & 3) == 0 + + def add_delay(pc: int) -> None: + nonlocal bad + delay = pc + 4 + if not inside(delay): + bad = True + return + word = _word_at(data, load_addr, delay) + if not _is_valid_mips_word(word): + bad = True + else: + visited.add(delay) + + while work and not bad and len(visited) < 2048: + pc = work.popleft() + if pc in visited: + continue + if not inside(pc): + bad = True # sequential fallthrough past the bounded probe + break + word = _word_at(data, load_addr, pc) + if not _is_valid_mips_word(word): + bad = True + break + visited.add(pc) + kind, branch_target = _classify_cf(pc, word) + if kind == 'normal': + work.append(pc + 4) + elif kind == 'branch': + add_delay(pc) + work.append(pc + 8) # conditional fallthrough must remain bounded + if inside(branch_target): + work.append(branch_target) + # An out-of-probe branch target is a shared external code edge. + elif kind in ('jal', 'jalr'): + add_delay(pc) + work.append(pc + 8) + elif kind == 'j': + add_delay(pc) + if inside(branch_target): + work.append(branch_target) + # Otherwise this is a bounded tail edge. + elif kind == 'jr_ra': + add_delay(pc) + saw_return = True + elif kind == 'jr': + add_delay(pc) # bounded indirect/tail exit + + return saw_return and not bad and not work and len(visited) < 2048 + + def _walk_overlay_function(data: bytes, load_addr: int, size: int, - entry: int, hard_cap: int) -> dict: + entry: int, hard_cap: int, + producer_ranges=()) -> dict: lo = load_addr hi = load_addr + size @@ -577,6 +650,37 @@ def _walk_overlay_function(data: bytes, load_addr: int, size: int, static_indirect_targets = set() branch_targets = set() jump_table_targets = set() + rejected_cross_producer_calls = set() + accepted_cross_producer_calls = set() + + def producer_for(addr: int): + for range_lo, range_hi in producer_ranges: + if range_lo <= addr < range_hi: + return range_lo, range_hi + return None + + def callable_transfer_target(source: int, target: int) -> bool: + if not producer_ranges: + return True + source_range = producer_for(source) + target_range = producer_for(target) + if source_range is not None and source_range == target_range: + return True + # Adjacent-producer composites are a byte envelope, not proof that a + # call encoded by producer A is linked to arbitrary bytes at the same + # address in every producer-B variant. Cross-boundary calls therefore + # need independent local boundary evidence. This keeps real exports + # while rejecting pointer/data words that happen to be valid MIPS. + if _callable_direct_jal_target(data, load_addr, target): + accepted_cross_producer_calls.add(target) + return True + if (target_range is not None and + _plausible_callable_target( + data, load_addr, size, target, target_range[1])): + accepted_cross_producer_calls.add(target) + return True + rejected_cross_producer_calls.add(target) + return False while work: pc = work.popleft() @@ -614,7 +718,8 @@ def _walk_overlay_function(data: bytes, load_addr: int, size: int, work.append(pc + 8) branch_targets.add(pc + 8) if (lo <= target < hi and (target & 3) == 0 and - _is_valid_mips_word(_word_at(data, load_addr, target))): + _is_valid_mips_word(_word_at(data, load_addr, target)) and + callable_transfer_target(pc, target)): direct_jals.add(target) elif kind == 'jalr': if in_function(delay): @@ -625,7 +730,8 @@ def _walk_overlay_function(data: bytes, load_addr: int, size: int, target_reg = (word >> 21) & 0x1F target = _resolve_constant_transfer(data, load_addr, entry, pc, target_reg) - if target is not None and lo <= target < hi and (target & 3) == 0: + if (target is not None and lo <= target < hi and (target & 3) == 0 + and callable_transfer_target(pc, target)): static_indirect_targets.add(target) elif kind == 'jr': if in_function(delay): @@ -636,7 +742,7 @@ def _walk_overlay_function(data: bytes, load_addr: int, size: int, if in_function(target): branch_targets.add(target) work.append(target) - else: + elif callable_transfer_target(pc, target): static_indirect_targets.add(target) else: for jt in _find_jump_table_targets(data, load_addr, size, @@ -655,6 +761,8 @@ def _walk_overlay_function(data: bytes, load_addr: int, size: int, 'static_indirect_targets': static_indirect_targets, 'branch_targets': branch_targets, 'jump_table_targets': jump_table_targets, + 'rejected_cross_producer_calls': rejected_cross_producer_calls, + 'accepted_cross_producer_calls': accepted_cross_producer_calls, } @@ -685,6 +793,24 @@ def classify_overlay_seeds(cap: dict, data: bytes, load_addr: int, size: int, lo = load_addr hi = load_addr + size region = lambda a: lo <= a < hi and (a & 3) == 0 + producer_ranges = [] + for raw_range in cap.get('producer_ranges', []) or []: + if not isinstance(raw_range, dict): + raise RuntimeError('producer_ranges entries must be objects') + try: + range_lo = _parse_addr(raw_range['start']) + range_hi = _parse_addr(raw_range['end']) + except (KeyError, TypeError, ValueError) as exc: + raise RuntimeError(f'invalid producer_ranges entry: {raw_range}') from exc + if not (lo <= range_lo < range_hi <= hi): + raise RuntimeError( + f'producer range 0x{range_lo:08X}..0x{range_hi:08X} ' + f'outside capture 0x{lo:08X}..0x{hi:08X}') + producer_ranges.append((range_lo, range_hi)) + producer_ranges.sort() + if any(producer_ranges[i-1][1] > producer_ranges[i][0] + for i in range(1, len(producer_ranges))): + raise RuntimeError('producer_ranges overlap') schema_keys = {'executed_pcs', 'observed_pcs', 'dispatch_entry_pcs', 'function_entry_pcs'} @@ -714,7 +840,8 @@ def classify_overlay_seeds(cap: dict, data: bytes, load_addr: int, size: int, pre_roots_sorted = sorted(a for a in pre_roots if region(a)) for i, entry in enumerate(pre_roots_sorted): hard_cap = pre_roots_sorted[i + 1] if i + 1 < len(pre_roots_sorted) else hi - walk = _walk_overlay_function(data, load_addr, size, entry, hard_cap) + walk = _walk_overlay_function( + data, load_addr, size, entry, hard_cap, producer_ranges) jump_table_targets.update(walk['jump_table_targets']) def impossible_entry_start(addr: int) -> bool: @@ -800,7 +927,8 @@ def classify_overlay_seeds(cap: dict, data: bytes, load_addr: int, size: int, processed.add(entry) sorted_known = sorted(known) hard_cap = next((x for x in sorted_known if x > entry), hi) - walk = _walk_overlay_function(data, load_addr, size, entry, hard_cap) + walk = _walk_overlay_function( + data, load_addr, size, entry, hard_cap, producer_ranges) all_branch_targets.update(walk['branch_targets']) all_branch_targets.update(walk['jump_table_targets']) for target in sorted(walk['direct_jals']): @@ -828,7 +956,8 @@ def classify_overlay_seeds(cap: dict, data: bytes, load_addr: int, size: int, sorted_known = sorted(known) for i, entry in enumerate(sorted_known): hard_cap = sorted_known[i + 1] if i + 1 < len(sorted_known) else hi - walk = _walk_overlay_function(data, load_addr, size, entry, hard_cap) + walk = _walk_overlay_function( + data, load_addr, size, entry, hard_cap, producer_ranges) covered |= walk['visited'] promoted = sorted(a for a, r in included.items() if r == 'DISPATCH_INTERIOR' and a not in covered @@ -847,12 +976,19 @@ def classify_overlay_seeds(cap: dict, data: bytes, load_addr: int, size: int, # Re-walk with the final function set so the branch-target exclusion count # matches the actual compilation boundaries. all_branch_targets.clear() + rejected_cross_producer_calls = set() + accepted_cross_producer_calls = set() sorted_known = sorted(known) for i, entry in enumerate(sorted_known): hard_cap = sorted_known[i + 1] if i + 1 < len(sorted_known) else hi - walk = _walk_overlay_function(data, load_addr, size, entry, hard_cap) + walk = _walk_overlay_function( + data, load_addr, size, entry, hard_cap, producer_ranges) all_branch_targets.update(walk['branch_targets']) all_branch_targets.update(walk['jump_table_targets']) + rejected_cross_producer_calls.update( + walk['rejected_cross_producer_calls']) + accepted_cross_producer_calls.update( + walk['accepted_cross_producer_calls']) candidates = {a for a in (executed_pcs | dispatch_entry_pcs | captured_function_entries | legacy_seeds | toml_entries) @@ -885,6 +1021,9 @@ def classify_overlay_seeds(cap: dict, data: bytes, load_addr: int, size: int, 'branch_targets_excluded_count': len(all_branch_targets - set(included)), 'counts': counts, 'excluded_counts': excluded_counts, + 'producer_ranges': producer_ranges, + 'rejected_cross_producer_calls': rejected_cross_producer_calls, + 'accepted_cross_producer_calls': accepted_cross_producer_calls, } # Interior entries carry the 'interior' marker so the recompiler emits # them as overlapping aliases, never as walk roots. Promoted kernel @@ -899,7 +1038,12 @@ def classify_overlay_seeds(cap: dict, data: bytes, load_addr: int, size: int, if r in ('DIRECT_JAL_TARGET', 'STATIC_INDIRECT_TARGET'): return f'call_root 0x{addr:08X}' return f'0x{addr:08X}' - seeds = [seed_line(addr) for addr in sorted(included)] + seeds = [] + for range_lo, range_hi in producer_ranges: + seeds.append(f'producer_range 0x{range_lo:08X} 0x{range_hi:08X}') + for addr in sorted(accepted_cross_producer_calls): + seeds.append(f'cross_call_allow 0x{addr:08X}') + seeds.extend(seed_line(addr) for addr in sorted(included)) return seeds, audit @@ -915,6 +1059,10 @@ def print_seed_audit(audit: dict) -> None: print(f'toml_entries_included: {audit["counts"].get("TOML_DECLARED_ENTRY", 0)}') print(f'dispatch_interior_included: {audit["counts"].get("DISPATCH_INTERIOR", 0)}') print(f'dispatch_roots_promoted: {audit["counts"].get("DISPATCH_ROOT", 0)}') + print(f'cross_producer_calls_rejected: ' + f'{len(audit["rejected_cross_producer_calls"])}') + print(f'cross_producer_calls_accepted: ' + f'{len(audit["accepted_cross_producer_calls"])}') print(f'branch_targets_excluded: {audit["branch_targets_excluded_count"]}') print(f'observed_only_excluded: {audit["excluded_counts"].get("OBSERVED_PC_ONLY", 0)}') print(f'unknown_excluded: {audit["excluded_counts"].get("UNKNOWN", 0)}')