From 51f3ab874bc538859467e102af32e0f8c968c84b Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 30 Jul 2026 03:49:13 -0500 Subject: [PATCH 01/10] feat(source-parity): add shared single-address relocation-evidence helper U1 of docs/plans/2026-07-30-001-fix-generalize-relocation-evidence-plan.md --- .../source_parity_synthesize.py | 20 ++++++++++++ tests/test_relocation_evidence_helper.py | 32 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 tests/test_relocation_evidence_helper.py diff --git a/src/agentdecompile_recovery/source_parity_synthesize.py b/src/agentdecompile_recovery/source_parity_synthesize.py index ab4541a..0f08dee 100755 --- a/src/agentdecompile_recovery/source_parity_synthesize.py +++ b/src/agentdecompile_recovery/source_parity_synthesize.py @@ -629,6 +629,26 @@ def inc_abs_global(row: dict[str, Any], c_name: str, data: bytes) -> list[Genera ] +def single_absolute_address_relocation(offset: int, addr: int) -> list[dict[str, Any]]: + """Return the absoluteAddressRelocations shape for one absolute-address reference. + + Matches the target-side reconstruction absolute_address_relocations() reads + (render_target_coff_for_candidate) -- without this, the synthetic target + object renders the address as a raw byte blob instead of a symbol + relocation, and a candidate referencing the same global through a + compiler-visible symbol can never byte-match it. + """ + + return [ + { + "offset": offset, + "type": "IMAGE_REL_I386_DIR32", + "symbol": f"_DAT_{addr:08x}", + "decodedAddress": f"0x{addr:08x}", + } + ] + + def inc_field_return_stack4(row: dict[str, Any], c_name: str, data: bytes) -> list[GeneratedCandidate]: if len(data) != 10 or data[0] != 0x8B or data[1] != 0x41 or data[3] != 0x40 or data[4] != 0x89 or data[5] != 0x41: return [] diff --git a/tests/test_relocation_evidence_helper.py b/tests/test_relocation_evidence_helper.py new file mode 100644 index 0000000..8ec738e --- /dev/null +++ b/tests/test_relocation_evidence_helper.py @@ -0,0 +1,32 @@ +"""Tests for the shared single-address relocation-evidence helper. + +See src/agentdecompile_recovery/source_parity_synthesize.py's +single_absolute_address_relocation() and the inc_abs_global fix it +generalizes (PR #149) -- both exist to give render_target_coff_for_candidate() +a symbol relocation to reconstruct on the target side, instead of a raw byte +blob that a correctly-compiled candidate can never byte-match. +""" + +from __future__ import annotations + +from agentdecompile_recovery.source_parity_synthesize import single_absolute_address_relocation + + +def test_single_absolute_address_relocation_shape() -> None: + relocations = single_absolute_address_relocation(offset=6, addr=0x00830540) + + assert relocations == [ + { + "offset": 6, + "type": "IMAGE_REL_I386_DIR32", + "symbol": "_DAT_00830540", + "decodedAddress": "0x00830540", + } + ] + + +def test_single_absolute_address_relocation_leading_zero_address() -> None: + relocations = single_absolute_address_relocation(offset=0, addr=0x00001000) + + assert relocations[0]["symbol"] == "_DAT_00001000" + assert relocations[0]["decodedAddress"] == "0x00001000" From f802d73db3bb536438535c280a1bd6d1b19e1885 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 30 Jul 2026 04:02:11 -0500 Subject: [PATCH 02/10] feat(source-parity): wire relocation evidence into 10 single-address rules float_multiply_global, import_call_self_stdcall, global_setter_u32_stdcall, call_indirect_zero, virtual_call_eq_global, import_call_return_self, global_indexed_store_cdecl, import_call_arg_return_one_stdcall8, global_virtual_call_stack_arg, global_field_eq_one_bool now populate absoluteAddressRelocations, closing the same target-side rendering gap inc_abs_global had (PR #149). U2 of docs/plans/2026-07-30-001-fix-generalize-relocation-evidence-plan.md --- .../source_parity_synthesize.py | 86 ++++++++-- ...test_rule_generator_relocation_evidence.py | 156 ++++++++++++++++++ 2 files changed, 228 insertions(+), 14 deletions(-) create mode 100644 tests/test_rule_generator_relocation_evidence.py diff --git a/src/agentdecompile_recovery/source_parity_synthesize.py b/src/agentdecompile_recovery/source_parity_synthesize.py index 0f08dee..36979f4 100755 --- a/src/agentdecompile_recovery/source_parity_synthesize.py +++ b/src/agentdecompile_recovery/source_parity_synthesize.py @@ -704,7 +704,10 @@ def float_multiply_global(row: dict[str, Any], c_name: str, data: bytes) -> list source=source, callconv="cdecl", return_type="float", - evidence={"absoluteAddress": f"0x{addr:08x}"}, + evidence={ + "absoluteAddress": f"0x{addr:08x}", + "absoluteAddressRelocations": single_absolute_address_relocation(6, addr), + }, ) ] @@ -758,7 +761,10 @@ def import_call_self_stdcall(row: dict[str, Any], c_name: str, data: bytes) -> l source=source, callconv="fastcall", return_type="void", - evidence={"callPointerAddress": f"0x{addr:08x}"}, + evidence={ + "callPointerAddress": f"0x{addr:08x}", + "absoluteAddressRelocations": single_absolute_address_relocation(3, addr), + }, ) ] @@ -5299,7 +5305,12 @@ def global_setter_u32_stdcall(row: dict[str, Any], c_name: str, data: bytes) -> source=source, callconv="stdcall", return_type="void", - evidence={"pattern": "mov-eax-stack4-store-abs-ret4", "address": f"0x{address:08x}", "stackBytes": 4}, + evidence={ + "pattern": "mov-eax-stack4-store-abs-ret4", + "address": f"0x{address:08x}", + "stackBytes": 4, + "absoluteAddressRelocations": single_absolute_address_relocation(5, address), + }, ) ] @@ -8681,7 +8692,11 @@ def call_indirect_zero(row: dict[str, Any], c_name: str, data: bytes) -> list[Ge source=direct_source, callconv="cdecl", return_type="void", - evidence={"callPointerAddress": f"0x{addr:08x}", "arg0": 0}, + evidence={ + "callPointerAddress": f"0x{addr:08x}", + "arg0": 0, + "absoluteAddressRelocations": single_absolute_address_relocation(4, addr), + }, extra_flags=("/O1",), ), GeneratedCandidate( @@ -8692,7 +8707,11 @@ def call_indirect_zero(row: dict[str, Any], c_name: str, data: bytes) -> list[Ge source=loaded_source, callconv="cdecl", return_type="void", - evidence={"callPointerAddress": f"0x{addr:08x}", "arg0": 0}, + evidence={ + "callPointerAddress": f"0x{addr:08x}", + "arg0": 0, + "absoluteAddressRelocations": single_absolute_address_relocation(4, addr), + }, extra_flags=("/O1",), ), ] @@ -9533,7 +9552,12 @@ def virtual_call_eq_global(row: dict[str, Any], c_name: str, data: bytes) -> lis source=source_direct, callconv="fastcall", return_type="unsigned char", - evidence={"vtableSlotBytes": slot_bytes, "vtableSlotIndex": slot_index, "absoluteAddress": f"0x{addr:08x}"}, + evidence={ + "vtableSlotBytes": slot_bytes, + "vtableSlotIndex": slot_index, + "absoluteAddress": f"0x{addr:08x}", + "absoluteAddressRelocations": single_absolute_address_relocation(7, addr), + }, ), GeneratedCandidate( rule="virtual-call-eq-global", @@ -9543,7 +9567,12 @@ def virtual_call_eq_global(row: dict[str, Any], c_name: str, data: bytes) -> lis source=source_char_temp, callconv="fastcall", return_type="unsigned char", - evidence={"vtableSlotBytes": slot_bytes, "vtableSlotIndex": slot_index, "absoluteAddress": f"0x{addr:08x}"}, + evidence={ + "vtableSlotBytes": slot_bytes, + "vtableSlotIndex": slot_index, + "absoluteAddress": f"0x{addr:08x}", + "absoluteAddressRelocations": single_absolute_address_relocation(7, addr), + }, extra_flags=("/O1",), ), ] @@ -9659,7 +9688,10 @@ def import_call_return_self(row: dict[str, Any], c_name: str, data: bytes) -> li source=source, callconv="fastcall", return_type="void *", - evidence={"callPointerAddress": f"0x{addr:08x}"}, + evidence={ + "callPointerAddress": f"0x{addr:08x}", + "absoluteAddressRelocations": single_absolute_address_relocation(6, addr), + }, ) ] @@ -9752,7 +9784,10 @@ def global_indexed_store_cdecl(row: dict[str, Any], c_name: str, data: bytes) -> source=source, callconv="cdecl", return_type="void", - evidence={"absoluteAddress": f"0x{addr:08x}"}, + evidence={ + "absoluteAddress": f"0x{addr:08x}", + "absoluteAddressRelocations": single_absolute_address_relocation(11, addr), + }, ) ] @@ -17174,7 +17209,11 @@ def import_call_arg_return_one_stdcall8(row: dict[str, Any], c_name: str, data: source=source_direct, callconv="stdcall", return_type="int", - evidence={"callPointerAddress": f"0x{addr:08x}", "stackBytes": 8}, + evidence={ + "callPointerAddress": f"0x{addr:08x}", + "stackBytes": 8, + "absoluteAddressRelocations": single_absolute_address_relocation(6, addr), + }, ), GeneratedCandidate( rule="import-call-arg-return-one-stdcall8", @@ -17184,7 +17223,11 @@ def import_call_arg_return_one_stdcall8(row: dict[str, Any], c_name: str, data: source=source_inc, callconv="stdcall", return_type="int", - evidence={"callPointerAddress": f"0x{addr:08x}", "stackBytes": 8}, + evidence={ + "callPointerAddress": f"0x{addr:08x}", + "stackBytes": 8, + "absoluteAddressRelocations": single_absolute_address_relocation(6, addr), + }, extra_flags=("/O1",), ), ] @@ -17711,7 +17754,12 @@ def global_virtual_call_stack_arg(row: dict[str, Any], c_name: str, data: bytes) source=source_value_first, callconv="cdecl", return_type="void", - evidence={"absoluteAddress": f"0x{addr:08x}", "vtableSlotBytes": slot, "vtableSlotIndex": slot_index}, + evidence={ + "absoluteAddress": f"0x{addr:08x}", + "vtableSlotBytes": slot, + "vtableSlotIndex": slot_index, + "absoluteAddressRelocations": single_absolute_address_relocation(2, addr), + }, ), GeneratedCandidate( rule="global-virtual-call-stack-arg", @@ -17721,7 +17769,13 @@ def global_virtual_call_stack_arg(row: dict[str, Any], c_name: str, data: bytes) source=source_obj_first, callconv="cdecl", return_type="void", - evidence={"absoluteAddress": f"0x{addr:08x}", "vtableSlotBytes": slot, "vtableSlotIndex": slot_index, "loadOrder": "object-first"}, + evidence={ + "absoluteAddress": f"0x{addr:08x}", + "vtableSlotBytes": slot, + "vtableSlotIndex": slot_index, + "loadOrder": "object-first", + "absoluteAddressRelocations": single_absolute_address_relocation(2, addr), + }, ), ] @@ -18125,7 +18179,11 @@ def global_field_eq_one_bool(row: dict[str, Any], c_name: str, data: bytes) -> l source=source, callconv="cdecl", return_type="int", - evidence={"absoluteAddress": f"0x{addr:08x}", "fieldOffset": offset}, + evidence={ + "absoluteAddress": f"0x{addr:08x}", + "fieldOffset": offset, + "absoluteAddressRelocations": single_absolute_address_relocation(1, addr), + }, ) ] diff --git a/tests/test_rule_generator_relocation_evidence.py b/tests/test_rule_generator_relocation_evidence.py new file mode 100644 index 0000000..7594ce2 --- /dev/null +++ b/tests/test_rule_generator_relocation_evidence.py @@ -0,0 +1,156 @@ +"""Regression tests: 10 mechanical rule generators now populate +absoluteAddressRelocations, closing the same target-side rendering gap +inc_abs_global had (PR #149) -- see +docs/plans/2026-07-30-001-fix-generalize-relocation-evidence-plan.md, U2. + +Each test constructs a byte pattern that satisfies the rule's own guard +clause, calls the generator, and asserts every returned GeneratedCandidate +carries the correct absoluteAddressRelocations entry (or entries, for rules +returning multiple variants built from the same address) -- plus an +integration check that render_target_coff_for_candidate() actually emits a +`.long ` relocation instead of a raw byte blob for the address slice. +""" + +from __future__ import annotations + +from agentdecompile_recovery.source_parity_synthesize import ( + call_indirect_zero, + float_multiply_global, + global_field_eq_one_bool, + global_indexed_store_cdecl, + global_setter_u32_stdcall, + global_virtual_call_stack_arg, + import_call_arg_return_one_stdcall8, + import_call_return_self, + import_call_self_stdcall, + render_target_coff_for_candidate, + virtual_call_eq_global, +) + + +def _assert_relocation(evidence: dict, *, offset: int, addr: int) -> None: + relocations = evidence.get("absoluteAddressRelocations") + assert isinstance(relocations, list) and len(relocations) == 1 + relocation = relocations[0] + assert relocation["offset"] == offset + assert relocation["type"] == "IMAGE_REL_I386_DIR32" + assert relocation["symbol"] == f"_DAT_{addr:08x}" + assert relocation["decodedAddress"] == f"0x{addr:08x}" + + +def test_float_multiply_global_relocation_evidence() -> None: + addr = 0x00830540 + data = b"\xd9\x44\x24\x04\xd8\x0d" + addr.to_bytes(4, "little") + b"\xc3" + candidates = float_multiply_global({}, "FUN_test", data) + assert len(candidates) == 1 + _assert_relocation(candidates[0].evidence, offset=6, addr=addr) + + rendered = render_target_coff_for_candidate(candidates[0], data) + assert f".long _DAT_{addr:08x}" in rendered["asm"] + + +def test_import_call_self_stdcall_relocation_evidence() -> None: + addr = 0x00404000 + data = b"\x51\xff\x15" + addr.to_bytes(4, "little") + b"\xc3" + candidates = import_call_self_stdcall({}, "FUN_test", data) + assert len(candidates) == 1 + _assert_relocation(candidates[0].evidence, offset=3, addr=addr) + + rendered = render_target_coff_for_candidate(candidates[0], data) + assert f".long _DAT_{addr:08x}" in rendered["asm"] + + +def test_global_setter_u32_stdcall_relocation_evidence() -> None: + addr = 0x00830544 + data = b"\x8b\x44\x24\x04\xa3" + addr.to_bytes(4, "little") + b"\xc2\x04\x00" + candidates = global_setter_u32_stdcall({}, "FUN_test", data) + assert len(candidates) == 1 + _assert_relocation(candidates[0].evidence, offset=5, addr=addr) + + rendered = render_target_coff_for_candidate(candidates[0], data) + assert f".long _DAT_{addr:08x}" in rendered["asm"] + + +def test_call_indirect_zero_relocation_evidence_both_variants() -> None: + addr = 0x00405000 + data = b"\x6a\x00\xff\x15" + addr.to_bytes(4, "little") + b"\xc3" + candidates = call_indirect_zero({}, "FUN_test", data) + assert len(candidates) == 2 + for candidate in candidates: + _assert_relocation(candidate.evidence, offset=4, addr=addr) + rendered = render_target_coff_for_candidate(candidate, data) + assert f".long _DAT_{addr:08x}" in rendered["asm"] + + +def test_virtual_call_eq_global_relocation_evidence_both_variants() -> None: + addr = 0x00830548 + data = b"\x8b\x01\xff\x50\x04\x3b\x05" + addr.to_bytes(4, "little") + b"\x0f\x94\xc0\xc3" + candidates = virtual_call_eq_global({}, "FUN_test", data) + assert len(candidates) == 2 + for candidate in candidates: + _assert_relocation(candidate.evidence, offset=7, addr=addr) + rendered = render_target_coff_for_candidate(candidate, data) + assert f".long _DAT_{addr:08x}" in rendered["asm"] + + +def test_import_call_return_self_relocation_evidence() -> None: + addr = 0x00406000 + data = b"\x56\x8b\xf1\x56\xff\x15" + addr.to_bytes(4, "little") + b"\x8b\xc6\x5e\xc3" + candidates = import_call_return_self({}, "FUN_test", data) + assert len(candidates) == 1 + _assert_relocation(candidates[0].evidence, offset=6, addr=addr) + + rendered = render_target_coff_for_candidate(candidates[0], data) + assert f".long _DAT_{addr:08x}" in rendered["asm"] + + +def test_global_indexed_store_cdecl_relocation_evidence() -> None: + addr = 0x0083054C + data = b"\x8b\x44\x24\x08\x8b\x4c\x24\x04\x89\x04\x8d" + addr.to_bytes(4, "little") + b"\xc3" + candidates = global_indexed_store_cdecl({}, "FUN_test", data) + assert len(candidates) == 1 + _assert_relocation(candidates[0].evidence, offset=11, addr=addr) + + rendered = render_target_coff_for_candidate(candidates[0], data) + assert f".long _DAT_{addr:08x}" in rendered["asm"] + + +def test_import_call_arg_return_one_stdcall8_relocation_evidence_both_variants() -> None: + addr = 0x00407000 + data = b"\xff\x74\x24\x04\xff\x15" + addr.to_bytes(4, "little") + b"\x33\xc0\x40\xc2\x08\x00" + candidates = import_call_arg_return_one_stdcall8({}, "FUN_test", data) + assert len(candidates) == 2 + for candidate in candidates: + _assert_relocation(candidate.evidence, offset=6, addr=addr) + rendered = render_target_coff_for_candidate(candidate, data) + assert f".long _DAT_{addr:08x}" in rendered["asm"] + + +def test_global_virtual_call_stack_arg_relocation_evidence_both_variants() -> None: + addr = 0x00830550 + data = ( + b"\x8b\x0d" + + addr.to_bytes(4, "little") + + b"\x8b\x54\x24\x04\x8b\x01\x52\xff\x50\x04\xc3" + ) + candidates = global_virtual_call_stack_arg({}, "FUN_test", data) + assert len(candidates) == 2 + for candidate in candidates: + _assert_relocation(candidate.evidence, offset=2, addr=addr) + rendered = render_target_coff_for_candidate(candidate, data) + assert f".long _DAT_{addr:08x}" in rendered["asm"] + + +def test_global_field_eq_one_bool_relocation_evidence() -> None: + addr = 0x00830554 + data = ( + b"\xa1" + + addr.to_bytes(4, "little") + + b"\x8b\x48\x08\x8b\x11\x33\xc0\x83\xfa\x01\x0f\x94\xc0\xc3" + ) + candidates = global_field_eq_one_bool({}, "FUN_test", data) + assert len(candidates) == 1 + _assert_relocation(candidates[0].evidence, offset=1, addr=addr) + + rendered = render_target_coff_for_candidate(candidates[0], data) + assert f".long _DAT_{addr:08x}" in rendered["asm"] From d85c5a3550172c251bf37373c76c0b1a7c3bc81e Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 30 Jul 2026 04:09:56 -0500 Subject: [PATCH 03/10] Revert "feat(source-parity): wire relocation evidence into 10 single-address rules" This reverts commit f802d73db3bb536438535c280a1bd6d1b19e1885. --- .../source_parity_synthesize.py | 86 ++-------- ...test_rule_generator_relocation_evidence.py | 156 ------------------ 2 files changed, 14 insertions(+), 228 deletions(-) delete mode 100644 tests/test_rule_generator_relocation_evidence.py diff --git a/src/agentdecompile_recovery/source_parity_synthesize.py b/src/agentdecompile_recovery/source_parity_synthesize.py index 36979f4..0f08dee 100755 --- a/src/agentdecompile_recovery/source_parity_synthesize.py +++ b/src/agentdecompile_recovery/source_parity_synthesize.py @@ -704,10 +704,7 @@ def float_multiply_global(row: dict[str, Any], c_name: str, data: bytes) -> list source=source, callconv="cdecl", return_type="float", - evidence={ - "absoluteAddress": f"0x{addr:08x}", - "absoluteAddressRelocations": single_absolute_address_relocation(6, addr), - }, + evidence={"absoluteAddress": f"0x{addr:08x}"}, ) ] @@ -761,10 +758,7 @@ def import_call_self_stdcall(row: dict[str, Any], c_name: str, data: bytes) -> l source=source, callconv="fastcall", return_type="void", - evidence={ - "callPointerAddress": f"0x{addr:08x}", - "absoluteAddressRelocations": single_absolute_address_relocation(3, addr), - }, + evidence={"callPointerAddress": f"0x{addr:08x}"}, ) ] @@ -5305,12 +5299,7 @@ def global_setter_u32_stdcall(row: dict[str, Any], c_name: str, data: bytes) -> source=source, callconv="stdcall", return_type="void", - evidence={ - "pattern": "mov-eax-stack4-store-abs-ret4", - "address": f"0x{address:08x}", - "stackBytes": 4, - "absoluteAddressRelocations": single_absolute_address_relocation(5, address), - }, + evidence={"pattern": "mov-eax-stack4-store-abs-ret4", "address": f"0x{address:08x}", "stackBytes": 4}, ) ] @@ -8692,11 +8681,7 @@ def call_indirect_zero(row: dict[str, Any], c_name: str, data: bytes) -> list[Ge source=direct_source, callconv="cdecl", return_type="void", - evidence={ - "callPointerAddress": f"0x{addr:08x}", - "arg0": 0, - "absoluteAddressRelocations": single_absolute_address_relocation(4, addr), - }, + evidence={"callPointerAddress": f"0x{addr:08x}", "arg0": 0}, extra_flags=("/O1",), ), GeneratedCandidate( @@ -8707,11 +8692,7 @@ def call_indirect_zero(row: dict[str, Any], c_name: str, data: bytes) -> list[Ge source=loaded_source, callconv="cdecl", return_type="void", - evidence={ - "callPointerAddress": f"0x{addr:08x}", - "arg0": 0, - "absoluteAddressRelocations": single_absolute_address_relocation(4, addr), - }, + evidence={"callPointerAddress": f"0x{addr:08x}", "arg0": 0}, extra_flags=("/O1",), ), ] @@ -9552,12 +9533,7 @@ def virtual_call_eq_global(row: dict[str, Any], c_name: str, data: bytes) -> lis source=source_direct, callconv="fastcall", return_type="unsigned char", - evidence={ - "vtableSlotBytes": slot_bytes, - "vtableSlotIndex": slot_index, - "absoluteAddress": f"0x{addr:08x}", - "absoluteAddressRelocations": single_absolute_address_relocation(7, addr), - }, + evidence={"vtableSlotBytes": slot_bytes, "vtableSlotIndex": slot_index, "absoluteAddress": f"0x{addr:08x}"}, ), GeneratedCandidate( rule="virtual-call-eq-global", @@ -9567,12 +9543,7 @@ def virtual_call_eq_global(row: dict[str, Any], c_name: str, data: bytes) -> lis source=source_char_temp, callconv="fastcall", return_type="unsigned char", - evidence={ - "vtableSlotBytes": slot_bytes, - "vtableSlotIndex": slot_index, - "absoluteAddress": f"0x{addr:08x}", - "absoluteAddressRelocations": single_absolute_address_relocation(7, addr), - }, + evidence={"vtableSlotBytes": slot_bytes, "vtableSlotIndex": slot_index, "absoluteAddress": f"0x{addr:08x}"}, extra_flags=("/O1",), ), ] @@ -9688,10 +9659,7 @@ def import_call_return_self(row: dict[str, Any], c_name: str, data: bytes) -> li source=source, callconv="fastcall", return_type="void *", - evidence={ - "callPointerAddress": f"0x{addr:08x}", - "absoluteAddressRelocations": single_absolute_address_relocation(6, addr), - }, + evidence={"callPointerAddress": f"0x{addr:08x}"}, ) ] @@ -9784,10 +9752,7 @@ def global_indexed_store_cdecl(row: dict[str, Any], c_name: str, data: bytes) -> source=source, callconv="cdecl", return_type="void", - evidence={ - "absoluteAddress": f"0x{addr:08x}", - "absoluteAddressRelocations": single_absolute_address_relocation(11, addr), - }, + evidence={"absoluteAddress": f"0x{addr:08x}"}, ) ] @@ -17209,11 +17174,7 @@ def import_call_arg_return_one_stdcall8(row: dict[str, Any], c_name: str, data: source=source_direct, callconv="stdcall", return_type="int", - evidence={ - "callPointerAddress": f"0x{addr:08x}", - "stackBytes": 8, - "absoluteAddressRelocations": single_absolute_address_relocation(6, addr), - }, + evidence={"callPointerAddress": f"0x{addr:08x}", "stackBytes": 8}, ), GeneratedCandidate( rule="import-call-arg-return-one-stdcall8", @@ -17223,11 +17184,7 @@ def import_call_arg_return_one_stdcall8(row: dict[str, Any], c_name: str, data: source=source_inc, callconv="stdcall", return_type="int", - evidence={ - "callPointerAddress": f"0x{addr:08x}", - "stackBytes": 8, - "absoluteAddressRelocations": single_absolute_address_relocation(6, addr), - }, + evidence={"callPointerAddress": f"0x{addr:08x}", "stackBytes": 8}, extra_flags=("/O1",), ), ] @@ -17754,12 +17711,7 @@ def global_virtual_call_stack_arg(row: dict[str, Any], c_name: str, data: bytes) source=source_value_first, callconv="cdecl", return_type="void", - evidence={ - "absoluteAddress": f"0x{addr:08x}", - "vtableSlotBytes": slot, - "vtableSlotIndex": slot_index, - "absoluteAddressRelocations": single_absolute_address_relocation(2, addr), - }, + evidence={"absoluteAddress": f"0x{addr:08x}", "vtableSlotBytes": slot, "vtableSlotIndex": slot_index}, ), GeneratedCandidate( rule="global-virtual-call-stack-arg", @@ -17769,13 +17721,7 @@ def global_virtual_call_stack_arg(row: dict[str, Any], c_name: str, data: bytes) source=source_obj_first, callconv="cdecl", return_type="void", - evidence={ - "absoluteAddress": f"0x{addr:08x}", - "vtableSlotBytes": slot, - "vtableSlotIndex": slot_index, - "loadOrder": "object-first", - "absoluteAddressRelocations": single_absolute_address_relocation(2, addr), - }, + evidence={"absoluteAddress": f"0x{addr:08x}", "vtableSlotBytes": slot, "vtableSlotIndex": slot_index, "loadOrder": "object-first"}, ), ] @@ -18179,11 +18125,7 @@ def global_field_eq_one_bool(row: dict[str, Any], c_name: str, data: bytes) -> l source=source, callconv="cdecl", return_type="int", - evidence={ - "absoluteAddress": f"0x{addr:08x}", - "fieldOffset": offset, - "absoluteAddressRelocations": single_absolute_address_relocation(1, addr), - }, + evidence={"absoluteAddress": f"0x{addr:08x}", "fieldOffset": offset}, ) ] diff --git a/tests/test_rule_generator_relocation_evidence.py b/tests/test_rule_generator_relocation_evidence.py deleted file mode 100644 index 7594ce2..0000000 --- a/tests/test_rule_generator_relocation_evidence.py +++ /dev/null @@ -1,156 +0,0 @@ -"""Regression tests: 10 mechanical rule generators now populate -absoluteAddressRelocations, closing the same target-side rendering gap -inc_abs_global had (PR #149) -- see -docs/plans/2026-07-30-001-fix-generalize-relocation-evidence-plan.md, U2. - -Each test constructs a byte pattern that satisfies the rule's own guard -clause, calls the generator, and asserts every returned GeneratedCandidate -carries the correct absoluteAddressRelocations entry (or entries, for rules -returning multiple variants built from the same address) -- plus an -integration check that render_target_coff_for_candidate() actually emits a -`.long ` relocation instead of a raw byte blob for the address slice. -""" - -from __future__ import annotations - -from agentdecompile_recovery.source_parity_synthesize import ( - call_indirect_zero, - float_multiply_global, - global_field_eq_one_bool, - global_indexed_store_cdecl, - global_setter_u32_stdcall, - global_virtual_call_stack_arg, - import_call_arg_return_one_stdcall8, - import_call_return_self, - import_call_self_stdcall, - render_target_coff_for_candidate, - virtual_call_eq_global, -) - - -def _assert_relocation(evidence: dict, *, offset: int, addr: int) -> None: - relocations = evidence.get("absoluteAddressRelocations") - assert isinstance(relocations, list) and len(relocations) == 1 - relocation = relocations[0] - assert relocation["offset"] == offset - assert relocation["type"] == "IMAGE_REL_I386_DIR32" - assert relocation["symbol"] == f"_DAT_{addr:08x}" - assert relocation["decodedAddress"] == f"0x{addr:08x}" - - -def test_float_multiply_global_relocation_evidence() -> None: - addr = 0x00830540 - data = b"\xd9\x44\x24\x04\xd8\x0d" + addr.to_bytes(4, "little") + b"\xc3" - candidates = float_multiply_global({}, "FUN_test", data) - assert len(candidates) == 1 - _assert_relocation(candidates[0].evidence, offset=6, addr=addr) - - rendered = render_target_coff_for_candidate(candidates[0], data) - assert f".long _DAT_{addr:08x}" in rendered["asm"] - - -def test_import_call_self_stdcall_relocation_evidence() -> None: - addr = 0x00404000 - data = b"\x51\xff\x15" + addr.to_bytes(4, "little") + b"\xc3" - candidates = import_call_self_stdcall({}, "FUN_test", data) - assert len(candidates) == 1 - _assert_relocation(candidates[0].evidence, offset=3, addr=addr) - - rendered = render_target_coff_for_candidate(candidates[0], data) - assert f".long _DAT_{addr:08x}" in rendered["asm"] - - -def test_global_setter_u32_stdcall_relocation_evidence() -> None: - addr = 0x00830544 - data = b"\x8b\x44\x24\x04\xa3" + addr.to_bytes(4, "little") + b"\xc2\x04\x00" - candidates = global_setter_u32_stdcall({}, "FUN_test", data) - assert len(candidates) == 1 - _assert_relocation(candidates[0].evidence, offset=5, addr=addr) - - rendered = render_target_coff_for_candidate(candidates[0], data) - assert f".long _DAT_{addr:08x}" in rendered["asm"] - - -def test_call_indirect_zero_relocation_evidence_both_variants() -> None: - addr = 0x00405000 - data = b"\x6a\x00\xff\x15" + addr.to_bytes(4, "little") + b"\xc3" - candidates = call_indirect_zero({}, "FUN_test", data) - assert len(candidates) == 2 - for candidate in candidates: - _assert_relocation(candidate.evidence, offset=4, addr=addr) - rendered = render_target_coff_for_candidate(candidate, data) - assert f".long _DAT_{addr:08x}" in rendered["asm"] - - -def test_virtual_call_eq_global_relocation_evidence_both_variants() -> None: - addr = 0x00830548 - data = b"\x8b\x01\xff\x50\x04\x3b\x05" + addr.to_bytes(4, "little") + b"\x0f\x94\xc0\xc3" - candidates = virtual_call_eq_global({}, "FUN_test", data) - assert len(candidates) == 2 - for candidate in candidates: - _assert_relocation(candidate.evidence, offset=7, addr=addr) - rendered = render_target_coff_for_candidate(candidate, data) - assert f".long _DAT_{addr:08x}" in rendered["asm"] - - -def test_import_call_return_self_relocation_evidence() -> None: - addr = 0x00406000 - data = b"\x56\x8b\xf1\x56\xff\x15" + addr.to_bytes(4, "little") + b"\x8b\xc6\x5e\xc3" - candidates = import_call_return_self({}, "FUN_test", data) - assert len(candidates) == 1 - _assert_relocation(candidates[0].evidence, offset=6, addr=addr) - - rendered = render_target_coff_for_candidate(candidates[0], data) - assert f".long _DAT_{addr:08x}" in rendered["asm"] - - -def test_global_indexed_store_cdecl_relocation_evidence() -> None: - addr = 0x0083054C - data = b"\x8b\x44\x24\x08\x8b\x4c\x24\x04\x89\x04\x8d" + addr.to_bytes(4, "little") + b"\xc3" - candidates = global_indexed_store_cdecl({}, "FUN_test", data) - assert len(candidates) == 1 - _assert_relocation(candidates[0].evidence, offset=11, addr=addr) - - rendered = render_target_coff_for_candidate(candidates[0], data) - assert f".long _DAT_{addr:08x}" in rendered["asm"] - - -def test_import_call_arg_return_one_stdcall8_relocation_evidence_both_variants() -> None: - addr = 0x00407000 - data = b"\xff\x74\x24\x04\xff\x15" + addr.to_bytes(4, "little") + b"\x33\xc0\x40\xc2\x08\x00" - candidates = import_call_arg_return_one_stdcall8({}, "FUN_test", data) - assert len(candidates) == 2 - for candidate in candidates: - _assert_relocation(candidate.evidence, offset=6, addr=addr) - rendered = render_target_coff_for_candidate(candidate, data) - assert f".long _DAT_{addr:08x}" in rendered["asm"] - - -def test_global_virtual_call_stack_arg_relocation_evidence_both_variants() -> None: - addr = 0x00830550 - data = ( - b"\x8b\x0d" - + addr.to_bytes(4, "little") - + b"\x8b\x54\x24\x04\x8b\x01\x52\xff\x50\x04\xc3" - ) - candidates = global_virtual_call_stack_arg({}, "FUN_test", data) - assert len(candidates) == 2 - for candidate in candidates: - _assert_relocation(candidate.evidence, offset=2, addr=addr) - rendered = render_target_coff_for_candidate(candidate, data) - assert f".long _DAT_{addr:08x}" in rendered["asm"] - - -def test_global_field_eq_one_bool_relocation_evidence() -> None: - addr = 0x00830554 - data = ( - b"\xa1" - + addr.to_bytes(4, "little") - + b"\x8b\x48\x08\x8b\x11\x33\xc0\x83\xfa\x01\x0f\x94\xc0\xc3" - ) - candidates = global_field_eq_one_bool({}, "FUN_test", data) - assert len(candidates) == 1 - _assert_relocation(candidates[0].evidence, offset=1, addr=addr) - - rendered = render_target_coff_for_candidate(candidates[0], data) - assert f".long _DAT_{addr:08x}" in rendered["asm"] From a6a5e539505c249be9e6dda6f51838fc7d673762 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 30 Jul 2026 04:10:41 -0500 Subject: [PATCH 04/10] docs(source-parity): document why the multi-address rules need no relocation fix Real-toolchain A/B testing (real MSVC8/wine compile + real objdiff, not just unit-level assertions) overturned this plan's premise for both rep_stos_global_clear and global_and_global_bool: - rep_stos_global_clear's only candidate embeds every address as literal bytes via inline-asm _emit, not a pointer dereference -- it already reaches differences: 0 with no change. - global_and_global_bool dereferences addresses via literal C pointer casts, which MSVC compiles as bare immediates (no relocation) -- adding target-side absoluteAddressRelocations against a literal-cast candidate was A/B tested and found to be strictly worse (ARGUMENT_MISMATCH 6 vs 2 on the best profile), not better. Reverted. The corrected understanding: relocation evidence only helps when the candidate's own compiled object references the address through a compiler-emitted relocation (a named extern symbol), which none of these mechanical rule generators produce -- they all synthesize literal pointer casts. This changes the scope of U2/U4 in docs/plans/2026-07-30-001-fix-generalize-relocation-evidence-plan.md. U3 of docs/plans/2026-07-30-001-fix-generalize-relocation-evidence-plan.md --- ..._multi_address_rule_relocation_evidence.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test_multi_address_rule_relocation_evidence.py diff --git a/tests/test_multi_address_rule_relocation_evidence.py b/tests/test_multi_address_rule_relocation_evidence.py new file mode 100644 index 0000000..d2baa5b --- /dev/null +++ b/tests/test_multi_address_rule_relocation_evidence.py @@ -0,0 +1,63 @@ +"""U3 of docs/plans/2026-07-30-001-fix-generalize-relocation-evidence-plan.md: +findings on the two multi-address rule generators the plan targeted. + +Real-toolchain verification (real MSVC8/wine compile + real objdiff, not just +unit-level evidence-shape assertions) overturned this plan's premise for both +rules -- see the plan's amended Key Technical Decisions for the full writeup. +Neither rule was changed; this file documents why, so the finding isn't lost +or rediscovered as an open gap. + +**The corrected understanding:** `render_target_coff_for_candidate()`'s +relocation reconstruction only helps when the candidate's OWN compiled object +also references the address through a compiler-emitted relocation (e.g. a +named `extern` symbol, as in a packaged-source candidate's +`DAT_00830540 = DAT_00830540 + 1;`, or a subagent's inline-asm rewrite +referencing a named symbol). Every rule generator in this codebase that +synthesizes a raw C literal pointer cast (`*(unsigned int *)0x...`) compiles +that address as a bare immediate with NO relocation on the candidate side -- +MSVC does not route a literal address cast through an extern symbol. Adding +`absoluteAddressRelocations` to reconstruct a *symbolic* target side against a +candidate that stays *literal* does not help; it was empirically confirmed to +introduce spurious `ARGUMENT_MISMATCH` entries that don't exist without the +evidence (verified for `global_and_global_bool` and, separately, for one of +the ten single-address rules this plan also reverted -- see git history on +this branch). + +- `global_and_global_bool` dereferences two absolute addresses via literal + casts. A/B tested against the real toolchain: WITHOUT relocation evidence, + the best profile shows `ARGUMENT_MISMATCH: 2`; WITH the evidence this plan + originally added, the same profile shows `ARGUMENT_MISMATCH: 6` -- strictly + worse. The evidence was reverted; no fix was needed or applied. +- `rep_stos_global_clear` was a **false positive** from a different angle: + its only returned candidate embeds every address as literal bytes via + inline-asm `_emit` directives, not any pointer dereference at all. Verified + directly against the real toolchain: it already reaches `differences: 0` + with no evidence change. +""" + +from __future__ import annotations + +from agentdecompile_recovery.source_parity_synthesize import global_and_global_bool + + +def test_global_and_global_bool_uses_literal_address_casts_not_relocations() -> None: + """Documents the current (correct, unchanged) shape: no + absoluteAddressRelocations, because the candidate source uses literal + pointer casts that a real MSVC compile bakes as bare immediates, not a + relocation an objdiff target reconstruction could usefully mirror. + """ + + left = 0x00830560 + right = 0x00830564 + data = ( + b"\xa1" + + left.to_bytes(4, "little") + + b"\x8b\x0d" + + right.to_bytes(4, "little") + + b"\x23\xc8\x3b\xc8\x0f\x94\xc0\xc3" + ) + candidates = global_and_global_bool({}, "FUN_test", data) + assert len(candidates) == 1 + assert "absoluteAddressRelocations" not in candidates[0].evidence + assert f"0x{left:08x}" in candidates[0].source + assert f"0x{right:08x}" in candidates[0].source From 7f1120580bfce1e116b071303eb19dabe658df6e Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 30 Jul 2026 04:12:25 -0500 Subject: [PATCH 05/10] test(source-parity): guard against pairing relocation evidence with literal casts U4 of docs/plans/2026-07-30-001-fix-generalize-relocation-evidence-plan.md, with corrected scope: guards the real anti-pattern found during U2/U3 (absoluteAddressRelocations paired with a literal pointer cast, which real-toolchain testing showed makes objdiff matching worse, not better) rather than the plan's original premise (missing evidence), which real verification overturned. --- .../test_no_unrelocated_absolute_addresses.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 tests/test_no_unrelocated_absolute_addresses.py diff --git a/tests/test_no_unrelocated_absolute_addresses.py b/tests/test_no_unrelocated_absolute_addresses.py new file mode 100644 index 0000000..fa48301 --- /dev/null +++ b/tests/test_no_unrelocated_absolute_addresses.py @@ -0,0 +1,109 @@ +"""U4 of docs/plans/2026-07-30-001-fix-generalize-relocation-evidence-plan.md. + +**Scope correction (documented in the plan's amended Key Technical +Decisions):** the plan originally set out to guard against rule generators +that embed a raw absolute address in generated C source *without* populating +`absoluteAddressRelocations`. Real-toolchain investigation (real MSVC8/wine +compile + real objdiff) overturned that premise: `absoluteAddressRelocations` +only helps when the candidate's own compiled object references the address +through a compiler-emitted relocation (a named `extern` symbol) -- not a raw +literal pointer cast (`*(unsigned int *)0x...`), which MSVC compiles as a bare +immediate with no relocation. Adding the evidence to a literal-cast candidate +was A/B tested and found to make matching *worse* (spurious +`ARGUMENT_MISMATCH` entries), not better -- confirmed for +`global_and_global_bool` and for one of the ten single-address rules this +plan's U2 originally (and incorrectly) wired up, then reverted. + +This test guards the corrected, real anti-pattern instead: a rule generator +must never populate `absoluteAddressRelocations` for an address its own +generated source only references via a literal pointer cast. `inc_abs_global` +is exempt from this check -- its own literal-cast candidates are unaffected +either way (real-toolchain A/B testing showed identical results with and +without the evidence), and the evidence field exists there specifically so a +*different*, later-constructed candidate for the same target (e.g. a +subagent rewrite referencing a named `DAT_` symbol, per +docs/solutions/architecture-patterns/rewrite-queue-subagent-fulfillment.md) +can carry matching relocation evidence of its own. Extending that +inheritance to fire automatically for every packaged-source/rewrite +candidate is a distinct, deferred piece of work -- see the plan's Scope +Boundaries. +""" + +from __future__ import annotations + +import ast +import re +from pathlib import Path + +SOURCE_PATH = Path(__file__).resolve().parent.parent / "src" / "agentdecompile_recovery" / "source_parity_synthesize.py" + +# Rules exempt from this check: their own literal-cast candidate is +# unaffected by the evidence (verified real-toolchain neutral, not harmful), +# and the evidence exists to serve a different, later-constructed candidate. +EXEMPT_RULE_FUNCTIONS = {"inc_abs_global"} + +LITERAL_CAST_RE = re.compile(r"\*\s*\([^)]*\*\)\s*0x\{[a-zA-Z_]+") + + +def _iter_rule_generator_functions(tree: ast.Module) -> list[ast.FunctionDef]: + functions = [] + for node in ast.walk(tree): + if not isinstance(node, ast.FunctionDef): + continue + args = [a.arg for a in node.args.args] + if args[:3] == ["row", "c_name", "data"]: + functions.append(node) + return functions + + +def _function_source(source_lines: list[str], node: ast.FunctionDef) -> str: + return "\n".join(source_lines[node.lineno - 1 : node.end_lineno]) + + +def test_no_rule_generator_pairs_relocation_evidence_with_a_literal_cast() -> None: + text = SOURCE_PATH.read_text(encoding="utf-8") + tree = ast.parse(text) + source_lines = text.splitlines() + + violations = [] + for node in _iter_rule_generator_functions(tree): + if node.name in EXEMPT_RULE_FUNCTIONS: + continue + body = _function_source(source_lines, node) + if "absoluteAddressRelocations" not in body: + continue + if LITERAL_CAST_RE.search(body): + violations.append(node.name) + + assert violations == [], ( + f"Rule generator(s) {violations} populate absoluteAddressRelocations " + "while their generated source only references the address via a " + "literal pointer cast -- real-toolchain testing showed this makes " + "objdiff matching worse, not better (see this test's module " + "docstring). Either remove the evidence, or change the generated " + "source to reference the address through a named extern symbol." + ) + + +def test_check_actually_detects_the_anti_pattern() -> None: + """Regression guard on the check itself: prove it fires on a synthetic + function shaped exactly like the mistake this test exists to prevent. + """ + + synthetic_source = ( + "def fake_rule(row: dict[str, Any], c_name: str, data: bytes) -> list[GeneratedCandidate]:\n" + " addr = u32(data[2:6])\n" + ' source = f"*(unsigned int *)0x{addr:08x} = 1;"\n' + " return [GeneratedCandidate(\n" + ' rule="fake", variant="fake", c_name=c_name, symbol=c_name,\n' + " source=source, callconv=\"cdecl\", return_type=\"void\",\n" + ' evidence={"absoluteAddressRelocations": [{"offset": 2}]},\n' + " )]\n" + ) + tree = ast.parse(synthetic_source) + source_lines = synthetic_source.splitlines() + functions = _iter_rule_generator_functions(tree) + assert len(functions) == 1 + body = _function_source(source_lines, functions[0]) + assert "absoluteAddressRelocations" in body + assert LITERAL_CAST_RE.search(body) is not None From 9ed21f15fd27430d2602acdb9d76b8eea96b8134 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 30 Jul 2026 04:16:39 -0500 Subject: [PATCH 06/10] docs(plan): record corrected findings from real-toolchain verification U2 and U3's original premise (generalize PR #149's relocation-evidence fix to 12 more mechanical rule generators) was overturned by real MSVC8/wine + objdiff A/B testing during implementation: every one of those rules synthesizes a literal pointer cast, which MSVC compiles as a bare immediate with no relocation, so the target-side reconstruction this evidence enables has nothing matching to mirror on the candidate side. Confirmed to make matching measurably worse for the tested representatives, not better. The plan is updated in place to record what was actually found and shipped: a reusable helper (U1), a regression test guarding the real anti-pattern found (U4), and the real generalization opportunity this surfaced (relocation-evidence inheritance for subagent-rewrite candidates) deferred as follow-up. --- ...on-evidence-generalization-requirements.md | 40 ++++++ ...fix-generalize-relocation-evidence-plan.md | 136 ++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 docs/brainstorms/2026-07-30-relocation-evidence-generalization-requirements.md create mode 100644 docs/plans/2026-07-30-001-fix-generalize-relocation-evidence-plan.md diff --git a/docs/brainstorms/2026-07-30-relocation-evidence-generalization-requirements.md b/docs/brainstorms/2026-07-30-relocation-evidence-generalization-requirements.md new file mode 100644 index 0000000..d13b2a1 --- /dev/null +++ b/docs/brainstorms/2026-07-30-relocation-evidence-generalization-requirements.md @@ -0,0 +1,40 @@ +--- +date: 2026-07-30 +topic: relocation-evidence-generalization +--- + +# Generalize target-side relocation evidence across mechanical rule generators + +## Summary + +Generalize the relocation-evidence fix shipped in PR #149 (`inc_abs_global`) to every other mechanical rule generator in `src/agentdecompile_recovery/source_parity_synthesize.py` with the same gap, via a shared helper plus a regression test that prevents the pattern from recurring in future rules. + +## Problem Frame + +`render_target_coff_for_candidate()` can reconstruct the objdiff target object with a matching symbol relocation for absolute-address (`DIR32`) references, via `absolute_address_relocations()`, which reads `candidate.evidence["absoluteAddressRelocations"]`. Without it, the target side falls back to a raw byte blob with the address baked in literally, and any candidate that correctly references the same global through a compiler-visible symbol can never byte-match — confirmed empirically (PR #149) against the real MSVC8/wine toolchain and real objdiff. + +`inc_abs_global` had this gap and is now fixed. A scan for the same shape (a rule generator embedding a raw absolute address in generated C source without populating `absoluteAddressRelocations`) found roughly twenty more rule functions with the identical pattern. Some of these rules validate a single fixed-offset address the same way `inc_abs_global` did (e.g. `float_multiply_global`); others may reference multiple addresses or non-global values the initial heuristic scan can't distinguish without closer reading per function. Left unfixed, any function in a real recovery run whose target-matching rule falls into this class faces the same unclosable ceiling `inc_abs_global` did — a candidate can be semantically and structurally perfect and still never reach `objdiff differences: 0`. + +## Requirements + +- R1. Every mechanical rule generator that embeds an absolute global address in its generated C source populates `absoluteAddressRelocations` in its `GeneratedCandidate.evidence`, following the same `{offset, type: "IMAGE_REL_I386_DIR32", symbol, decodedAddress}` shape `absolute_address_relocations()` already reads. +- R2. A shared helper covers the common single-address case so each affected rule adopts the fix in one line rather than a hand-rolled dict, mirroring the naming convention already established (`_DAT_`-style symbols). +- R3. Rules with more than one absolute-address reference (following the `bink_buffer_set_direct_draw_forwarder` shape) are verified individually rather than assumed to fit the single-address helper. +- R4. A regression test scans rule generators for the anti-pattern (a literal hex address baked into generated C source with no matching relocation evidence) so a future new rule cannot reintroduce this gap silently. +- R5. Each fixed rule is verified against the real MSVC8/wine toolchain and real objdiff for at least one representative byte pattern, not just a unit-level assertion on the evidence shape (the same standard PR #149 held itself to). + +## Scope Boundaries + +- Does not change the objdiff verification harness itself (`run_objdiff`, `parse_objdiff_report`) — PR #149 established that harness is correct once given matching relocation evidence. +- Does not attempt to fix the separate, already-documented "remaining 64" candidate-generation-quality backlog item (Ghidra-synthesized type names, embedded `int3` artifacts) — that is a distinct class of bug. +- Does not run a full proof-campaign or vacuum loop against `swkotor.exe` to chase new verified accepts — that is a separate, later effort this fix unblocks but does not itself perform. + +## Dependencies / Assumptions + +- Assumes the exact set of affected rule generators is confirmed by reading each flagged function during planning/implementation, not taken as a fixed count from the initial heuristic scan. +- Assumes the real MSVC8/wine toolchain used to verify PR #149 (`VC_ROOT=/run/media/brunner56/MyBook/Toolchains/msvc8.0-main`, `WINEPREFIX=target/wine-smoke-prefix`) remains available for per-rule verification. + +## Outstanding Questions + +**Deferred to Planning** +- Whether the regression test (R4) should be a static scan over rule-generator source text, or a data-driven check that calls every rule generator with a representative byte pattern and inspects its returned `evidence` — the right mechanism depends on how uniformly testable the ~20 rules turn out to be once read individually. diff --git a/docs/plans/2026-07-30-001-fix-generalize-relocation-evidence-plan.md b/docs/plans/2026-07-30-001-fix-generalize-relocation-evidence-plan.md new file mode 100644 index 0000000..6810fd2 --- /dev/null +++ b/docs/plans/2026-07-30-001-fix-generalize-relocation-evidence-plan.md @@ -0,0 +1,136 @@ +--- +title: "fix: Generalize target-side relocation evidence across mechanical rule generators" +date: 2026-07-30 +origin: docs/brainstorms/2026-07-30-relocation-evidence-generalization-requirements.md +--- + +# fix: Generalize target-side relocation evidence across mechanical rule generators + +## Summary + +Investigated extending the `absoluteAddressRelocations` fix shipped in PR #149 (`inc_abs_global`) to twelve other mechanical rule generators in `src/agentdecompile_recovery/source_parity_synthesize.py`. Real-toolchain A/B testing overturned the premise: none of the twelve need the fix, and adding it made two confirmed cases measurably worse. Shipped a shared helper (for any future rule that legitimately needs it), reverted the incorrect wiring, and added a regression test guarding the real anti-pattern this investigation found. + +## Problem Frame + +`render_target_coff_for_candidate()` reconstructs the objdiff target object with a matching symbol relocation for absolute-address (`DIR32`) references, via `absolute_address_relocations()`, which reads `candidate.evidence["absoluteAddressRelocations"]`. This only helps when the candidate's own compiled object *also* references the address through a compiler-emitted relocation — a named `extern` symbol, as in a packaged-source candidate's `DAT_00830540 = DAT_00830540 + 1;`, or a subagent-produced inline-asm rewrite referencing a named symbol. + +`inc_abs_global` was fixed in PR #149 on that premise. A full read of every other rule generator embedding a raw absolute address found twelve more candidates for the same fix: ten single-address, two multi-address. Real-toolchain A/B testing (real MSVC8/wine compile + real objdiff, not just unit-level shape assertions) overturned that premise for all twelve: every one of them synthesizes a raw C **literal pointer cast** (`*(unsigned int *)0x...`), not a named-symbol reference — and MSVC compiles a literal cast as a bare immediate with no relocation on the candidate side at all. Reconstructing a *symbolic* target against a candidate that stays *literal* does not help; it was confirmed to introduce spurious `ARGUMENT_MISMATCH` entries that don't exist without the evidence (`global_and_global_bool`: 2 → 6 on the best profile; one of the ten single-address rules showed the same pattern). A/B testing `inc_abs_global`'s own already-shipped fix against its own literal-cast candidate found it neutral (identical mismatch counts with and without the evidence) — its value comes entirely from making the evidence *available* for a later, differently-constructed candidate (e.g. a subagent rewrite) to match against, not from helping its own candidate. + +## Requirements + +- R1. (Superseded — see Key Technical Decisions.) Originally: each of the ten single-address rule generators populates `absoluteAddressRelocations`. Real-toolchain testing found none of the ten need it; none were changed. +- R2. A shared helper (`single_absolute_address_relocation`) produces the `absoluteAddressRelocations` list for the single-address case, available for any rule that genuinely needs it (a candidate referencing the address through a named symbol, not a literal cast), using the same `_DAT_`-style symbol naming `inc_abs_global` established. +- R3. (Superseded — see Key Technical Decisions.) Originally: `rep_stos_global_clear` and `global_and_global_bool` populate `absoluteAddressRelocations`. Real-toolchain testing found neither needs it: `rep_stos_global_clear` already reaches `differences: 0` via literal `_emit` bytes; `global_and_global_bool`'s literal-cast candidate was measurably worse with the evidence added. Neither was changed. +- R4. A regression test guards the *actual* anti-pattern this investigation found: a rule generator populating `absoluteAddressRelocations` while its own generated source only references that address via a literal pointer cast (confirmed to make matching worse, not better). +- R5. (Superseded.) Originally called for real-toolchain verification of at least three fixed rules. Real-toolchain verification did run — on `float_multiply_global`, `global_and_global_bool`, and `inc_abs_global` itself — but as A/B comparisons proving the fix should *not* be applied, not as confirmation of a successful fix. + +## Key Technical Decisions + +- **The premise was wrong; real-toolchain A/B testing found this during implementation, not planning.** `absoluteAddressRelocations` only helps when the candidate's own compiled object contains a matching relocation. Every one of the twelve rule generators in scope synthesizes a literal pointer cast, which MSVC compiles as a bare immediate — no relocation exists on the candidate side for the evidence to usefully mirror. Confirmed by A/B testing three representative cases against the real MSVC8/wine toolchain and real objdiff: + - `float_multiply_global`: unfixed best-profile histogram `{ARGUMENT_MISMATCH: 2, INSERTION: 14}`; with the evidence added, `{ARGUMENT_MISMATCH: 4, INSERTION: 14}` — worse. + - `global_and_global_bool`: unfixed `{ARGUMENT_MISMATCH: 2, INSERTION: 4}`; with the evidence added, `{ARGUMENT_MISMATCH: 6, INSERTION: 4}` — worse. + - `inc_abs_global` (PR #149's own shipped fix): identical histograms with and without the evidence on its own candidate — neutral. Its value is that the evidence field is now *available* on that `GeneratedCandidate` for a differently-constructed candidate for the same target (e.g. a subagent rewrite referencing a named `DAT_` symbol) to carry matching evidence of its own — demonstrated live in this session by combining a subagent's inline-asm rewrite (`inc dword ptr [DAT_00830540]`) with hand-attached relocation evidence, reaching `differences: 0`. + - `rep_stos_global_clear`'s only returned candidate embeds every address as literal `_emit` bytes, not a pointer dereference at all — a different reason the evidence doesn't apply, verified to already reach `differences: 0` with no change. +- **U2's ten-rule wiring and U3's `global_and_global_bool` wiring were implemented, real-toolchain-tested, found to regress matching, and reverted** (see git history on this branch: commits wiring the fix, followed by a revert commit and a documentation commit explaining why). The shared helper (U1) was kept — it's correct and reusable for any future rule that legitimately references an address through a named symbol. +- **No changes to the objdiff verification harness itself.** `run_objdiff` and `render_target_coff_for_candidate` are correct as-is; this investigation was about which candidates should populate evidence for them, not about the harness. + +## Scope Boundaries + +- Does not touch `run_objdiff`, `parse_objdiff_report`, or `render_target_coff_for_candidate` — confirmed correct. +- Does not attempt the separate, already-documented "remaining 64" candidate-generation-quality backlog item (Ghidra-synthesized type names, embedded `int3` artifacts) — a distinct class of bug. +- Does not run a proof-campaign or vacuum loop against `swkotor.exe` to chase new verified accepts. + +### Deferred to Follow-Up Work + +- **The real generalization opportunity this investigation surfaced**: relocation evidence should be inherited or computed automatically wherever a subagent-rewrite or packaged-source candidate is constructed for a near-miss target that references an absolute address — not attached per mechanical rule generator. This session's subagent-rewrite proof required hand-attaching the evidence to the rewrite candidate; whether `pending_rewrite_variant()` (or wherever mechanism-3 candidates are actually constructed in the production path) does this automatically was not verified and is out of scope here. +- Any additional rule generators beyond the twelve investigated here that may embed absolute addresses in less obvious forms — the regression test (U4) is a static-source-text scan and may not catch every future variant. + +## Implementation Units + +### U1. Shared single-address relocation-evidence helper + +**Goal:** Add a helper that produces the `absoluteAddressRelocations` list for the common single-address case, matching the shape `inc_abs_global` already emits. + +**Requirements:** R2 + +**Dependencies:** None + +**Files:** +- Modify: `src/agentdecompile_recovery/source_parity_synthesize.py` (add helper near `inc_abs_global`, e.g. adjacent to line 573) +- Test: `tests/test_relocation_evidence_helper.py` + +**Approach:** A small function taking `(offset: int, addr: int)` (or `(offset, addr, symbol_prefix)` if a rule needs a non-`DAT_` symbol convention) and returning the single-entry `absoluteAddressRelocations` list, mirroring the literal `inc_abs_global` currently constructs inline. `inc_abs_global` itself is not required to switch to the helper (it already works and is out of scope for behavior change), but new callers use it. + +**Patterns to follow:** `inc_abs_global` (`src/agentdecompile_recovery/source_parity_synthesize.py:573`) for the exact shape and the `_DAT_` symbol convention. + +**Test scenarios:** +- Happy path: helper called with a representative offset/address returns a one-entry list with `type: "IMAGE_REL_I386_DIR32"`, correct `offset`, `symbol` formatted as `_DAT_`, and `decodedAddress` formatted as `0x`. +- Edge case: address value with leading zero bytes (e.g. `0x00830540`) formats consistently with the existing `inc_abs_global` convention. + +**Verification:** Unit tests pass; output shape matches what `absolute_address_relocations()` (`:21725`) already reads without modification. + +--- + +### U2. Investigate the ten single-address rules (superseded — reverted) + +**Goal:** Originally: wire the U1 helper into each of the ten confirmed single-address rule generators. **Actual outcome:** implemented, real-toolchain-verified, found to regress matching for the tested representative (`float_multiply_global`), and reverted for all ten. + +**Requirements:** R1 (superseded) + +**Dependencies:** U1 + +**Files:** +- No net change to `src/agentdecompile_recovery/source_parity_synthesize.py` (wired, then reverted via `git revert`) +- `tests/test_rule_generator_relocation_evidence.py` was added, then removed by the revert + +**Finding:** All ten rules synthesize a raw literal pointer cast (`*(type *)0x{addr:08x}`), which MSVC compiles as a bare immediate with no relocation. Adding target-side `absoluteAddressRelocations` against a literal candidate does not help. A/B tested for `float_multiply_global` against the real MSVC8/wine toolchain: unfixed best-profile histogram `{ARGUMENT_MISMATCH: 2, INSERTION: 14}` vs. fixed `{ARGUMENT_MISMATCH: 4, INSERTION: 14}` — worse, not better. + +**Verification:** `git log` on this branch shows the wiring commit followed by a revert commit; the ten rules are unchanged from their pre-plan state. + +--- + +### U3. Investigate the two multi-address rules (superseded — one reverted, one never applicable) + +**Goal:** Originally: wire per-relocation evidence into `rep_stos_global_clear` and `global_and_global_bool`. **Actual outcome:** neither needed the fix. + +**Requirements:** R3 (superseded) + +**Dependencies:** None + +**Files:** +- No net change to `src/agentdecompile_recovery/source_parity_synthesize.py` +- `tests/test_multi_address_rule_relocation_evidence.py` documents the finding for both rules + +**Finding:** +- `rep_stos_global_clear`'s only returned candidate embeds every address as literal bytes via inline-asm `_emit` directives, not a pointer dereference — verified against the real toolchain to already reach `differences: 0` with no change. +- `global_and_global_bool` dereferences two addresses via literal casts, same as U2's rules. A/B tested: unfixed `{ARGUMENT_MISMATCH: 2, INSERTION: 4}` vs. fixed `{ARGUMENT_MISMATCH: 6, INSERTION: 4}` — worse. Wired, then reverted before commit. + +**Verification:** `tests/test_multi_address_rule_relocation_evidence.py` asserts the current (correct, unchanged) shape for `global_and_global_bool` — no `absoluteAddressRelocations` in its evidence. + +--- + +### U4. Regression test guarding the real anti-pattern (scope corrected) + +**Goal:** Originally: guard against a rule generator embedding an address with no relocation evidence. **Corrected goal** (per the U2/U3 findings): guard against a rule generator pairing `absoluteAddressRelocations` with a literal pointer cast — the actual anti-pattern this investigation found and fixed. + +**Requirements:** R4 + +**Dependencies:** U2, U3 (their findings are what U4 guards) + +**Files:** +- Test: `tests/test_no_unrelocated_absolute_addresses.py` + +**Approach:** An AST scan over `source_parity_synthesize.py` for rule-generator functions (matching the `(row, c_name, data)` parameter signature) whose body both sets `absoluteAddressRelocations` in evidence and contains a literal-cast pattern (`*(type *)0x{addr}`-shaped) referencing an address. `inc_abs_global` is exempted — its own literal-cast candidate is neutral (not harmful) per direct A/B testing, and the evidence field serves a different, later-constructed candidate rather than its own. + +**Patterns to follow:** None directly — a new kind of test for this codebase. + +**Test scenarios:** +- Happy path: the scan against the current, corrected state of `source_parity_synthesize.py` finds zero violations. +- Failure-path (regression guard): a synthetic in-test function shaped exactly like the mistake this test exists to prevent (literal cast + relocation evidence) is detected by the scan. + +**Verification:** Both tests pass; the synthetic-violation test confirms the check has teeth, not that it passes vacuously. + +## Dependencies / Assumptions + +- Assumes the real MSVC8/wine toolchain used to verify PR #149 (`VC_ROOT=/run/media/brunner56/MyBook/Toolchains/msvc8.0-main`, `WINEPREFIX=target/wine-smoke-prefix`) remains available for the R5 sample verification. +- Assumes no other rule generators beyond these twelve reference absolute addresses in a form the manual read missed (see Deferred to Follow-Up Work). From 0aabbce4e814032461f043c910e2e8160de08036 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 30 Jul 2026 04:32:58 -0500 Subject: [PATCH 07/10] fix(review): warn against misuse, broaden anti-pattern regex, add positive-path test Code review (ce-code-review) on PR #151 found and validated three issues: - single_absolute_address_relocation()'s docstring didn't warn against the exact misuse this PR's own investigation found and reverted (pairing the helper with a literal-cast candidate). Added an explicit negative-case warning. - LITERAL_CAST_RE only matched the *(TYPE *)0x{addr} idiom, missing two other literal-cast forms already present in the file (indexed-store and assign-then-deref) -- confirmed latent (neither currently pairs with absoluteAddressRelocations, so not a live miss today) but a real robustness gap. Broadened the regex to cover all three forms; reverified zero false positives against bink_buffer_set_direct_draw_forwarder. - The regression test's "allowed pattern" branch (a rule generator that legitimately uses absoluteAddressRelocations with a named symbol) had no dedicated positive-path test -- only incidental pass-through of the whole-file scan. Added a test that directly targets bink_buffer_set_direct_draw_forwarder as the positive-path exemplar. 693 tests pass (up from 692). --- .../source_parity_synthesize.py | 10 +++++++ .../test_no_unrelocated_absolute_addresses.py | 30 ++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/agentdecompile_recovery/source_parity_synthesize.py b/src/agentdecompile_recovery/source_parity_synthesize.py index 0f08dee..7197f2a 100755 --- a/src/agentdecompile_recovery/source_parity_synthesize.py +++ b/src/agentdecompile_recovery/source_parity_synthesize.py @@ -637,6 +637,16 @@ def single_absolute_address_relocation(offset: int, addr: int) -> list[dict[str, object renders the address as a raw byte blob instead of a symbol relocation, and a candidate referencing the same global through a compiler-visible symbol can never byte-match it. + + Do NOT call this for a candidate whose generated source only references + the address via a literal pointer cast (e.g. `*(unsigned int *)0x...`) -- + MSVC compiles a literal cast as a bare immediate with no relocation, so + there is nothing on the candidate side for this evidence to mirror. + Confirmed via real MSVC8/wine + objdiff A/B testing to make matching + measurably worse for literal-cast candidates (see + docs/plans/2026-07-30-001-fix-generalize-relocation-evidence-plan.md). + Only use this when the generated source references the address through a + named extern symbol, as bink_buffer_set_direct_draw_forwarder does. """ return [ diff --git a/tests/test_no_unrelocated_absolute_addresses.py b/tests/test_no_unrelocated_absolute_addresses.py index fa48301..4283990 100644 --- a/tests/test_no_unrelocated_absolute_addresses.py +++ b/tests/test_no_unrelocated_absolute_addresses.py @@ -35,6 +35,11 @@ import re from pathlib import Path +from agentdecompile_recovery.source_parity_synthesize import ( + BINK_BUFFER_SET_DIRECT_DRAW_FORWARDER, + bink_buffer_set_direct_draw_forwarder, +) + SOURCE_PATH = Path(__file__).resolve().parent.parent / "src" / "agentdecompile_recovery" / "source_parity_synthesize.py" # Rules exempt from this check: their own literal-cast candidate is @@ -42,7 +47,7 @@ # and the evidence exists to serve a different, later-constructed candidate. EXEMPT_RULE_FUNCTIONS = {"inc_abs_global"} -LITERAL_CAST_RE = re.compile(r"\*\s*\([^)]*\*\)\s*0x\{[a-zA-Z_]+") +LITERAL_CAST_RE = re.compile(r"\([^)]*\*\)\s*0x\{[a-zA-Z_]+") def _iter_rule_generator_functions(tree: ast.Module) -> list[ast.FunctionDef]: @@ -85,6 +90,29 @@ def test_no_rule_generator_pairs_relocation_evidence_with_a_literal_cast() -> No ) +def test_legitimate_named_symbol_usage_is_not_flagged() -> None: + """Positive-path case: bink_buffer_set_direct_draw_forwarder is the one + pre-existing rule that genuinely needs absoluteAddressRelocations -- it + references its addresses through named extern symbols, not a literal + cast. Deliberately targets this function (rather than relying on the + whole-file scan's incidental pass) so the check's "allowed pattern" + branch has its own coverage, not just the negative case. + """ + + candidates = bink_buffer_set_direct_draw_forwarder({}, "FUN_test", BINK_BUFFER_SET_DIRECT_DRAW_FORWARDER) + assert len(candidates) >= 1 + relocations = candidates[0].evidence.get("absoluteAddressRelocations") + assert relocations and len(relocations) >= 1 + + text = SOURCE_PATH.read_text(encoding="utf-8") + tree = ast.parse(text) + source_lines = text.splitlines() + functions = {node.name: node for node in _iter_rule_generator_functions(tree)} + body = _function_source(source_lines, functions["bink_buffer_set_direct_draw_forwarder"]) + assert "absoluteAddressRelocations" in body + assert LITERAL_CAST_RE.search(body) is None + + def test_check_actually_detects_the_anti_pattern() -> None: """Regression guard on the check itself: prove it fires on a synthetic function shaped exactly like the mistake this test exists to prevent. From 03d6f0e6745350aa127269b338991b708bea84d3 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 30 Jul 2026 04:51:20 -0500 Subject: [PATCH 08/10] fix(review): tighten positive-path test to exact relocation content Round-2 code review found test_legitimate_named_symbol_usage_is_not_flagged asserted only len(candidates) >= 1 and len(relocations) >= 1 -- loose enough that a regression dropping 5 of the real 6 relocation entries, or corrupting their offsets/symbols, would still pass. Validated directly: the real call returns 2 candidates with 6 relocation entries in the first. Tightened to exact equality on both counts and full relocation content. 693 tests pass. --- tests/test_no_unrelocated_absolute_addresses.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_no_unrelocated_absolute_addresses.py b/tests/test_no_unrelocated_absolute_addresses.py index 4283990..507b888 100644 --- a/tests/test_no_unrelocated_absolute_addresses.py +++ b/tests/test_no_unrelocated_absolute_addresses.py @@ -100,9 +100,16 @@ def test_legitimate_named_symbol_usage_is_not_flagged() -> None: """ candidates = bink_buffer_set_direct_draw_forwarder({}, "FUN_test", BINK_BUFFER_SET_DIRECT_DRAW_FORWARDER) - assert len(candidates) >= 1 + assert len(candidates) == 2 relocations = candidates[0].evidence.get("absoluteAddressRelocations") - assert relocations and len(relocations) >= 1 + assert relocations == [ + {"offset": 0x14, "type": "IMAGE_REL_I386_DIR32", "symbol": "_recovery_global_30068c6c", "decodedAddress": "0x30068c6c"}, + {"offset": 0x19, "type": "IMAGE_REL_I386_DIR32", "symbol": "_recovery_global_30068c70", "decodedAddress": "0x30068c70"}, + {"offset": 0x1F, "type": "IMAGE_REL_I386_DIR32", "symbol": "_recovery_global_30068c68", "decodedAddress": "0x30068c68"}, + {"offset": 0x36, "type": "IMAGE_REL_I386_DIR32", "symbol": "_recovery_global_30068c6c", "decodedAddress": "0x30068c6c"}, + {"offset": 0x3C, "type": "IMAGE_REL_I386_DIR32", "symbol": "_recovery_global_30068c70", "decodedAddress": "0x30068c70"}, + {"offset": 0x42, "type": "IMAGE_REL_I386_DIR32", "symbol": "_recovery_global_30068c68", "decodedAddress": "0x30068c68"}, + ] text = SOURCE_PATH.read_text(encoding="utf-8") tree = ast.parse(text) From 0184b2cbcf34480e8b8af046bc051f2d5e9aa358 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 30 Jul 2026 05:03:11 -0500 Subject: [PATCH 09/10] fix(review): parametrize anti-pattern detection test over all 3 broadened cast forms Round-3 code review found round 1's LITERAL_CAST_RE broadening (to catch indexed-store and assign-then-deref literal-cast idioms, not just the original dereferenced-cast form) had no test proving it actually catches those two forms -- only the original narrow synthetic fixture was tested. Validated directly: narrowing the regex back to its pre-round-1 form left every existing test passing. Parametrized test_check_actually_detects_the_anti_pattern over all three idioms. Confirmed the fix has teeth: temporarily narrowing the regex makes exactly the two new parametrized cases fail (indexed-store, assign-then-deref) while the original case still passes. 695 tests pass. --- .../test_no_unrelocated_absolute_addresses.py | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/test_no_unrelocated_absolute_addresses.py b/tests/test_no_unrelocated_absolute_addresses.py index 507b888..d365316 100644 --- a/tests/test_no_unrelocated_absolute_addresses.py +++ b/tests/test_no_unrelocated_absolute_addresses.py @@ -35,6 +35,8 @@ import re from pathlib import Path +import pytest + from agentdecompile_recovery.source_parity_synthesize import ( BINK_BUFFER_SET_DIRECT_DRAW_FORWARDER, bink_buffer_set_direct_draw_forwarder, @@ -120,16 +122,30 @@ def test_legitimate_named_symbol_usage_is_not_flagged() -> None: assert LITERAL_CAST_RE.search(body) is None -def test_check_actually_detects_the_anti_pattern() -> None: +_ANTI_PATTERN_SOURCE_LINES = { + "dereferenced-cast": ' source = f"*(unsigned int *)0x{addr:08x} = 1;"\n', + "indexed-store": ' source = f"((unsigned int *)0x{addr:08x})[index] = value;"\n', + "assign-then-deref": ( + ' source = f"unsigned int *slot = (unsigned int *)0x{addr:08x}; *slot = 1;"\n' + ), +} + + +@pytest.mark.parametrize("source_line", _ANTI_PATTERN_SOURCE_LINES.values(), ids=_ANTI_PATTERN_SOURCE_LINES.keys()) +def test_check_actually_detects_the_anti_pattern(source_line: str) -> None: """Regression guard on the check itself: prove it fires on a synthetic - function shaped exactly like the mistake this test exists to prevent. + function shaped exactly like the mistake this test exists to prevent -- + covering all three literal-cast idioms LITERAL_CAST_RE was broadened to + catch (dereferenced cast, indexed-store, assign-then-deref), not just the + first one. Without this, a future narrowing of the regex back to only the + first idiom would pass every test in this file silently. """ synthetic_source = ( "def fake_rule(row: dict[str, Any], c_name: str, data: bytes) -> list[GeneratedCandidate]:\n" " addr = u32(data[2:6])\n" - ' source = f"*(unsigned int *)0x{addr:08x} = 1;"\n' - " return [GeneratedCandidate(\n" + + source_line + + " return [GeneratedCandidate(\n" ' rule="fake", variant="fake", c_name=c_name, symbol=c_name,\n' " source=source, callconv=\"cdecl\", return_type=\"void\",\n" ' evidence={"absoluteAddressRelocations": [{"offset": 2}]},\n' From 489447d70e8f87db665759867bc095938f11f83d Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 30 Jul 2026 05:20:54 -0500 Subject: [PATCH 10/10] fix: remove unused .state.now import (pre-existing ruff F401) Pre-existing on master before PR #149/#151 -- blocks CI lint step (ruff check --no-fix) on both PR branches. Unrelated to the relocation- evidence work; fixing since it's a one-line, zero-risk removal blocking merge readiness. 601 unit tests pass. --- src/agentdecompile_recovery/source_parity_synthesize.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/agentdecompile_recovery/source_parity_synthesize.py b/src/agentdecompile_recovery/source_parity_synthesize.py index 7197f2a..960ff60 100755 --- a/src/agentdecompile_recovery/source_parity_synthesize.py +++ b/src/agentdecompile_recovery/source_parity_synthesize.py @@ -24,7 +24,6 @@ from typing import Any, Iterable from .package_verify import build_shim, compile_with_msvc -from .state import now ROOT = Path.cwd() DEFAULT_VC_ROOT: Path | None = None