diff --git a/.github/workflows/cpp-policy.yml b/.github/workflows/cpp-policy.yml new file mode 100644 index 0000000..ee4b18c --- /dev/null +++ b/.github/workflows/cpp-policy.yml @@ -0,0 +1,75 @@ +name: C++ evaluation contracts +on: + pull_request: + push: + branches: [main, 'codex/cpp-type-policy'] + workflow_dispatch: +permissions: + contents: read +jobs: + contracts: + runs-on: ubuntu-24.04 + timeout-minutes: 25 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: pip + - name: Native debug fixture compilers + run: sudo apt-get update && sudo apt-get install -y clang lld + - uses: dtolnay/rust-toolchain@stable + - name: Pinned existing CFG dependency + run: | + git clone https://github.com/Noelo-Lab/rust-joern.git ../rust-joern + git -C ../rust-joern checkout 9fd97f50885202596b125ebd2504e64910874bcb + cargo build --release --manifest-path ../rust-joern/Cargo.toml + python -m pip install ../rust-joern + test -f ../rust-joern/target/release/librust_joern.so + - name: Development dependencies + run: python -m pip install -e '.[dev]' + - name: Evaluation and provenance regressions + env: + DECBENCH_NO_CACHE: '1' + RUST_JOERN_LIBRARY: ${{ github.workspace }}/../rust-joern/target/release/librust_joern.so + run: | + python -m pytest --no-cov tests/test_cpp_first_pc_locations.py tests/test_cpp_native_inputs.py tests/test_cpp_toolchain_probe.py tests/test_producer_validation.py tests/test_cpp_type_graph.py tests/test_cpp_review_contracts.py \ + tests/test_cpp_ged_identity.py tests/test_cpp_ged_compilation_units.py \ + tests/test_recompiler_arch.py tests/test_byte_match_fixup_policy.py \ + tests/test_metric_function_identity.py \ + tests/test_cpp_support.py tests/test_cpp_identity_backends.py \ + tests/test_cpp_identity_deep_validation.py tests/test_cpp_identity_end_to_end.py \ + tests/test_cpp_identity_optimized_dwarf.py tests/test_cpp_identity_progress.py \ + tests/test_cpp_identity_thumb.py tests/test_function_identity.py \ + tests/test_cpp_identity_icf.py tests/test_cpp_identity_slice_invariance.py \ + tests/test_type_match_correspondence.py tests/test_provenance_sanitizer.py \ + tests/test_raw_line_provenance.py + - name: New policy code style + run: | + black --check tests/test_cpp_first_pc_locations.py + ruff check tests/test_cpp_first_pc_locations.py + black --check scripts/audit_cpp_toolchains.py tests/test_cpp_native_inputs.py tests/test_cpp_toolchain_probe.py decbench/models/decompilation.py decbench/decompilers/raw/angr_raw.py decbench/decompilers/raw/ghidra_raw.py decbench/decompilers/raw/kuna_raw.py decbench/utils/producer_validation.py tests/test_producer_validation.py decbench/utils/cfg_identity.py decbench/pipeline/cpp_ged.py \ + scripts/evaluate_cpp_ged.py tests/test_cpp_ged_identity.py \ + decbench/utils/compilation_units.py tests/test_cpp_ged_compilation_units.py \ + decbench/metrics/variable_features.py \ + decbench/utils/binfmt.py decbench/metrics/byte_match.py tests/test_recompiler_arch.py \ + decbench/metrics/cpp_type_graph.py decbench/metrics/cpp_recovered_types.py \ + decbench/metrics/cpp_types.py decbench/metrics/cpp_type_match.py \ + decbench/utils/pdb_info.py decbench/decompilers/raw/fission_raw.py \ + tests/test_cpp_type_graph.py tests/test_cpp_review_contracts.py + ruff check scripts/audit_cpp_toolchains.py tests/test_cpp_native_inputs.py tests/test_cpp_toolchain_probe.py decbench/models/decompilation.py decbench/decompilers/raw/angr_raw.py decbench/decompilers/raw/ghidra_raw.py decbench/decompilers/raw/kuna_raw.py decbench/utils/producer_validation.py tests/test_producer_validation.py decbench/utils/cfg_identity.py decbench/pipeline/cpp_ged.py \ + scripts/evaluate_cpp_ged.py tests/test_cpp_ged_identity.py \ + decbench/utils/compilation_units.py tests/test_cpp_ged_compilation_units.py \ + decbench/metrics/variable_features.py \ + decbench/utils/binfmt.py decbench/metrics/byte_match.py tests/test_recompiler_arch.py \ + decbench/metrics/cpp_type_graph.py decbench/metrics/cpp_recovered_types.py \ + decbench/metrics/cpp_types.py decbench/metrics/cpp_type_match.py \ + decbench/utils/pdb_info.py decbench/decompilers/raw/fission_raw.py \ + tests/test_cpp_type_graph.py tests/test_cpp_review_contracts.py + - name: New policy type contracts + run: | + mypy --follow-imports=silent scripts/audit_cpp_toolchains.py decbench/models/decompilation.py decbench/utils/producer_validation.py decbench/utils/cfg_identity.py \ + decbench/utils/compilation_units.py \ + decbench/pipeline/cpp_ged.py decbench/metrics/cpp_type_graph.py \ + decbench/metrics/cpp_recovered_types.py decbench/metrics/cpp_types.py \ + decbench/metrics/cpp_type_match.py diff --git a/decbench/decompilers/external.py b/decbench/decompilers/external.py index e25a5c3..cfc5cd3 100644 --- a/decbench/decompilers/external.py +++ b/decbench/decompilers/external.py @@ -82,12 +82,6 @@ def decompile_binary( EXTERNAL_DECOMPILERS: tuple[ExternalSpec, ...] = ( - ExternalSpec( - id="fission", - display_name="Fission", - url="https://github.com/fission-systems/Fission", - version="0.2.3", - ), ExternalSpec( id="ventris", display_name="Ventris", diff --git a/decbench/decompilers/provenance.py b/decbench/decompilers/provenance.py index 850eb75..1fabd85 100644 --- a/decbench/decompilers/provenance.py +++ b/decbench/decompilers/provenance.py @@ -115,6 +115,9 @@ def _hydrate_additive_provenance( ) -> None: """Hydrate additive fields absent from legacy pickled model instances.""" for mapping in function.line_mappings: + if not hasattr(mapping, "defined_variables"): + mapping.defined_variables = None + report.increment("legacy_additive_fields_hydrated") if not isinstance(getattr(mapping, "addresses", None), list): mapping.addresses = [] report.increment("legacy_additive_fields_hydrated") @@ -179,6 +182,13 @@ def _sanitize_function( sanitized_mappings = [] report.increment("line_mapping_rows_seen", len(function.line_mappings)) for mapping in function.line_mappings: + if mapping.defined_variables is not None and not 1 <= mapping.line_number <= len( + function.decompiled_code.splitlines() + ): + modified = True + report.increment("line_mapping_rows_dropped") + report.record_address_drop("line_outside_output") + continue addresses = _filter_addresses( mapping.addresses, code=code, diff --git a/decbench/decompilers/raw/__init__.py b/decbench/decompilers/raw/__init__.py index 623f847..536b587 100644 --- a/decbench/decompilers/raw/__init__.py +++ b/decbench/decompilers/raw/__init__.py @@ -19,6 +19,7 @@ angr_raw, # noqa: F401 binja_raw, # noqa: F401 dewolf_raw, # noqa: F401 + fission_raw, # noqa: F401 ghidra_raw, # noqa: F401 glaurung_raw, # noqa: F401 ida_raw, # noqa: F401 @@ -35,4 +36,5 @@ "dewolf_raw", "manifold_raw", "glaurung_raw", + "fission_raw", ] diff --git a/decbench/decompilers/raw/angr_raw.py b/decbench/decompilers/raw/angr_raw.py index e43f86c..6de1013 100644 --- a/decbench/decompilers/raw/angr_raw.py +++ b/decbench/decompilers/raw/angr_raw.py @@ -30,8 +30,10 @@ DecompilerMetadata, FunctionDecompilation, LineMapping, + ProducerLanguageContract, VariableInfo, ) +from decbench.utils.function_identity import insert_function _l = logging.getLogger(__name__) @@ -141,7 +143,7 @@ def _dump() -> None: _l.debug("angr-raw: failed to decompile %s: %s", func_name, e) if func_result is not None: - decompiled_functions[func_name] = func_result + insert_function(decompiled_functions, func_result) else: failed_functions.append(func_name) _dump() @@ -264,6 +266,9 @@ def _decompile_one( function_name=func_name, ) metadata = common.extract_metrics(code) + metadata["output_contract"] = ProducerLanguageContract( + surface="c-like-pseudocode", declarations="partial", dialect="angr-c" + ).model_dump() return FunctionDecompilation( name=func_name, diff --git a/decbench/decompilers/raw/binja_raw.py b/decbench/decompilers/raw/binja_raw.py index 23bed1e..27966ac 100644 --- a/decbench/decompilers/raw/binja_raw.py +++ b/decbench/decompilers/raw/binja_raw.py @@ -35,6 +35,7 @@ LineMapping, VariableInfo, ) +from decbench.utils.function_identity import insert_function _l = logging.getLogger(__name__) @@ -149,8 +150,8 @@ def _dump() -> None: bv = self._load(binary_path) enumerated = self._enumerate(bv, elf_base, text_range, addr_targets) if functions is not None: - requested = {n for (n, _a) in functions} - enumerated = [(n, a) for (n, a) in enumerated if n in requested] + requested_addrs = {a for (_n, a) in functions} + enumerated = [(n, a) for (n, a) in enumerated if a in requested_addrs] enumerated = common.narrow_to_source( enumerated, function_names, @@ -170,7 +171,7 @@ def _dump() -> None: except Exception as e: # noqa: BLE001 _l.debug("binja-raw: failed to decompile %s: %s", func_name, e) if func_result is not None: - decompiled_functions[func_name] = func_result + insert_function(decompiled_functions, func_result) else: failed_functions.append(func_name) _dump() diff --git a/decbench/decompilers/raw/common.py b/decbench/decompilers/raw/common.py index 30f2201..ed00935 100644 --- a/decbench/decompilers/raw/common.py +++ b/decbench/decompilers/raw/common.py @@ -121,6 +121,12 @@ def elf_min_vaddr(binary_path: Path) -> int: if base is not None: return base return 0 + from decbench.utils import binfmt + + info = binfmt.detect(binary_path) + if info is not None and info.fmt == "macho": + base = binfmt.mach_o_image_base(binary_path) + return base if base is not None else 0 with open(binary_path, "rb") as f: elf = ELFFile(f) vaddrs = [seg["p_vaddr"] for seg in elf.iter_segments() if seg["p_type"] == "PT_LOAD"] diff --git a/decbench/decompilers/raw/dewolf_raw.py b/decbench/decompilers/raw/dewolf_raw.py index 66eabed..0154a2f 100644 --- a/decbench/decompilers/raw/dewolf_raw.py +++ b/decbench/decompilers/raw/dewolf_raw.py @@ -61,6 +61,7 @@ FunctionDecompilation, VariableInfo, ) +from decbench.utils.function_identity import insert_function _l = logging.getLogger(__name__) @@ -291,7 +292,7 @@ def _consume(addrs_arg: str) -> None: metadata=common.extract_metrics(code), ) with lock: - decompiled_functions[name] = fd + insert_function(decompiled_functions, fd) _dump() elif kind == "fail": with lock: diff --git a/decbench/decompilers/raw/fission_raw.py b/decbench/decompilers/raw/fission_raw.py new file mode 100644 index 0000000..a6e196f --- /dev/null +++ b/decbench/decompilers/raw/fission_raw.py @@ -0,0 +1,216 @@ +"""Fission CLI transport. Preserve direct SSA PCs; never invent line maps.""" + +from __future__ import annotations + +import contextlib +import json +import os +import shutil +import signal +import subprocess +import tempfile +import time +from collections import Counter +from pathlib import Path +from typing import Any + +from decbench.decompilers.base import Decompiler +from decbench.decompilers.provenance import sanitize_native_provenance +from decbench.decompilers.raw import common +from decbench.decompilers.registry import register_decompiler +from decbench.models.decompilation import ( + DecompilationResult, + DecompilerMetadata, + FunctionDecompilation, + LineMapping, + ProducerLanguageContract, + VariableInfo, +) + + +@register_decompiler("fission") +class RawFissionDecompiler(Decompiler): + name = "fission" + display_name = "Fission" + + @staticmethod + def _binary() -> str | None: + value = os.environ.get("FISSION_BIN") + executable = value if value and Path(value).is_file() else shutil.which("fission_cli") + return str(Path(executable).resolve()) if executable else None + + def is_available(self) -> bool: + return self._binary() is not None + + def get_version(self) -> str | None: + executable = self._binary() + if not executable: + return None + result = subprocess.run( + [executable, "--version"], capture_output=True, text=True, timeout=30, check=False + ) + return result.stdout.strip() or None + + @staticmethod + def _build_function( + record: dict[str, Any], name: str, address: int + ) -> FunctionDecompilation | None: + if record.get("fell_back") or record.get("error"): + return None + code = record.get("code_nir") or record.get("code") + if not isinstance(code, str) or not code.strip(): + return None + variables = [VariableInfo.model_validate(v) for v in record.get("variables", [])] + mappings = [LineMapping.model_validate(m) for m in record.get("line_mappings_nir", [])] + name_counts = Counter(variable.name for variable in variables) + for variable in variables: + variable.line_numbers = sorted( + { + m.line_number + for m in mappings + if variable.name + and name_counts[variable.name] == 1 + and m.defined_variables is not None + and variable.name in m.defined_variables + } + ) + return FunctionDecompilation( + name=name, + address=address, + decompiled_code=code, + line_count=len(code.splitlines()), + variables=variables, + line_mappings=mappings, + metadata={ + **common.extract_metrics(code), + "provenance_source": "fission_scalar_ssa", + "provenance_granularity": "variable_def_use_and_scoped_definition_lines", + "code_layer": "nir", + "output_contract": ProducerLanguageContract( + surface="c-like-pseudocode", declarations="partial", dialect="fission-nir-c" + ).model_dump(), + }, + ) + + def decompile_binary( + self, + binary_path: Path, + functions: list[tuple[str, int]] | None = None, + output_dir: Path | None = None, + function_names: set[str] | None = None, + progress_path: Path | None = None, + ) -> DecompilationResult: + executable = self._binary() + if not executable: + raise RuntimeError("Fission CLI unavailable; set FISSION_BIN") + if self.config.extra_options: + raise ValueError( + "Fission options use FISSION_RESOURCE_ROOT / FISSION_WORKING_DIR; " + "unknown extra_options" + ) + binary_path = binary_path.resolve() + start = time.monotonic() + selected = {int(address): name for name, address in functions or []} + requested = set(selected) | common.addr_targets_of(function_names) + result = DecompilationResult( + binary_path=binary_path, + binary_name=binary_path.stem, + output_dir=output_dir, + decompiler=DecompilerMetadata( + decompiler_name=self.id, + decompiler_version=self.get_version(), + extra={"backend": "fission", "via": "raw"}, + ), + ) + with tempfile.TemporaryDirectory(prefix="decbench-fission-") as directory: + command = [executable] + if root := os.environ.get("FISSION_RESOURCE_ROOT"): + command += ["--resource-root", root] + command += [ + "decomp", + str(binary_path), + "--json", + "--no-db", + "--layer", + "nir", + "--no-header", + "--no-warnings", + "--timeout-ms", + str(int(self.config.function_timeout_seconds * 1000)), + ] + if functions is not None and not functions: + return result + if requested: + address_file = Path(directory) / "addresses.txt" + address_file.write_text("".join(f"0x{a:x}\n" for a in sorted(requested))) + command += ["--addresses-file", str(address_file)] + else: + command += ["--all"] + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=os.environ.get("FISSION_WORKING_DIR"), + start_new_session=True, + ) + try: + stdout, stderr = process.communicate(timeout=self.config.binary_timeout_seconds) + except subprocess.TimeoutExpired: + result.decompiler.timeout_occurred = True + result.decompiler.extra["error"] = "binary timeout; no completed JSON payload" + stdout = "" + finally: + if process.poll() is None: + with contextlib.suppress(ProcessLookupError): + os.killpg(process.pid, signal.SIGKILL) + process.wait(timeout=15) + if stdout: + payload = json.loads(stdout) + records = ( + payload.get("functions", [payload]) if isinstance(payload, dict) else payload + ) + seen = set() + for record in records: + raw = record.get("address") + address = int(raw, 0) if isinstance(raw, str) else int(raw) + if address in seen: + raise ValueError("ambiguous duplicate Fission function address") + seen.add(address) + if requested and address not in requested: + continue + name = selected.get(address, str(record.get("name") or f"sub_{address:x}")) + function = self._build_function(record, name, address) + if function is None: + key = result.add_function( + FunctionDecompilation(name=name, address=address, decompiled_code="") + ) + result.decompiler.failed_functions.append(key) + else: + result.add_function(function) + if process.returncode: + result.decompiler.extra["process_exit_code"] = process.returncode + result.decompiler.extra["stderr_tail"] = stderr[-1000:] + returned = {f.address for f in result.functions.values()} + for address in sorted(requested - returned): + key = result.add_function( + FunctionDecompilation( + name=selected.get(address, f"sub_{address:x}"), + address=address, + decompiled_code="", + ) + ) + result.decompiler.failed_functions.append(key) + observed = {f.address for f in result.functions.values() if f.decompiled_code.strip()} + result.decompiler.extra["requested_addresses"] = sorted(requested) + result.decompiler.extra["missing_requested_addresses"] = sorted(requested - observed) + result.decompiler.total_time_seconds = time.monotonic() - start + # Worker inputs are isolated/stripped: defer until the evaluator owns + # exact debug ranges. Final metrics still validate strictly. + sanitize_native_provenance(result, defer_unavailable=True) + if progress_path: + common.dump_progress(progress_path, result) + if output_dir: + output_dir.mkdir(parents=True, exist_ok=True) + result.to_c_file(output_dir / f"fission_{binary_path.stem}.c") + return result diff --git a/decbench/decompilers/raw/ghidra_raw.py b/decbench/decompilers/raw/ghidra_raw.py index f3d4ddf..b77b5fa 100644 --- a/decbench/decompilers/raw/ghidra_raw.py +++ b/decbench/decompilers/raw/ghidra_raw.py @@ -42,8 +42,10 @@ DecompilerMetadata, FunctionDecompilation, LineMapping, + ProducerLanguageContract, VariableInfo, ) +from decbench.utils.function_identity import insert_function _l = logging.getLogger(__name__) @@ -264,11 +266,11 @@ def _dump() -> None: program = flat.getCurrentProgram() image_base = int(program.getImageBase().getOffset()) - requested = {n for (n, _a) in functions} if functions is not None else None + requested_addrs = {a for (_n, a) in functions} if functions is not None else None enumerated = self._enumerate(program, elf_base, text_range, addr_targets) - if requested is not None: - enumerated = [(n, a) for (n, a) in enumerated if n in requested] + if requested_addrs is not None: + enumerated = [(n, a) for (n, a) in enumerated if a in requested_addrs] enumerated = common.narrow_to_source( enumerated, @@ -281,11 +283,11 @@ def _dump() -> None: ifc.openProgram(program) monitor = ConsoleTaskMonitor() timeout_s = int(self.config.function_timeout_seconds) - by_name = self._functions_by_name(program) + by_address = self._functions_by_address(program, elf_base) for func_name, file_addr in enumerated: func_result = None - g_func = by_name.get(func_name) + g_func = by_address.get(file_addr) if g_func is not None: try: func_result = self._decompile_one( @@ -305,7 +307,7 @@ def _dump() -> None: e, ) if func_result is not None: - decompiled_functions[func_name] = func_result + insert_function(decompiled_functions, func_result) else: failed_functions.append(func_name) _dump() @@ -370,12 +372,15 @@ def _enumerate( return sorted(out, key=lambda x: x[1]) @staticmethod - def _functions_by_name(program: Any) -> dict[str, Any]: - """Map (last-seen) function name -> Ghidra Function object.""" - out: dict[str, Any] = {} + def _functions_by_address(program: Any, elf_base: int) -> dict[int, Any]: + """Map binary-local function address -> Ghidra Function object.""" + image_base = int(program.getImageBase().getOffset()) + out: dict[int, Any] = {} fm = program.getFunctionManager() for g_func in fm.getFunctions(True): - out[g_func.getName()] = g_func + offset = int(g_func.getEntryPoint().getOffset()) + file_addr = (offset - image_base) + elf_base + out[file_addr] = g_func return out def _decompile_one( @@ -422,6 +427,9 @@ def _decompile_one( {address for line in lines for address in line_addresses.get(line, set())} ) metadata = common.extract_metrics(code) + metadata["output_contract"] = ProducerLanguageContract( + surface="c-like-pseudocode", declarations="partial", dialect="ghidra-c" + ).model_dump() return FunctionDecompilation( name=func_name, diff --git a/decbench/decompilers/raw/ida_raw.py b/decbench/decompilers/raw/ida_raw.py index f411141..d6dd1d3 100644 --- a/decbench/decompilers/raw/ida_raw.py +++ b/decbench/decompilers/raw/ida_raw.py @@ -34,6 +34,7 @@ LineMapping, VariableInfo, ) +from decbench.utils.function_identity import insert_function _l = logging.getLogger(__name__) @@ -158,8 +159,12 @@ def _dump() -> None: try: enumerated = self._enumerate(elf_base, text_range, addr_targets) if functions is not None: - requested = {n for (n, _a) in functions} - enumerated = [(n, a) for (n, a) in enumerated if n in requested] + requested_addrs = {address for (_name, address) in functions} + enumerated = [ + (name, address) + for name, address in enumerated + if address in requested_addrs + ] enumerated = common.narrow_to_source( enumerated, function_names, @@ -173,7 +178,7 @@ def _dump() -> None: except Exception as e: # noqa: BLE001 _l.debug("ida-raw: failed to decompile %s: %s", func_name, e) if func_result is not None: - decompiled_functions[func_name] = func_result + insert_function(decompiled_functions, func_result) else: failed_functions.append(func_name) _dump() diff --git a/decbench/decompilers/raw/kuna_raw.py b/decbench/decompilers/raw/kuna_raw.py index 3ca0f62..8b8c099 100644 --- a/decbench/decompilers/raw/kuna_raw.py +++ b/decbench/decompilers/raw/kuna_raw.py @@ -56,8 +56,10 @@ DecompilationResult, DecompilerMetadata, FunctionDecompilation, + ProducerLanguageContract, VariableInfo, ) +from decbench.utils.function_identity import insert_function _l = logging.getLogger(__name__) @@ -158,20 +160,22 @@ def _dump() -> None: _l.error("kuna-raw failed on %s: %s", binary_path, e) return self._error_result(binary_path, start, str(e)) - records = {str(r.get("name") or ""): r for r in self._records(payload)} + records_by_address = self._records_by_address(payload) enumerated = sorted( ( - (n, int(r.get("address") or 0)) - for n, r in records.items() + (str(record.get("name") or ""), address) + for address, record in records_by_address.items() if not common.should_skip_function( - n, int(r.get("address") or 0), text_range, addr_targets + str(record.get("name") or ""), address, text_range, addr_targets ) ), key=lambda x: x[1], ) if functions is not None: - requested = {n for (n, _a) in functions} - enumerated = [(n, a) for (n, a) in enumerated if n in requested] + requested_addrs = {address for (_name, address) in functions} + enumerated = [ + (name, address) for name, address in enumerated if address in requested_addrs + ] enumerated = common.narrow_to_source( enumerated, function_names, backend="kuna", binary_name=binary_path.name ) @@ -179,11 +183,11 @@ def _dump() -> None: for func_name, file_addr in enumerated: fd = None try: - fd = self._build_function(records[func_name], func_name, file_addr) + fd = self._build_function(records_by_address[file_addr], func_name, file_addr) except Exception as e: # noqa: BLE001 _l.debug("kuna-raw: assembling %s failed: %s", func_name, e) if fd is not None: - decompiled[func_name] = fd + insert_function(decompiled, fd) else: failed.append(func_name) _dump() @@ -285,6 +289,21 @@ def _records(payload: Any) -> list[dict[str, Any]]: return list(payload.get("functions") or []) return payload if isinstance(payload, list) else [] + @classmethod + def _records_by_address(cls, payload: Any) -> dict[int, dict[str, Any]]: + """Index kuna records by binary-local address without collapsing names.""" + out: dict[int, dict[str, Any]] = {} + for record in cls._records(payload): + value = record.get("address") + if value is None: + continue + try: + address = int(value) + except (TypeError, ValueError): + continue + out[address] = record + return out + def _build_function( self, rec: dict[str, Any], name: str, file_addr: int ) -> FunctionDecompilation | None: @@ -327,7 +346,12 @@ def _evidence_address(value: Any) -> int | None: line_count=line_count, line_mappings=line_mappings, variables=self._variables(rec, line_count, _evidence_address), - metadata=common.extract_metrics(code), + metadata={ + **common.extract_metrics(code), + "output_contract": ProducerLanguageContract( + surface="c-like-pseudocode", declarations="partial", dialect="kuna-c" + ).model_dump(), + }, ) @staticmethod diff --git a/decbench/evalkit/ingest.py b/decbench/evalkit/ingest.py index 7723e96..1534e2c 100644 --- a/decbench/evalkit/ingest.py +++ b/decbench/evalkit/ingest.py @@ -361,6 +361,7 @@ def _build_entry( stems = resolve.source_stems(tree, opt, project) lookup = resolve.AddrLookup.for_binary(binary, stems=stems or None) name2addr = resolve.name_to_addr(binary, names=allowed, stems=stems or None) + allowed_by_addr = {address: storage_key for storage_key, address in name2addr.items()} snippets = split_c_functions(c_text) kept: dict[str, FunctionDecompilation] = {} @@ -373,8 +374,18 @@ def _build_entry( ) counters.dropped_extra += 1 continue - dwarf_name = lookup.name_for(addr) - if dwarf_name is None or dwarf_name not in allowed: + resolved = lookup.resolve(addr) + if resolved is None: + _warn( + warnings, + f"{where}: {sub_name} @ 0x{addr:x} is not a manifest function of this " + f"slice — dropped", + ) + counters.dropped_extra += 1 + continue + low_pc, dwarf_name = resolved + storage_key = allowed_by_addr.get(low_pc) + if storage_key is None: _warn( warnings, f"{where}: {sub_name} @ 0x{addr:x} is not a manifest function of this " @@ -393,17 +404,16 @@ def _build_entry( if dwarf_name != sub_name: code = re.sub(r"\b" + re.escape(sub_name) + r"\b", dwarf_name, code) counters.relabeled += 1 - low_pc = name2addr.get(dwarf_name, addr) - prev = kept.get(dwarf_name) + prev = kept.get(storage_key) if prev is not None: _warn( warnings, f"{where}: {sub_name} and another submitted function both relabel to " - f"{dwarf_name} — keeping the larger body", + f"{storage_key} — keeping the larger body", ) if len(code) < len(prev.decompiled_code or ""): continue - kept[dwarf_name] = FunctionDecompilation( + kept[storage_key] = FunctionDecompilation( name=dwarf_name, address=low_pc, decompiled_code=code, diff --git a/decbench/evalkit/resolve.py b/decbench/evalkit/resolve.py index e5ff58c..01d4e31 100644 --- a/decbench/evalkit/resolve.py +++ b/decbench/evalkit/resolve.py @@ -27,6 +27,7 @@ from pathlib import Path from decbench.utils.dwarf_policy import dwarf_follow_abstract_origin +from decbench.utils.function_identity import parse_function_storage_key from decbench.utils.langs import PREPROC_EXTS, build_stem_index, strip_source_ext _l = logging.getLogger(__name__) @@ -69,6 +70,20 @@ def _dwarf_addr_to_name(binary: Path, stems: set[str] | None) -> dict[int, str]: _l.warning("DWARF read failed for %s: %s", binary, e) return {} if dw is None: + native = binfmt.pdb_info(binary) + if native is not None: + if stems is not None: + return { + address: name + for address, (name, _stem) in binfmt.source_function_owners( + binary, stems + ).items() + } + return { + f.address: f.name + for f in native.functions + if native.at_address(f.address) is not None + } return {} addr2name: dict[int, str] = {} file_tables: dict[int, list[str | None]] = {} @@ -81,6 +96,9 @@ def _dwarf_addr_to_name(binary: Path, stems: set[str] | None) -> dict[int, str]: name = binfmt.die_str_attr(die, "DW_AT_name", follow_abstract_origin=follow) if name is None: continue + address = int(die.attributes["DW_AT_low_pc"].value) + if not binfmt.dwarf_low_pc_is_concrete(binary, address): + continue if stems is not None: fi, owner = binfmt.die_attr_owner( die, "DW_AT_decl_file", follow_abstract_origin=follow @@ -95,7 +113,7 @@ def _dwarf_addr_to_name(binary: Path, stems: set[str] | None) -> dict[int, str]: s.endswith("-" + stem) or s.endswith("_" + stem) for s in stem_index ): continue - addr2name[int(die.attributes["DW_AT_low_pc"].value)] = name + addr2name[address] = name except Exception as e: # noqa: BLE001 _l.warning("DWARF walk failed for %s: %s", binary, e) return {} @@ -117,19 +135,28 @@ def name_to_addr( addr2name = _dwarf_addr_to_name(Path(binary), stems) by_name: dict[str, list[int]] = {} for addr, nm in addr2name.items(): - if names is not None and nm not in names: - continue by_name.setdefault(nm, []).append(addr) + out: dict[str, int] = {} - for nm, addrs in by_name.items(): + requested = sorted(names) if names is not None else sorted(by_name) + for storage_key in requested: + semantic_name, keyed_address = parse_function_storage_key(storage_key) + if keyed_address is not None: + if addr2name.get(keyed_address) == semantic_name: + out[storage_key] = keyed_address + continue + + addrs = by_name.get(semantic_name, []) + if not addrs: + continue if len(addrs) > 1: _l.warning( "%s: name %r is ambiguous in DWARF (%s); keeping the lowest address", Path(binary).name, - nm, + semantic_name, ", ".join(hex(a) for a in sorted(addrs)), ) - out[nm] = min(addrs) + out[storage_key] = min(addrs) return out @@ -154,15 +181,23 @@ def for_binary(cls, binary: Path, stems: set[str] | None = None) -> AddrLookup: binary = Path(binary) return cls(_dwarf_addr_to_name(binary, stems), common.elf_min_vaddr(binary)) + def resolve(self, addr: int) -> tuple[int, str] | None: + """Return the canonical ``(low_pc, name)`` for a reported address.""" + a = int(addr) + seen: set[int] = set() + for candidate in (a, a & ~1, a + self.min_vaddr, (a + self.min_vaddr) & ~1): + if candidate in seen: + continue + seen.add(candidate) + name = self._map.get(candidate) + if name is not None: + return candidate, name + return None + def name_for(self, addr: int) -> str | None: """The DWARF name at ``addr``, or None when no tolerance rule matches.""" - a = int(addr) - return ( - self._map.get(a) - or self._map.get(a & ~1) - or self._map.get(a + self.min_vaddr) - or self._map.get((a + self.min_vaddr) & ~1) - ) + resolved = self.resolve(addr) + return resolved[1] if resolved is not None else None def _is_linked_elf(path: Path) -> bool: diff --git a/decbench/metrics/base.py b/decbench/metrics/base.py index a18ca21..da8b8ae 100644 --- a/decbench/metrics/base.py +++ b/decbench/metrics/base.py @@ -3,6 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +from collections import Counter from collections.abc import Callable from typing import TYPE_CHECKING, Any @@ -14,6 +15,7 @@ MetricResult, MetricValue, ) +from decbench.utils.function_identity import parse_function_storage_key if TYPE_CHECKING: from networkx import DiGraph @@ -95,6 +97,31 @@ def compute_for_function( **kwargs: Any, ) -> MetricValue: ... + @staticmethod + def _cfg_for_function( + cfgs: dict[str, DiGraph], + storage_key: str, + function_name: str, + name_count: int, + ) -> DiGraph | None: + """Resolve a CFG without conflating same-name C++ functions. + + Collision-safe CFG producers can key graphs by the decompilation result's + storage key (for example ``foo@0x401000``), which always wins. The + historical plain-name fallback remains valid only when the function name + is unique within this binary. For an overloaded/same-name group, a lone + ``cfgs['foo']`` is ambiguous and must not be applied to every function. + """ + exact = cfgs.get(storage_key) + if exact is not None: + return exact + _semantic_name, keyed_address = parse_function_storage_key(storage_key) + if keyed_address is not None: + return None + if name_count == 1: + return cfgs.get(function_name) + return None + def compute_for_binary( self, decompilation: DecompilationResult, @@ -112,11 +139,23 @@ def compute_for_binary( source_cfgs = source_cfgs or {} decompiled_cfgs = decompiled_cfgs or {} + name_counts = Counter(function.name for function in decompilation.functions.values()) - for func_name, func_decomp in decompilation.functions.items(): + for storage_key, func_decomp in decompilation.functions.items(): try: - source_cfg = source_cfgs.get(func_name) - decompiled_cfg = decompiled_cfgs.get(func_name) + count = name_counts[func_decomp.name] + source_cfg = self._cfg_for_function( + source_cfgs, + storage_key, + func_decomp.name, + count, + ) + decompiled_cfg = self._cfg_for_function( + decompiled_cfgs, + storage_key, + func_decomp.name, + count, + ) if self.requires_source_cfg and source_cfg is None: continue @@ -133,10 +172,10 @@ def compute_for_binary( # this metric's denominator uniformly instead of counting as a failure. if not math.isfinite(value.value): continue - function_results[func_name] = value + function_results[storage_key] = value except Exception as e: - errors.append(f"{func_name}: {str(e)}") + errors.append(f"{storage_key}: {str(e)}") result = MetricResult( metric_name=self.name, diff --git a/decbench/metrics/byte_match.py b/decbench/metrics/byte_match.py index 9110ffe..b9f628a 100644 --- a/decbench/metrics/byte_match.py +++ b/decbench/metrics/byte_match.py @@ -10,17 +10,20 @@ import logging import re +from collections import Counter from pathlib import Path from typing import TYPE_CHECKING, Any from decbench.metrics.base import Metric, MetricConfig from decbench.metrics.registry import register_metric from decbench.models.metrics import AggregationType, MetricResult, MetricValue +from decbench.utils.function_identity import parse_function_storage_key if TYPE_CHECKING: from networkx import DiGraph from decbench.models.decompilation import DecompilationResult, FunctionDecompilation + from decbench.utils.binfmt import BinInfo logger = logging.getLogger(__name__) @@ -230,7 +233,7 @@ class ByteMatchMetric(Metric): requires_source_cfg = False requires_decompiled_cfg = False - cache_version = "7" + cache_version = "9" def __init__(self, config: MetricConfig | None = None): super().__init__(config) @@ -271,7 +274,7 @@ def compute_for_function( info = binfmt.detect(original_binary_path) if info is None: return MetricValue(value=0.0, metadata={"error": "Unrecognized binary format"}) - recompiler = binfmt.recompiler_for(info) + recompiler = binfmt.recompiler_for(info, original_binary_path) if recompiler is None or not binfmt.tool_available(recompiler): return MetricValue( value=0.0, @@ -304,11 +307,12 @@ def compute_for_function( recompiler, list(flags), sorted(context_decls.items()) if context_decls else None, + (info.fmt, info.arch, info.bits), ] return self._cached_value( key_inputs, lambda: self._compute_uncached( - decompiled, original_bytes, recompiler, flags, arch_mode, context_decls + decompiled, original_bytes, recompiler, flags, arch_mode, context_decls, info ), ) @@ -320,6 +324,7 @@ def _compute_uncached( flags: list[str], arch_mode: tuple | None, context_decls: dict[str, str] | None = None, + expected_info: BinInfo | None = None, ) -> MetricValue: import shutil @@ -352,6 +357,18 @@ def _compute_uncached( "fixup_injected": len(fix.injected), } try: + if expected_info is not None: + actual_info = binfmt.object_info(obj_path) + if actual_info != expected_info: + return MetricValue( + value=0.0, + metadata={ + "skipped": True, + "reason": "compiler emitted an unproven or incompatible target", + "expected_target": vars(expected_info), + "emitted_target": vars(actual_info) if actual_info else None, + }, + ) recompiled_bytes = binfmt.object_text_bytes(obj_path, decompiled.name) if recompiled_bytes is None: @@ -433,7 +450,7 @@ def compute_for_binary( info = binfmt.detect(original_binary_path) if info is not None: - recompiler = binfmt.recompiler_for(info) + recompiler = binfmt.recompiler_for(info, original_binary_path) if recompiler is None or not binfmt.tool_available(recompiler): return MetricResult( metric_name=self.name, @@ -449,8 +466,17 @@ def compute_for_binary( from decbench.metrics.fixup import derive_context_decls + # Storage keys may be address-qualified (``foo@0x...``), while + # prototype recovery needs the semantic C identifier. A same-name + # collision is intentionally omitted: choosing either overload's + # signature for calls to the other would conflate distinct functions. + name_counts = Counter(fd.name for fd in decompilation.functions.values()) context_decls = derive_context_decls( - {name: fd.decompiled_code or "" for name, fd in decompilation.functions.items()} + { + fd.name: fd.decompiled_code or "" + for storage_key, fd in decompilation.functions.items() + if name_counts[fd.name] == 1 and parse_function_storage_key(storage_key)[1] is None + } ) for func_name, func_decomp in decompilation.functions.items(): @@ -460,6 +486,9 @@ def compute_for_binary( original_binary_path=original_binary_path, context_decls=context_decls, ) + if value.metadata.get("skipped"): + errors.append(f"{func_name}: abstained: {value.metadata.get('reason')}") + continue function_results[func_name] = value except Exception as e: diff --git a/decbench/metrics/cpp_recovered_types.py b/decbench/metrics/cpp_recovered_types.py new file mode 100644 index 0000000..640636c --- /dev/null +++ b/decbench/metrics/cpp_recovered_types.py @@ -0,0 +1,216 @@ +"""Complete ordinary C record definitions in emitted code, without oracle hints. + +Natural layout is admitted only for explicit LP64/LLP64 data models. Classes, +inheritance, bitfields, packing, attributes and uncertain declarators require +an explicit producer type graph rather than a guessed layout. +""" + +from __future__ import annotations + +import re +from dataclasses import replace +from typing import Any + +import tree_sitter_cpp +from tree_sitter import Language, Parser + +from decbench.metrics.cpp_type_graph import TypeEdge, TypeGraph, TypeNode, _Builder +from decbench.metrics.cpp_types import CppType, UnsupportedType, parse_type + + +class RecoveredTypeContext: + def __init__(self, code: str, data_model: str): + self.data_model = data_model + self.data = code.encode() + self.builder = _Builder() + self.definitions: dict[str, Any] = {} + self.ambiguous: set[str] = set() + self.alignments: dict[str, int] = {} + self.invalid_layout = bool( + re.search(r"#\s*pragma\s+pack|__attribute__|__declspec|alignas", code) + ) + parser = Parser(Language(tree_sitter_cpp.language())) + self.tree = parser.parse(self.data) + self._index(self.tree.root_node) + + def _text(self, node: Any) -> str: + return self.data[node.start_byte : node.end_byte].decode() + + def _index(self, node: Any, scope: str = "") -> None: + if node.type == "namespace_definition": + name = node.child_by_field_name("name") + body = node.child_by_field_name("body") + if name is not None and body is not None: + self._index(body, scope + self._text(name) + "::") + return + if node.type in {"struct_specifier", "union_specifier"}: + body = node.child_by_field_name("body") + name = node.child_by_field_name("name") + alias = None + if node.parent.type == "type_definition": + alias = node.parent.child_by_field_name("declarator") + spelling = self._text(name) if name else self._text(alias) if alias else "" + if body is not None and spelling: + key = scope + spelling + if key in self.definitions: + self.ambiguous.add(key) + self.definitions[key] = node + if alias is not None and self._text(alias) != spelling: + alias_key = scope + self._text(alias) + if alias_key in self.definitions: + self.ambiguous.add(alias_key) + self.definitions[alias_key] = node + return + if node.type in {"translation_unit", "declaration_list", "type_definition", "declaration"}: + for child in node.named_children: + self._index(child, scope) + + def graph(self, spelling: str) -> TypeGraph: + self.builder = _Builder() + self.alignments = {} + root = self._type(spelling) + return self.builder.finish(root) + + def _type(self, spelling: str) -> str: + try: + ty = parse_type(spelling, self.data_model) + key = "text:" + spelling + self.builder.primitive(ty, key) + self.alignments[key] = self._primitive_alignment(ty) + return key + except UnsupportedType: + pass + match = re.fullmatch( + r"(const |volatile )?(?:(?:struct|union) )?([\w:]+)((?:\s*(?:&&|[&*]))*)", + spelling.strip(), + ) + if match is None: + raise UnsupportedType("unsupported recovered object spelling") + qualifier, name, suffix = match.groups() + base = "record:" + name + if self.builder.reserve(base): + self._record(base, name) + if qualifier: + qkey = base + ":" + qualifier + if self.builder.reserve(qkey): + self.builder.nodes[qkey] = replace( + self.builder.nodes[base], qualifiers=(qualifier.strip(),) + ) + self.alignments[qkey] = self.alignments[base] + base = qkey + for i, token in enumerate(re.findall(r"&&|[&*]", suffix)): + if self.builder.nodes[base].kind in {"lref", "rref"}: + raise UnsupportedType("invalid nested reference") + key = spelling + f"/indirection{i}" + size = 4 if self.data_model == "ILP32" else 8 + if self.builder.reserve(key): + self.builder.nodes[key] = TypeNode( + {"*": "pointer", "&": "lref", "&&": "rref"}[token], + size, + edges=(TypeEdge("element", base),), + ) + self.alignments[key] = size + base = key + return base + + def _primitive_alignment(self, ty: CppType) -> int: + if ty.kind == "array": + if ty.element is None: + raise UnsupportedType("missing array element") + return self._primitive_alignment(ty.element) + if self.data_model == "ILP32" and ty.byte_size() > 4: + raise UnsupportedType("ILP32 natural alignment needs explicit target ABI") + if not ty.byte_size(): + raise UnsupportedType("void object field") + return ty.byte_size() + + def _declarator(self, node: Any, target: str, scope: str) -> tuple[str, str]: + if node.type in {"field_identifier", "identifier"}: + return self._text(node), target + if node.type == "pointer_declarator": + child = node.child_by_field_name("declarator") + if child is None or any(c.type == "type_qualifier" for c in node.named_children): + raise UnsupportedType("qualified pointer declarator requires explicit graph") + key = scope + f"/pointer:{node.start_byte}" + self.builder.reserve(key) + size = 4 if self.data_model == "ILP32" else 8 + self.builder.nodes[key] = TypeNode( + "pointer", size, edges=(TypeEdge("element", target),) + ) + self.alignments[key] = size + return self._declarator(child, key, scope) + if node.type == "array_declarator": + child = node.child_by_field_name("declarator") + size = node.child_by_field_name("size") + if child is None or size is None or not self._text(size).isdigit(): + raise UnsupportedType("unknown recovered array extent") + count = int(self._text(size)) + if count < 1 or self.builder.nodes[target].kind == "pending": + raise UnsupportedType("incomplete recovered array") + key = scope + f"/array:{node.start_byte}" + self.builder.reserve(key) + self.builder.nodes[key] = TypeNode( + "array", + count * self.builder.nodes[target].size, + count=count, + edges=(TypeEdge("element", target),), + ) + self.alignments[key] = self.alignments[target] + return self._declarator(child, key, scope) + raise UnsupportedType("uncertain recovered field declarator") + + def _record(self, key: str, name: str) -> None: + node = self.definitions.get(name) + if node is None or name in self.ambiguous or self.invalid_layout or node.has_error: + raise UnsupportedType("complete unambiguous recovered object definition required") + if self.data_model not in {"LP64", "LLP64"} or any( + c.type == "base_class_clause" for c in node.named_children + ): + raise UnsupportedType("recovered target/object layout requires explicit graph") + body = node.child_by_field_name("body") + edges = [] + offset, alignment = 0, 1 + union = node.type == "union_specifier" + for child in body.named_children: + if child.type != "field_declaration": + raise UnsupportedType("non-data recovered record member") + ty_node = child.child_by_field_name("type") + declarators = child.children_by_field_name("declarator") + if ty_node is None or len(declarators) != 1: + raise UnsupportedType("ambiguous recovered field declaration") + allowed = { + "primitive_type", + "type_identifier", + "qualified_identifier", + "sized_type_specifier", + } + if ty_node.type not in allowed or any( + c.type not in allowed | {"type_qualifier", declarators[0].type} + for c in child.named_children + ): + raise UnsupportedType("unsupported recovered field declaration") + spelling = " ".join( + self._text(c) + for c in child.named_children + if c == ty_node or c.type == "type_qualifier" + ) + if "::" in name and ty_node.type == "type_identifier": + candidate = name.rsplit("::", 1)[0] + "::" + spelling + if candidate in self.definitions: + spelling = candidate + target = self._type(spelling) + field_name, target = self._declarator(declarators[0], target, key) + if self.builder.nodes[target].kind == "pending": + raise UnsupportedType("recursive by-value field") + align = self.alignments[target] + alignment = max(alignment, align) + position = 0 if union else (offset + align - 1) // align * align + edges.append(TypeEdge("field", target, field_name, position * 8, access=3)) + offset = max(offset, position + self.builder.nodes[target].size) + if not edges: + raise UnsupportedType("empty recovered record ABI requires explicit graph") + size = (offset + alignment - 1) // alignment * alignment + self.builder.nodes[key] = TypeNode( + "union" if union else "record", size, name, edges=tuple(edges) + ) + self.alignments[key] = alignment diff --git a/decbench/metrics/cpp_type_graph.py b/decbench/metrics/cpp_type_graph.py new file mode 100644 index 0000000..2718bc4 --- /dev/null +++ b/decbench/metrics/cpp_type_graph.py @@ -0,0 +1,685 @@ +"""Bounded source/ABI object graphs; opaque names never establish layout equality. + +Graph edges retain actual debug offsets and virtual-base locators. Producers may +supply the same versioned graph contract under metadata.type_graphs (by complete +type spelling). Node identifiers are local references, never comparison evidence. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field, replace +from typing import Any, Protocol + +from decbench.metrics.cpp_types import CppType, UnsupportedType, parse_type +from decbench.utils import binfmt +from decbench.utils.pdb_info import _numeric, _string, _unpack + +MAX_NODES = 256 + + +@dataclass(frozen=True) +class TypeEdge: + role: str + target: str + name: str = "" + offset_bits: int | None = None + bit_width: int | None = None + virtual: bool = False + locator: str = "" + access: int = 0 + locator_type: str | None = None + + +@dataclass(frozen=True) +class TypeNode: + kind: str + size: int + name: str = "" + qualifiers: tuple[str, ...] = () + rank: str = "" + count: int = 0 + edges: tuple[TypeEdge, ...] = () + enumerators: tuple[tuple[str, int], ...] = () + + +@dataclass(frozen=True, eq=False) +class TypeGraph: + root: str + nodes: dict[str, TypeNode] + view: str = "source" + + def byte_size(self) -> int: + return self.nodes[self.root].size + + def project(self, view: str) -> TypeGraph: + if view not in {"source", "abi"}: + raise ValueError("invalid type view") + return replace(self, view=view) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, TypeGraph) or self.view != other.view: + return False + visited: set[tuple[str, str]] = set() + + def compare(a: str, b: str) -> bool: + source = self.view == "source" + while not source and self.nodes[a].kind == "enum": + a = self.nodes[a].edges[0].target + while not source and other.nodes[b].kind == "enum": + b = other.nodes[b].edges[0].target + if (a, b) in visited: + return True + visited.add((a, b)) + left, right = self.nodes[a], other.nodes[b] + + def label(n: TypeNode) -> tuple: + kind = "pointer" if not source and n.kind in {"lref", "rref"} else n.kind + return ( + kind, + n.size, + n.name if source else "", + n.qualifiers if source else (), + n.rank if source else "", + n.count, + tuple(sorted(n.enumerators)) if source else (), + ) + + def edge_label(e: TypeEdge) -> tuple: + return ( + e.role, + e.name if source else "", + e.offset_bits, + e.bit_width, + e.virtual, + e.locator, + e.access if source else 0, + ) + + if label(left) != label(right) or len(left.edges) != len(right.edges): + return False + remaining = list(right.edges) + for edge in left.edges: + for i, candidate in enumerate(remaining): + if edge_label(edge) != edge_label(candidate): + continue + saved = visited.copy() + locator_equal = ( + edge.locator_type is None and candidate.locator_type is None + ) or ( + edge.locator_type is not None + and candidate.locator_type is not None + and compare(edge.locator_type, candidate.locator_type) + ) + if locator_equal and compare(edge.target, candidate.target): + remaining.pop(i) + break + visited.clear() + visited.update(saved) + else: + return False + return True + + return compare(self.root, other.root) + + @classmethod + def from_payload(cls, payload: dict[str, Any]) -> TypeGraph: + """Validate explicit producer graphs, including reachable layout completeness.""" + if not isinstance(payload, dict) or payload.get("schema") != "decbench-cpp-type-graph-v1": + raise UnsupportedType("unknown recovered type graph schema") + raw = payload.get("nodes") + if not isinstance(raw, dict) or not 0 < len(raw) <= MAX_NODES: + raise UnsupportedType("invalid recovered type graph population") + nodes = {} + allowed = { + "record", + "union", + "enum", + "pointer", + "lref", + "rref", + "array", + "void", + "signed", + "unsigned", + "char", + "bool", + "float", + "char8_t", + "char16_t", + "char32_t", + } + try: + for key, node in raw.items(): + if ( + not isinstance(key, str) + or not isinstance(node, dict) + or node.get("complete") is not True + ): + raise UnsupportedType("incomplete recovered type node") + value = TypeNode( + kind=node["kind"], + size=node["size"], + name=node.get("name", ""), + qualifiers=tuple(sorted(node.get("qualifiers", []))), + rank=node.get("rank", ""), + count=node.get("count", 0), + edges=tuple(TypeEdge(**e) for e in node.get("edges", [])), + enumerators=tuple(tuple(e) for e in node.get("enumerators", [])), + ) + if value.kind not in allowed or type(value.size) is not int or value.size < 0: + raise UnsupportedType("invalid recovered type node") + nodes[key] = value + graph = cls(payload["root"], nodes) + graph.validate() + return graph + except (KeyError, TypeError) as exc: + raise UnsupportedType("invalid recovered graph contract") from exc + + def validate(self) -> None: + if self.root not in self.nodes: + raise UnsupportedType("missing type graph root") + for node in self.nodes.values(): + if len(node.edges) > MAX_NODES: + raise UnsupportedType("too many type graph edges") + if node.kind == "enum" and ( + len(node.edges) != 1 + or self.nodes.get(node.edges[0].target, TypeNode("missing", 0)).kind + not in {"signed", "unsigned", "char"} + ): + raise UnsupportedType("invalid enum underlying type") + if node.kind == "pending": + raise UnsupportedType("unresolved recursive type node") + roles = [e.role for e in node.edges] + if node.kind in {"pointer", "lref", "rref", "array", "enum"} and roles != ["element"]: + raise UnsupportedType("missing/ambiguous element type") + if node.kind == "array" and node.count < 1: + raise UnsupportedType("unknown array extent") + if ( + node.kind not in {"record", "union", "pointer", "lref", "rref", "array", "enum"} + and roles + ): + raise UnsupportedType("scalar carries layout edges") + if ( + not isinstance(node.name, str) + or not isinstance(node.rank, str) + or any(q not in {"const", "volatile"} for q in node.qualifiers) + ): + raise UnsupportedType("invalid node identity/qualifiers") + if ( + node.kind == "array" + and node.size != node.count * self.nodes[node.edges[0].target].size + ): + raise UnsupportedType("array extent disagrees with element") + for edge in node.edges: + if ( + not isinstance(edge.name, str) + or not isinstance(edge.locator, str) + or type(edge.virtual) is not bool + or edge.access not in {0, 1, 2, 3} + ): + raise UnsupportedType("invalid edge attributes") + if edge.target not in self.nodes or edge.role not in { + "field", + "base", + "element", + "vptr", + }: + raise UnsupportedType("invalid type graph edge") + if edge.role in {"field", "base"} and not edge.virtual: + if type(edge.offset_bits) is not int or edge.offset_bits < 0: + raise UnsupportedType("unknown member offset") + width = ( + edge.bit_width + if edge.bit_width is not None + else self.nodes[edge.target].size * 8 + ) + if ( + type(width) is not int + or width < 0 + or edge.offset_bits + width > node.size * 8 + ): + raise UnsupportedType("member exceeds object extent") + if edge.virtual and ( + edge.role != "base" or not edge.locator or edge.offset_bits is not None + ): + raise UnsupportedType("incomplete virtual base locator") + if edge.locator_type is not None and ( + not edge.virtual + or edge.locator_type not in self.nodes + or self.nodes[edge.locator_type].kind != "pointer" + ): + raise UnsupportedType("invalid virtual base locator type") + if edge.bit_width is not None and ( + edge.role != "field" + or type(edge.bit_width) is not int + or not 0 < edge.bit_width <= self.nodes[edge.target].size * 8 + ): + raise UnsupportedType("invalid bit field storage width") + complete: set[str] = set() + active: set[str] = set() + + def storage_cycle(key: str) -> None: + if key in active: + raise UnsupportedType("cyclic by-value object storage") + if key in complete: + return + active.add(key) + node = self.nodes[key] + if node.kind not in {"pointer", "lref", "rref"}: + for edge in node.edges: + storage_cycle(edge.target) + active.remove(key) + complete.add(key) + + for key in self.nodes: + storage_cycle(key) + + +@dataclass +class _Builder: + nodes: dict[str, TypeNode] = field(default_factory=dict) + + def reserve(self, key: str) -> bool: + if key in self.nodes: + return False + if len(self.nodes) >= MAX_NODES: + raise UnsupportedType("type graph exceeds node bound") + self.nodes[key] = TypeNode("pending", 0) + return True + + def primitive(self, ty: CppType, key: str = "scalar") -> str: + if self.reserve(key): + edges = ( + (TypeEdge("element", self.primitive(ty.element, key + "/element")),) + if ty.element + else () + ) + self.nodes[key] = TypeNode( + ty.kind, + ty.byte_size(), + qualifiers=ty.qualifiers, + rank=ty.rank, + count=ty.count, + edges=edges, + ) + return key + + def qualified(self, key: str, target: str, qualifiers: tuple[str, ...]) -> None: + node = self.nodes[target] + if node.kind in {"pending", "alias", "cv"}: + self.nodes[key] = TypeNode( + "cv", 0, qualifiers=qualifiers, edges=(TypeEdge("element", target),) + ) + elif node.kind == "array": + child = key + "/qualified_element" + self.reserve(child) + self.qualified(child, node.edges[0].target, qualifiers) + self.nodes[key] = replace(node, edges=(TypeEdge("element", child),)) + elif node.kind in {"lref", "rref"}: + raise UnsupportedType("cv-qualified reference graph") + else: + self.nodes[key] = replace( + node, qualifiers=tuple(sorted(set(node.qualifiers) | set(qualifiers))) + ) + + def finish(self, root: str) -> TypeGraph: + for _ in range(MAX_NODES): + unresolved = [ + (key, node) for key, node in self.nodes.items() if node.kind in {"alias", "cv"} + ] + if not unresolved: + break + progress = False + for key, node in unresolved: + target = node.edges[0].target + if self.nodes[target].kind in {"pending", "alias", "cv"}: + continue + if node.kind == "cv": + self.qualified(key, target, node.qualifiers) + else: + self.nodes[key] = self.nodes[target] + progress = True + if not progress: + raise UnsupportedType("unresolved recursive type modifiers") + result = TypeGraph(root, self.nodes) + result.validate() + return result + + +def primitive_graph(ty: CppType) -> TypeGraph: + builder = _Builder() + return builder.finish(builder.primitive(ty)) + + +def _qualified_name(die: Any) -> str: + names = [] + while die is not None: + if die.tag in { + "DW_TAG_namespace", + "DW_TAG_class_type", + "DW_TAG_structure_type", + "DW_TAG_union_type", + "DW_TAG_enumeration_type", + }: + name = binfmt.die_str_attr(die, "DW_AT_name") + if not name: + raise UnsupportedType("anonymous object identity unsupported") + names.append(name) + die = die.get_parent() + return "::".join(reversed(names)) + + +def dwarf_graph(variable: Any, data_model: str) -> TypeGraph: + """Resolve complete DWARF graphs by DIE references; never borrow a named definition.""" + builder = _Builder() + + def reference(die: Any) -> str: + attr, owner = binfmt.die_attr_owner(die, "DW_AT_type", follow_abstract_origin=True) + return ( + visit(owner.get_DIE_from_attribute("DW_AT_type")) + if attr + else builder.primitive(parse_type("void", data_model)) + ) + + def visit(die: Any) -> str: + key = f"die:{die.offset}" + if not builder.reserve(key): + return key + tag = die.tag + + def attr(name: str) -> Any: + value = binfmt.die_attr(die, name) + return value.value if value is not None else None + + if tag == "DW_TAG_typedef": + target = reference(die) + builder.nodes[key] = ( + builder.nodes[target] + if builder.nodes[target].kind not in {"pending", "alias", "cv"} + else TypeNode("alias", 0, edges=(TypeEdge("element", target),)) + ) + elif tag == "DW_TAG_base_type": + ty = parse_type(binfmt.die_str_attr(die, "DW_AT_name") or "", data_model) + if attr("DW_AT_byte_size") != ty.byte_size(): + raise UnsupportedType("DWARF base width disagrees with data model") + builder.nodes[key] = TypeNode(ty.kind, ty.byte_size(), rank=ty.rank) + elif tag in {"DW_TAG_const_type", "DW_TAG_volatile_type"}: + target = reference(die) + q = "const" if tag == "DW_TAG_const_type" else "volatile" + builder.qualified(key, target, (q,)) + elif tag in { + "DW_TAG_pointer_type", + "DW_TAG_reference_type", + "DW_TAG_rvalue_reference_type", + }: + size = attr("DW_AT_byte_size") or die.cu["address_size"] + if size != (4 if data_model == "ILP32" else 8): + raise UnsupportedType("DWARF pointer width disagrees with data model") + builder.nodes[key] = TypeNode( + { + "DW_TAG_pointer_type": "pointer", + "DW_TAG_reference_type": "lref", + "DW_TAG_rvalue_reference_type": "rref", + }[tag], + size, + edges=(TypeEdge("element", reference(die)),), + ) + elif tag in {"DW_TAG_structure_type", "DW_TAG_class_type", "DW_TAG_union_type"}: + size = attr("DW_AT_byte_size") + if size is None or attr("DW_AT_declaration"): + raise UnsupportedType("incomplete DWARF object definition") + edges = [] + for child in die.iter_children(): + if child.tag not in {"DW_TAG_member", "DW_TAG_inheritance"}: + continue + location = binfmt.die_attr(child, "DW_AT_data_member_location") + bit_width = binfmt.die_attr(child, "DW_AT_bit_size") + bit_offset = binfmt.die_attr(child, "DW_AT_data_bit_offset") + virtual_attr = binfmt.die_attr(child, "DW_AT_virtuality") + virtual = bool(virtual_attr and virtual_attr.value) + access_attr = binfmt.die_attr(child, "DW_AT_accessibility") + # CodeView uses public=3/protected=2/private=1; DWARF reverses public/private. + access = ( + {1: 3, 2: 2, 3: 1}.get(access_attr.value, 0) + if access_attr + else (1 if tag == "DW_TAG_class_type" else 3) + ) + if child.tag == "DW_TAG_member" and location is None and bit_offset is None: + if binfmt.die_attr(child, "DW_AT_external") or binfmt.die_attr( + child, "DW_AT_declaration" + ): + continue + if tag != "DW_TAG_union_type": + raise UnsupportedType("unknown DWARF field offset") + locator = "" + if virtual: + if location is None or not isinstance(location.value, (list, bytes)): + raise UnsupportedType("unknown DWARF virtual base locator") + offset = None + locator = "dwarf:" + bytes(location.value).hex() + elif bit_width is not None: + if bit_offset is None: + raise UnsupportedType("legacy/endian-dependent bit field offset") + offset = int(bit_offset.value) + elif location is not None: + if type(location.value) is not int: + raise UnsupportedType("computed nonvirtual member offset") + offset = location.value * 8 + else: + offset = 0 + edges.append( + TypeEdge( + "base" if child.tag == "DW_TAG_inheritance" else "field", + reference(child), + binfmt.die_str_attr(child, "DW_AT_name") or "", + offset, + int(bit_width.value) if bit_width else None, + virtual, + locator, + access, + ) + ) + builder.nodes[key] = TypeNode( + "union" if tag == "DW_TAG_union_type" else "record", + int(size), + _qualified_name(die), + edges=tuple(edges), + ) + elif tag == "DW_TAG_array_type": + target = reference(die) + counts = [] + for child in die.iter_children(): + if child.tag == "DW_TAG_subrange_type": + count = binfmt.die_attr(child, "DW_AT_count") + upper = binfmt.die_attr(child, "DW_AT_upper_bound") + lower = binfmt.die_attr(child, "DW_AT_lower_bound") + n = ( + int(count.value) + if count + else ( + int(upper.value) - (int(lower.value) if lower else 0) + 1 + if upper + else 0 + ) + ) + if n < 1: + raise UnsupportedType("unknown DWARF array extent") + counts.append(n) + if not counts or builder.nodes[target].kind == "pending": + raise UnsupportedType("incomplete array element") + for i, n in reversed(list(enumerate(counts))): + node = TypeNode( + "array", + n * builder.nodes[target].size, + count=n, + edges=(TypeEdge("element", target),), + ) + if i: + inner = key + f"/dim{i}" + builder.reserve(inner) + builder.nodes[inner] = node + target = inner + else: + builder.nodes[key] = node + elif tag == "DW_TAG_enumeration_type": + target = reference(die) + values = tuple( + ( + binfmt.die_str_attr(c, "DW_AT_name") or "", + int(binfmt.die_attr(c, "DW_AT_const_value").value), + ) + for c in die.iter_children() + if c.tag == "DW_TAG_enumerator" + ) + if attr("DW_AT_byte_size") is None or builder.nodes[target].kind == "void": + raise UnsupportedType("incomplete enum underlying type") + builder.nodes[key] = TypeNode( + "enum", + int(attr("DW_AT_byte_size")), + _qualified_name(die), + edges=(TypeEdge("element", target),), + enumerators=values, + ) + else: + raise UnsupportedType(f"unsupported DWARF graph {tag}") + return key + + return builder.finish(reference(variable)) + + +def pdb_graph(types: Any, index: int, data_model: str) -> TypeGraph: + """Follow CodeView indices, preserving fields, ordinary bases and virtual locators.""" + builder = _Builder() + + def visit(index: int) -> str: + key = f"cv:{index:x}" + if not builder.reserve(key): + return key + if index < 0x1000: + scalar = types.resolve(index) + if scalar is None: + raise UnsupportedType("unsupported CodeView primitive") + ty = parse_type(scalar.source_name, data_model) + if ty.byte_size() != scalar.size: + raise UnsupportedType("CodeView primitive data model mismatch") + builder.nodes[key] = builder.nodes[builder.primitive(ty, key + "/primitive")] + return key + record = types.records.get(index) + if record is None: + raise UnsupportedType("missing CodeView type record") + kind, data = record + if kind in {0x1504, 0x1505, 0x1519, 0x1506}: + layout = types.record_layout(index) + edges = [] + for member in layout["members"]: + role = member["kind"] + if role == "enumerator": + raise UnsupportedType("enumerator in object field list") + target_index = member["type_index"] + width = None + position = member.get("offset") + offset = position * 8 if position is not None else None + bit_record = types.records.get(target_index) + if bit_record and bit_record[0] == 0x1205: + target_index, width, bit_position = _unpack("> 5) & 7 + size = (attrs >> 13) & 63 or {10: 4, 12: 8}.get(attrs & 31) + if ( + mode not in {0, 1, 4} + or size != (4 if data_model == "ILP32" else 8) + or attrs & 0x1000 + ): + raise UnsupportedType("unsupported CodeView pointer/member pointer") + qualifiers = tuple( + q for q, flag in (("const", 0x400), ("volatile", 0x200)) if attrs & flag + ) + builder.nodes[key] = TypeNode( + {0: "pointer", 1: "lref", 4: "rref"}[mode], + size, + qualifiers=qualifiers, + edges=(TypeEdge("element", visit(base)),), + ) + elif kind == 0x1001: + base, flags = _unpack(" TypeGraph: ... + + +def recovered_graph( + spelling: str, + data_model: str, + metadata: dict[str, Any], + context: RecoveredGraphContext | None = None, +) -> TypeGraph: + """Require complete producer graphs for object types; scalar spelling stays supported.""" + payload = metadata.get("type_graphs", {}).get(spelling) + if payload is not None: + return TypeGraph.from_payload(payload) + try: + return primitive_graph(parse_type(spelling, data_model)) + except UnsupportedType: + if context is None: + raise + return context.graph(spelling) diff --git a/decbench/metrics/cpp_type_match.py b/decbench/metrics/cpp_type_match.py new file mode 100644 index 0000000..558a8f9 --- /dev/null +++ b/decbench/metrics/cpp_type_match.py @@ -0,0 +1,346 @@ +"""Opt-in C++ policy proposal; deliberately separate from type_match / Union.""" + +from __future__ import annotations + +from dataclasses import asdict +from pathlib import Path +from typing import Any + +from decbench.decompilers.provenance import NativeProvenanceContext, sanitize_native_provenance +from decbench.metrics.base import Metric, MetricConfig +from decbench.metrics.cpp_recovered_types import RecoveredTypeContext +from decbench.metrics.cpp_type_graph import dwarf_graph, pdb_graph, recovered_graph +from decbench.metrics.cpp_types import ( + POLICY_VERSION, + UnsupportedType, + dwarf_location_supported, +) +from decbench.metrics.type_evidence import PreprocessedSourceContext, build_source_evidence +from decbench.metrics.type_match import _get_location +from decbench.metrics.variable_match import ( + VariableEvidence, + extract_decompiler_evidence, + match_variables, +) +from decbench.models.decompilation import DecompilationResult, FunctionDecompilation +from decbench.models.metrics import MetricResult, MetricValue +from decbench.utils import binfmt +from decbench.utils.native_code import die_ranges, entry_address_candidates +from decbench.utils.pdb_info import PdbFormatError +from decbench.utils.producer_validation import inspect_producer + + +class CppOracleContext: + """One immutable image's debug index shared across producer/view comparisons. + + This is in-memory oracle preparation, not a score or decompilation cache. + A changed file invalidates the context instead of serving stale evidence. + """ + + def __init__(self, path: Path): + self.path = path.resolve() + self.signature = self._signature() + self.debug_snapshot = binfmt.NativeDebugSnapshot.capture(self.path) + self.info = binfmt.detect(self.path) + self.dwarf = binfmt.dwarf_info(self.path) + self.pdb = binfmt.pdb_info(self.path) if self.dwarf is None else None + self.native = NativeProvenanceContext(self.path) + self.index: dict[int, list[Any]] = {} + if self.dwarf is not None: + for cu in self.dwarf.iter_CUs(): + for die in cu.iter_DIEs(): + if die.tag != "DW_TAG_subprogram": + continue + ranges = die_ranges(die, self.dwarf) + if ranges: + self.index.setdefault(min(a for a, _ in ranges), []).append(die) + self.validate(self.path) + + def _signature(self) -> tuple[int, int, int]: + stat = self.path.stat() + return stat.st_ino, stat.st_size, stat.st_mtime_ns + + def validate(self, path: Path) -> None: + self.debug_snapshot.validate() + if path.resolve() != self.path or self._signature() != self.signature: + raise ValueError("C++ oracle context does not match immutable input") + + +class CppTypeMatchMetric(Metric): + name = "cpp_type_match" + display_name = "C++ Types (proposal, supported subset)" + lower_is_better = False + perfect_value = 1.0 + cache_version = "5" + + def __init__(self, config: MetricConfig | None = None): + super().__init__(config) + options = self.config.extra_options + self.view = options.get("view", "source") + self.data_model = str(options.get("data_model")) + if self.view not in {"source", "abi"}: + raise ValueError("C++ view must be source or abi") + if self.data_model not in {"LP64", "LLP64", "ILP32"}: + raise ValueError("C++ proposal requires an explicit data_model: LP64/LLP64/ILP32") + if set(options) - {"view", "data_model"}: + raise ValueError("unknown C++ type policy option") + + def compute_for_function( + self, decompiled: FunctionDecompilation, source_cfg=None, decompiled_cfg=None, **kwargs: Any + ) -> MetricValue: + gt = kwargs.get("ground_truth_vars") + if not gt: + raise ValueError("C++ policy requires complete typed ground truth") + evidence = kwargs.get("source_evidence") + if evidence is None: + raise ValueError("C++ policy requires type-blind source evidence") + inspection = inspect_producer(decompiled.decompiled_code, decompiled.metadata) + if inspection.invalid: + return MetricValue( + value=0.0, + raw_value=len(gt), + metadata={ + "policy": POLICY_VERSION, + "view": self.view, + "data_model": self.data_model, + "ground_truth_count": len(gt), + "correct_count": 0, + "matched_count": 0, + "producer_failure": "invalid_producer_output", + "producer_validation": inspection.evidence(), + "official_score": False, + "decisions": [ + { + "identity": row["identity"], + "matched_identity": None, + "expected": asdict(row["descriptor"].project(self.view)), + "actual": None, + "verdict": "invalid_producer_output", + } + for row in gt + ], + }, + ) + recovered = extract_decompiler_evidence( + decompiled, backend="decompiler", identity_prefix="decompiler", include_unnamed=True + ) + match = match_variables(evidence, recovered, min_overlap=0.1, ambiguity_margin=0.03) + by_id = {f"decompiler:{i}": v for i, v in enumerate(decompiled.variables)} + mapping = {m.source_id: m.decompiled_id for m in match.matches} + decisions = [] + recovered_types = RecoveredTypeContext(decompiled.decompiled_code, self.data_model) + for row in gt: + expected = row["descriptor"].project(self.view) + variable = by_id.get(mapping.get(row["identity"], "")) + actual = None + reason = "unmatched" + if variable is not None: + try: + actual = recovered_graph( + variable.type, self.data_model, decompiled.metadata, recovered_types + ).project(self.view) + reason = "correct" if actual == expected else "type_mismatch" + if variable.size is not None and variable.size != actual.byte_size(): + reason = "inconsistent_recovered_storage_width" + except UnsupportedType: + reason = "unsupported_recovered_type" + decisions.append( + { + "identity": row["identity"], + "matched_identity": mapping.get(row["identity"]), + "expected": asdict(expected), + "actual": asdict(actual) if actual else None, + "verdict": reason, + } + ) + correct = sum(d["verdict"] == "correct" for d in decisions) + return MetricValue( + value=correct / len(gt), + raw_value=len(gt) - correct, + metadata={ + "policy": POLICY_VERSION, + "view": self.view, + "data_model": self.data_model, + "ground_truth_count": len(gt), + "correct_count": correct, + "matched_count": len(match.matches), + "unmatched_source": match.unmatched_source, + "unobservable_source": match.unobservable_source, + "decisions": decisions, + "official_score": False, + "producer_validation": inspection.evidence(), + }, + ) + + def _dwarf_rows( + self, dwarf: Any, address_candidates: tuple[int, ...], oracle_index: dict[int, list[Any]] + ) -> list[dict]: + candidates = { + die.offset: die + for address in address_candidates + for die in oracle_index.get(address, []) + } + if len(candidates) != 1: + raise UnsupportedType("DWARF function missing or entry identity ambiguous") + function_die = next(iter(candidates.values())) + # Aggregate returns do not prove ordinary ABI formal ordinals. + return_type = dwarf_graph(function_die, self.data_model) + formal_ordinals_proven = return_type.nodes[return_type.root].kind not in { + "record", + "union", + "array", + } + rows = [] + arg_index = 0 + + def walk(parent: Any) -> None: + nonlocal arg_index + for child in parent.iter_children(): + if child.tag == "DW_TAG_inlined_subroutine": + # Nested inline locals and split/abstract identities require + # their own source correspondence contract. + raise UnsupportedType("inline variable identity policy unsupported") + if child.tag == "DW_TAG_lexical_block": + walk(child) + continue + if child.tag not in {"DW_TAG_formal_parameter", "DW_TAG_variable"}: + continue + is_arg = child.tag == "DW_TAG_formal_parameter" and parent is function_die + ordinal = arg_index if is_arg and formal_ordinals_proven else None + if is_arg: + arg_index += 1 + offsets, observable = _get_location(child, dwarf) + dwarf_location_supported(child, dwarf) + if not observable: + # Optimized-out oracle variables are excluded uniformly. + continue + descriptor = dwarf_graph(child, self.data_model) + if descriptor.nodes[descriptor.root].kind == "void": + raise UnsupportedType("missing variable type") + rows.append( + { + "identity": f"dwarf:0x{child.offset:x}", + "descriptor": descriptor, + "rbp_offset": sorted(set(offsets)), + "arg_index": ordinal, + "name": binfmt.die_str_attr( + child, "DW_AT_name", follow_abstract_origin=True + ) + or "", + } + ) + + walk(function_die) + return rows + + def compute_for_binary( + self, + decompilation: DecompilationResult, + source_cfgs=None, + decompiled_cfgs=None, + **kwargs: Any, + ) -> MetricResult: + # Do not mutate reused producer output when removing unverifiable PCs. + working = decompilation.model_copy(deep=True) + requested = kwargs.get("requested_functions") + if requested is not None: + addresses = {address for _, address in requested} + if len(addresses) != len(requested): + raise ValueError("duplicate requested function entry") + working.functions = { + key: f for key, f in working.functions.items() if f.address in addresses + } + # Evaluation plans own the population. A missing producer result is an + # empty recovery at the requested entry, not a new oracle exclusion. + producer_addresses = [f.address for f in working.functions.values()] + if len(producer_addresses) != len(set(producer_addresses)): + raise ValueError("ambiguous producer records at one requested address") + returned = set(producer_addresses) + for name, address in requested or []: + if address not in returned: + working.add_function( + FunctionDecompilation( + name=name, + address=address, + decompiled_code="", + ) + ) + returned.add(address) + oracle_context = kwargs.get("oracle_context") or CppOracleContext(working.binary_path) + oracle_context.validate(working.binary_path) + sanitizer = sanitize_native_provenance(working, context=oracle_context.native) + info, dwarf, pdb = oracle_context.info, oracle_context.dwarf, oracle_context.pdb + oracle_index = oracle_context.index + values, errors = {}, [] + context = PreprocessedSourceContext( + tuple(Path(p) for p in kwargs.get("preprocessed_sources", [])), working.binary_name + ) + try: + for key, function in working.functions.items(): + try: + if dwarf is not None: + gt = self._dwarf_rows( + dwarf, + entry_address_candidates(function.address, info.arch if info else ""), + oracle_index, + ) + evidence = list( + build_source_evidence( + working.binary_path, function.name, function.address, gt, context + ).variables + ) + elif pdb is not None: + from decbench.utils.pdb_info import ground_truth_variables + + oracle = pdb.at_address(function.address) + if oracle is None or not oracle.measurable: + raise UnsupportedType( + "PDB identity/storage/formal classification incomplete" + ) + raw = ground_truth_variables(working.binary_path, oracle) + gt = [] + evidence = [] + for row in raw: + # The oracle graph is independent of the selected view. + if pdb.types is None: + raise UnsupportedType("PDB type graph context unavailable") + variable = next( + v for v in oracle.variables if v.identity == row["identity"] + ) + descriptor = pdb_graph(pdb.types, variable.type_index, self.data_model) + if descriptor.byte_size() != row["size"]: + raise UnsupportedType("PDB width disagrees with data model") + gt.append({**row, "descriptor": descriptor}) + evidence.append( + VariableEvidence(row["identity"], frozenset(row["addresses"])) + ) + else: + raise UnsupportedType("no readable debug oracle") + if not gt: + raise UnsupportedType("no observable variable ground truth") + if any( + not (v.addresses or v.stack_offsets or v.arg_index is not None) + for v in evidence + ): + raise UnsupportedType("oracle variable correspondence unavailable") + except (UnsupportedType, PdbFormatError) as exc: + errors.append(f"{key}: {exc}") + continue + value = self.compute_for_function( + function, ground_truth_vars=gt, source_evidence=evidence + ) + value.metadata.update( + ground_truth_format="dwarf" if dwarf else "pdb", native_provenance=sanitizer + ) + values[key] = value + finally: + context.close() + result = MetricResult( + metric_name=self.name, + decompiler_name=working.decompiler.decompiler_name, + binary_name=working.binary_name, + function_results=values, + errors=errors, + ) + result.compute_aggregates(perfect_value=self.perfect_value) + return result diff --git a/decbench/metrics/cpp_types.py b/decbench/metrics/cpp_types.py new file mode 100644 index 0000000..96258e4 --- /dev/null +++ b/decbench/metrics/cpp_types.py @@ -0,0 +1,270 @@ +"""Versioned, conservative C++ type descriptors. No C-era lossy normalization. + +This is a proposal implementation, not the public leaderboard's type policy. +Named object layout, enums, member/function pointers and alias recovery require +additional contracts; an unsupported oracle type excludes the whole function. +An unsupported *recovered* type is a miss, never an evaluator exclusion. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, replace +from typing import Any + +from decbench.utils import binfmt + +POLICY_VERSION = "cpp-types-v2-proposal" + + +@dataclass(frozen=True) +class CppType: + kind: str + bits: int = 0 + qualifiers: tuple[str, ...] = () + element: CppType | None = None + count: int = 0 + rank: str = "" + + def byte_size(self) -> int: + if self.kind == "array": + if self.element is None: + raise UnsupportedType("array has no element") + return self.count * self.element.byte_size() + return self.bits // 8 + + def project(self, view: str) -> CppType: + if view == "source": + return self + if view != "abi": + raise ValueError("view must be source or abi") + return replace( + self, + kind="pointer" if self.kind in {"lref", "rref"} else self.kind, + qualifiers=(), + rank="", + element=self.element.project(view) if self.element else None, + ) + + +class UnsupportedType(ValueError): + """The ground truth cannot support a complete policy verdict.""" + + +def parse_type(spelling: str, data_model: str) -> CppType: + """Parse only the documented scalar / ordinary indirection subset. + + Never erase a namespace, instantiate a template, equate a name to an object + layout, decay an array, or award width-only undefinedN credit. + """ + if data_model not in {"LP64", "LLP64", "ILP32"}: + raise ValueError("data_model must be explicitly LP64, LLP64 or ILP32") + tokens = re.findall(r"&&|\w+|[^\s]", spelling) + boundary = next((i for i, t in enumerate(tokens) if t in {"*", "&", "&&", "["}), len(tokens)) + base = tokens[:boundary] + qualifiers = tuple(sorted({t for t in base if t in {"const", "volatile"}})) + words = [t for t in base if t not in {"const", "volatile"}] + name = " ".join(words) + long_bits = 64 if data_model == "LP64" else 32 + pointer_bits = 32 if data_model == "ILP32" else 64 + aliases = { + "uint": "unsigned int", + "ushort": "unsigned short", + "uchar": "unsigned char", + "ulong": "unsigned long", + "size_t": ( + "unsigned long" + if data_model == "LP64" + else ("unsigned long long" if data_model == "LLP64" else "unsigned int") + ), + "ssize_t": ( + "long" if data_model == "LP64" else ("long long" if data_model == "LLP64" else "int") + ), + } + source_alias = name in {"size_t", "ssize_t"} + name = aliases.get(name, name) + fixed = re.fullmatch(r"(u?)int(8|16|32|64)_t", name) + if fixed: + # A fixed-width alias does not prove its fundamental C++ rank (long + # and long long can both be 64 bits). It may match only in the ABI view. + ty = CppType( + "unsigned" if fixed[1] else "signed", + int(fixed[2]), + qualifiers, + rank="fixed_width_alias", + ) + elif name in {"void", "bool", "char", "char8_t", "char16_t", "char32_t", "float", "double"}: + kind, bits = { + "void": ("void", 0), + "bool": ("bool", 8), + "char": ("char", 8), + "char8_t": ("char8_t", 8), + "char16_t": ("char16_t", 16), + "char32_t": ("char32_t", 32), + "float": ("float", 32), + "double": ("float", 64), + }[name] + ty = CppType(kind, bits, qualifiers, rank=name) + else: + parts = name.split() + unsigned = "unsigned" in parts + if "signed" in parts and unsigned: + raise UnsupportedType(f"invalid signedness: {spelling}") + core = [p for p in parts if p not in {"signed", "unsigned", "int"}] + valid = { + (): 32, + ("char",): 8, + ("short",): 16, + ("long",): long_bits, + ("long", "long"): 64, + ("__int8",): 8, + ("__int16",): 16, + ("__int32",): 32, + ("__int64",): 64, + } + if ( + not parts + or tuple(core) not in valid + or parts.count("int") > 1 + or parts.count("unsigned") > 1 + or parts.count("signed") > 1 + or ("int" in parts and core and (core[0] == "char" or core[0].startswith("__int"))) + ): + raise UnsupportedType(f"unsupported nominal/complex type: {spelling}") + rank = " ".join(core) or "int" + if rank.startswith("__int"): + rank = "fixed_width_alias" + ty = CppType( + "unsigned" if unsigned else "signed", valid[tuple(core)], qualifiers, rank=rank + ) + if source_alias: + ty = replace(ty, rank="fixed_width_alias") + i = boundary + array_counts: list[int] = [] + while i < len(tokens): + token = tokens[i] + i += 1 + if token in {"*", "&", "&&"} and not array_counts: + q = [] + while i < len(tokens) and tokens[i] in {"const", "volatile"}: + q.append(tokens[i]) + i += 1 + if token != "*" and q: + raise UnsupportedType("cv-qualified reference") + if ty.kind in {"lref", "rref"}: + raise UnsupportedType("invalid nested reference") + ty = CppType( + {"*": "pointer", "&": "lref", "&&": "rref"}[token], + pointer_bits, + tuple(sorted(set(q))), + ty, + ) + elif token == "[" and i + 1 < len(tokens) and tokens[i].isdigit() and tokens[i + 1] == "]": + count = int(tokens[i]) + i += 2 + if count < 1: + raise UnsupportedType("unknown/zero array extent") + array_counts.append(count) + else: + raise UnsupportedType(f"unsupported declarator: {spelling}") + for count in reversed(array_counts): + if ty.kind in {"lref", "rref", "void"}: + raise UnsupportedType("invalid array element category") + ty = CppType("array", element=ty, count=count) + return ty + + +def dwarf_type(variable: Any, data_model: str, depth: int = 0) -> CppType: + """Read the actual DIE graph, retaining cv and reference categories.""" + if depth > 32: + raise UnsupportedType("cyclic/deep DWARF type") + attr, owner = binfmt.die_attr_owner(variable, "DW_AT_type", follow_abstract_origin=True) + if attr is None: + return CppType("void", rank="void") + die = owner.get_DIE_from_attribute("DW_AT_type") + tag = die.tag + if tag == "DW_TAG_typedef": + return dwarf_type(die, data_model, depth + 1) + if tag == "DW_TAG_base_type": + name = binfmt.die_str_attr(die, "DW_AT_name") or "" + result = parse_type(name, data_model) + size = binfmt.die_attr(die, "DW_AT_byte_size") + if size is None or result.bits != int(size.value) * 8: + raise UnsupportedType("base type width disagrees with explicit data model") + return result + if tag in {"DW_TAG_const_type", "DW_TAG_volatile_type"}: + element = dwarf_type(die, data_model, depth + 1) + qualifier = "const" if tag == "DW_TAG_const_type" else "volatile" + + def qualify(value: CppType) -> CppType: + # cv on an array typedef qualifies its elements, at every array + # dimension; cv on a pointer qualifies the pointer itself. + if value.kind == "array" and value.element is not None: + return replace(value, element=qualify(value.element)) + if value.kind in {"lref", "rref"}: + raise UnsupportedType("cv-qualified reference oracle") + return replace(value, qualifiers=tuple(sorted(set(value.qualifiers) | {qualifier}))) + + return qualify(element) + if tag in {"DW_TAG_pointer_type", "DW_TAG_reference_type", "DW_TAG_rvalue_reference_type"}: + size = binfmt.die_attr(die, "DW_AT_byte_size") + bits = int(size.value) * 8 if size else int(die.cu["address_size"]) * 8 + expected = 32 if data_model == "ILP32" else 64 + if bits != expected: + raise UnsupportedType("pointer width disagrees with data model") + return CppType( + { + "DW_TAG_pointer_type": "pointer", + "DW_TAG_reference_type": "lref", + "DW_TAG_rvalue_reference_type": "rref", + }[tag], + bits, + element=dwarf_type(die, data_model, depth + 1), + ) + if tag == "DW_TAG_array_type": + counts = [] + for child in die.iter_children(): + if child.tag != "DW_TAG_subrange_type": + continue + count = child.attributes.get("DW_AT_count") + upper = child.attributes.get("DW_AT_upper_bound") + lower = child.attributes.get("DW_AT_lower_bound") + if count is not None and count.form.startswith("DW_FORM_data"): + counts.append(int(count.value)) + elif upper is not None and upper.form.startswith("DW_FORM_data"): + counts.append(int(upper.value) - (int(lower.value) if lower else 0) + 1) + else: + raise UnsupportedType("dynamic/unknown array extent") + if not counts or any(c < 1 for c in counts): + raise UnsupportedType("incomplete array") + result = dwarf_type(die, data_model, depth + 1) + for count in reversed(counts): + result = CppType("array", element=result, count=count) + return result + raise UnsupportedType(f"unsupported DWARF type category: {tag}") + + +def dwarf_location_supported(variable: Any, dwarf: Any) -> None: + """Reject partial/composite/computed locations rather than grade a subset.""" + from elftools.dwarf.dwarf_expr import DWARFExprParser + + attr = variable.attributes.get("DW_AT_location") + if attr is None: + return + parser = DWARFExprParser(dwarf.structs) + if attr.form == "DW_FORM_exprloc": + expressions = [attr.value] + elif attr.form == "DW_FORM_sec_offset": + expressions = [ + e.loc_expr + for e in dwarf.location_lists().get_location_list_at_offset(attr.value, die=variable) + if getattr(e, "loc_expr", None) + ] + else: + raise UnsupportedType(f"unsupported location form: {attr.form}") + for expression in expressions: + ops = parser.parse_expr(expression) + if len(ops) != 1 or not re.fullmatch( + r"DW_OP_(fbreg|reg\d+|regx|breg\d+|bregx|addr)", ops[0].op_name + ): + raise UnsupportedType("partial/composite/computed DWARF location") diff --git a/decbench/metrics/type_match.py b/decbench/metrics/type_match.py index 4d56a02..5f7284d 100644 --- a/decbench/metrics/type_match.py +++ b/decbench/metrics/type_match.py @@ -28,6 +28,7 @@ match_variables, ) from decbench.models.metrics import AggregationType, MetricResult, MetricValue +from decbench.utils.function_identity import parse_function_storage_key from decbench.utils.native_code import die_ranges, entry_address_candidates if TYPE_CHECKING: @@ -301,15 +302,27 @@ def has_line_address_evidence(decompiled: FunctionDecompilation) -> bool: return any(mapping.addresses for mapping in decompiled.line_mappings) +def has_native_address_evidence(decompiled: FunctionDecompilation) -> bool: + """Direct variable PCs are evidence even when no output-line map exists. + + The producer boundary must validate PCs with sanitize_native_provenance. + A variable's definition/use map must never be expanded onto every line + mentioning its name: that would invent per-occurrence provenance. + """ + return has_line_address_evidence(decompiled) or any( + variable.addresses for variable in decompiled.variables + ) + + def extract_ground_truth_type_index(binary_path: Path) -> GroundTruthTypeIndex: """Extract DWARF variable types keyed by function address and name. - Works for **ELF and PE** binaries (the PE/MinGW malware targets) — the DWARF + Works for **ELF, PE/MinGW and thin Mach-O/dSYM** — the DWARF is read via :func:`decbench.utils.binfmt.dwarf_info`, which handles PE's string-table-encoded section names. The DIE-walking below is format-agnostic. Args: - binary_path: Path to an ELF or PE binary compiled with -g + binary_path: Debug-bearing binary, with its adjacent dSYM for Mach-O Returns: Dict mapping function address -> function name -> variable dicts with: @@ -325,12 +338,21 @@ def extract_ground_truth_type_index(binary_path: Path) -> GroundTruthTypeIndex: try: dwarfinfo = binfmt.dwarf_info(binary_path) if dwarfinfo is None: + native = binfmt.pdb_info(binary_path) + if native is not None: + from decbench.utils.pdb_info import ground_truth_variables + + for function in native.functions: + exact = native.at_address(function.address) + if exact is not None: + result.setdefault(function.address, {})[function.name] = ( + ground_truth_variables(binary_path, function, include_addresses=False) + ) logger.debug("No DWARF info in %s", binary_path) return result for CU in dwarfinfo.iter_CUs(): - top_DIE = CU.get_top_DIE() - for DIE in top_DIE.iter_children(): + for DIE in CU.iter_DIEs(): if DIE.tag != "DW_TAG_subprogram": continue @@ -348,6 +370,65 @@ def extract_ground_truth_type_index(binary_path: Path) -> GroundTruthTypeIndex: return result +def extract_ground_truth_types_by_address( + binary_path: Path, +) -> dict[int, list[dict[str, Any]]]: + """Extract DWARF variable ground truth keyed by concrete function address. + + This is the collision-safe companion to :func:`extract_ground_truth_types`. + It intentionally keeps the legacy name-keyed extractor for compatibility, + while allowing C++ overloads and same-named methods to remain distinct. + """ + from decbench.utils import binfmt + + result: dict[int, list[dict[str, Any]]] = {} + + try: + dwarfinfo = binfmt.dwarf_info(binary_path) + if dwarfinfo is None: + native = binfmt.pdb_info(binary_path) + if native is not None: + from decbench.utils.pdb_info import ground_truth_variables + + for function in native.functions: + exact = native.at_address(function.address) + result[function.address] = ( + ground_truth_variables(binary_path, exact, include_addresses=False) + if exact is not None + else [] + ) + return result + + for CU in dwarfinfo.iter_CUs(): + # Walk the whole DIE tree, not just the CU's direct children: a C++ + # DW_TAG_subprogram can sit inside a namespace or class scope, and + # those are exactly the functions this address map exists to keep + # apart. dwarf_function_identities() already walks the full tree, so + # a shallower walk here would name an overload at foo@0x... and then + # silently have no ground truth for that address. + for DIE in CU.iter_DIEs(): + if DIE.tag != "DW_TAG_subprogram" or "DW_AT_low_pc" not in DIE.attributes: + continue + + address = int(DIE.attributes["DW_AT_low_pc"].value) + if not binfmt.dwarf_low_pc_is_concrete(binary_path, address): + continue + _func_name, variables = _parse_function_die(DIE, dwarfinfo) + # Keep the address even when the function has no variables. Its + # empty list is still authoritative and must not fall back to a + # different same-name overload's ground truth. + result[address] = variables + + except Exception as e: + logger.warning( + "Failed to extract address-keyed DWARF types from %s: %s", + binary_path, + e, + ) + + return result + + def extract_ground_truth_types(binary_path: Path) -> dict[str, list[dict[str, Any]]]: """Return the legacy name-only view for functions with an unambiguous name. @@ -389,6 +470,24 @@ def _ground_truth_for_function( return by_name[0] if len(by_name) == 1 else [] +def _ground_truth_for_storage_key( + index: GroundTruthTypeIndex, + by_address: dict[int, list[dict[str, Any]]], + storage_key: str, + decompiled: FunctionDecompilation, + architecture: str, +) -> list[dict[str, Any]]: + """Resolve a storage key without letting an overload borrow another's types.""" + semantic_name, keyed_address = parse_function_storage_key(storage_key) + if keyed_address is not None: + return by_address.get(keyed_address, []) + + function_address = int(decompiled.address) + if function_address in by_address: + return by_address[function_address] + return _ground_truth_for_function(index, semantic_name, function_address, architecture) + + def _parse_function_die(die: Any, dwarfinfo: Any) -> tuple[str | None, list[dict[str, Any]]]: """Parse a DW_TAG_subprogram DIE to extract function variable types. @@ -456,8 +555,8 @@ def _parse_variable_die( if "DW_AT_abstract_origin" in die.attributes: try: - attr = die.attributes["DW_AT_abstract_origin"] - die = dwarfinfo.get_DIE_from_refaddr(attr.value + die.cu.cu_offset) + # DW_FORM_ref_addr is absolute; CU-relative forms have a different base. + die = die.get_DIE_from_attribute("DW_AT_abstract_origin") except Exception: pass @@ -549,9 +648,8 @@ def _parse_type_die(die: Any, dwarfinfo: Any) -> tuple[list[str], int]: if "DW_AT_type" not in die.attributes: return ["void"], 0 - attr = die.attributes["DW_AT_type"] - type_offset = attr.value + die.cu.cu_offset - type_die = dwarfinfo.get_DIE_from_refaddr(type_offset) + # Let the DWARF reader honor absolute, CU-relative and signature references. + type_die = die.get_DIE_from_attribute("DW_AT_type") tag = type_die.tag if tag == "DW_TAG_base_type": @@ -723,6 +821,11 @@ def _find_definition_params(code: str, func_name: str) -> str | None: """ if not func_name: return None + from decbench.metrics.variable_features import function_parameters, index_c_functions + + definitions = index_c_functions(code) + if any("::" in name or name.startswith(("operator", "~")) for name in definitions): + return function_parameters(code, func_name) for m in re.finditer(r"\b" + re.escape(func_name) + r"\s*\(", code): open_i = code.index("(", m.start()) depth = 0 @@ -975,7 +1078,7 @@ class TypeMatchMetric(Metric): display_name = "Type Correctness" description = "Accuracy of variable type recovery vs DWARF ground truth" - cache_version = "17" + cache_version = "25" weight = 1.0 lower_is_better = False @@ -988,6 +1091,7 @@ class TypeMatchMetric(Metric): def __init__(self, config: MetricConfig | None = None): super().__init__(config) self._ground_truth_cache: dict[str, GroundTruthTypeIndex] = {} + self._ground_truth_address_cache: dict[str, dict[int, list[dict[str, Any]]]] = {} self._source_context_key: tuple[Path, str, tuple[Path, ...]] | None = None self._source_context: PreprocessedSourceContext | None = None options = self.config.extra_options @@ -1027,7 +1131,7 @@ def compute_for_function( if address_provenance is None: address_provenance = not uses_legacy_correspondence( backend - ) or has_line_address_evidence(decompiled) + ) or has_native_address_evidence(decompiled) if not address_provenance: return self._legacy_value(decompiled, ground_truth_vars, calibration_shift) @@ -1558,6 +1662,15 @@ def compute_for_binary( binary_info = binfmt.detect(binary_path) architecture = binary_info.arch if binary_info is not None else "" + if ( + binary_info is not None + and binary_info.fmt == "pe" + and binfmt.dwarf_info(binary_path) is None + ): + # No partially supported PDB function may be reported as perfect. + # CodeView formal flags carry no ordinal; the legacy no-address + # fallback would invent parameter order from their record order. + return self._compute_pdb_binary(decompilation) preprocessed_sources = tuple( sorted({Path(path).resolve() for path in (kwargs.get("preprocessed_sources") or [])}) ) @@ -1587,23 +1700,32 @@ def compute_for_binary( gt_types = extract_ground_truth_type_index(binary_path) self._ground_truth_cache[cache_key] = gt_types - if not gt_types: + if cache_key in self._ground_truth_address_cache: + gt_types_by_address = self._ground_truth_address_cache[cache_key] + else: + gt_types_by_address = extract_ground_truth_types_by_address(binary_path) + self._ground_truth_address_cache[cache_key] = gt_types_by_address + + if not gt_types and not gt_types_by_address: logger.warning( "No DWARF ground truth types for %s. " "Binary may not have been compiled with -g.", binary_path, ) - binary_shift = self._calibrate_binary_shift(decompilation, gt_types, architecture) + binary_shift = self._calibrate_binary_shift( + decompilation, gt_types, gt_types_by_address, architecture + ) native_address_provenance = not uses_legacy_correspondence( decompilation.decompiler.decompiler_name ) - for func_name, func_decomp in decompilation.functions.items(): + for storage_key, func_decomp in decompilation.functions.items(): try: - gt_vars = _ground_truth_for_function( + gt_vars = _ground_truth_for_storage_key( gt_types, - func_decomp.name, - int(func_decomp.address), + gt_types_by_address, + storage_key, + func_decomp, architecture, ) if not gt_vars: @@ -1617,13 +1739,13 @@ def compute_for_binary( source_context=source_context, backend=decompilation.decompiler.decompiler_name, address_provenance=( - native_address_provenance or has_line_address_evidence(func_decomp) + native_address_provenance or has_native_address_evidence(func_decomp) ), ) - function_results[func_name] = value + function_results[storage_key] = value except Exception as e: - errors.append(f"{func_name}: {str(e)}") + errors.append(f"{storage_key}: {str(e)}") if gt_types and ( not function_results or all(v.value == 0.0 for v in function_results.values()) @@ -1666,25 +1788,101 @@ def compute_for_binary( return result + def _compute_pdb_binary(self, decompilation: DecompilationResult) -> MetricResult: + import time + + from decbench.utils import binfmt + from decbench.utils.pdb_info import ground_truth_variables + + start = time.time() + values = {} + errors = [] + if self.config.extra_options.get("pdb_abi_diagnostic", False) is not True: + return MetricResult( + metric_name=self.name, + decompiler_name=decompilation.decompiler.decompiler_name, + binary_name=decompilation.binary_name, + function_results={}, + errors=[ + "PDB source-level scoring policy is not qualified; " + "pdb_abi_diagnostic opt-in measures only the ABI projection" + ], + ) + try: + native = binfmt.pdb_info(decompilation.binary_path) + if native is None: + raise ValueError("no matching adjacent PDB") + for key, function in decompilation.functions.items(): + _name, address = parse_function_storage_key(key) + address = address if address is not None else int(function.address) + oracle = native.at_address(address) + if oracle is None: + errors.append(f"{key}: PDB function absent or ICF identity ambiguous") + continue + reasons = sorted( + set( + oracle.exclusions + + [reason for v in oracle.variables for reason in v.exclusions] + ) + ) + if reasons: + errors.append(f"{key}: PDB unmeasurable: {'; '.join(reasons)}") + continue + if not oracle.variables: + errors.append(f"{key}: PDB has no variable ground truth") + continue + if uses_legacy_correspondence( + decompilation.decompiler.decompiler_name + ) and not has_native_address_evidence(function): + errors.append( + f"{key}: PDB requires native address correspondence; " + "parameter ordinals/stack origin are unproven" + ) + continue + gt = ground_truth_variables(decompilation.binary_path, oracle) + value = self.compute_for_function( + function, + ground_truth_vars=gt, + backend=decompilation.decompiler.decompiler_name, + address_provenance=True, + ) + value.metadata["ground_truth_format"] = "pdb" + value.metadata["ground_truth_type_view"] = "abi_projection" + values[key] = value + except Exception as exc: + errors.append(f"PDB oracle unavailable: {exc}") + result = MetricResult( + metric_name=self.name, + decompiler_name=decompilation.decompiler.decompiler_name, + binary_name=decompilation.binary_name, + function_results=values, + computation_time_seconds=time.time() - start, + errors=errors, + ) + result.compute_aggregates(perfect_value=self.perfect_value) + return result + @staticmethod def _calibrate_binary_shift( decompilation: DecompilationResult, gt_types: GroundTruthTypeIndex, + gt_types_by_address: dict[int, list[dict[str, Any]]], architecture: str, ) -> int | None: """Calibrate the offset shift across all functions of a binary. - Gathers per-function (ground-truth, decompiled) stack offset sets and - finds the single additive shift that aligns them best across the - binary. Returns ``None`` when there is nothing to calibrate against. + Address-keyed ground truth is preferred so C++ overloads do not share a + calibration payload. Only unqualified keys may use the unique-name + fallback for old or unusual artifacts where an address cannot be resolved. """ pairs: list[tuple[list[int], list[int]]] = [] - for func_decomp in decompilation.functions.values(): - gt_vars = _ground_truth_for_function( + for storage_key, func_decomp in decompilation.functions.items(): + gt_vars = _ground_truth_for_storage_key( gt_types, - func_decomp.name, - int(func_decomp.address), + gt_types_by_address, + storage_key, + func_decomp, architecture, ) if not gt_vars: diff --git a/decbench/metrics/variable_features.py b/decbench/metrics/variable_features.py index 3342ff1..e3469ee 100644 --- a/decbench/metrics/variable_features.py +++ b/decbench/metrics/variable_features.py @@ -8,9 +8,11 @@ from dataclasses import dataclass import tree_sitter_c +import tree_sitter_cpp from tree_sitter import Language, Node, Parser _C_LANGUAGE = Language(tree_sitter_c.language()) +_CPP_LANGUAGE = Language(tree_sitter_cpp.language()) @dataclass(frozen=True) @@ -26,6 +28,32 @@ def _parser() -> Parser: return Parser(_C_LANGUAGE) +def _parse_source(source: bytes): + """Retain C behavior; use C++ when its declarators require that grammar.""" + c_tree = _parser().parse(source) + cpp_tree = Parser(_CPP_LANGUAGE).parse(source) + if c_tree.root_node.has_error and not cpp_tree.root_node.has_error: + return cpp_tree + if any( + node.type in {"namespace_definition", "class_specifier", "template_declaration"} + and not node.has_error + for node in _walk(cpp_tree.root_node) + ): + return cpp_tree + if not c_tree.root_node.has_error: + return c_tree + cpp_declarators = {"qualified_identifier", "operator_name", "destructor_name"} + for function in _walk(cpp_tree.root_node): + if function.type != "function_definition": + continue + declarator = function.child_by_field_name("declarator") + if declarator is not None and any( + node.type in cpp_declarators for node in _walk(declarator) + ): + return cpp_tree + return c_tree + + def _walk(node: Node) -> Iterator[Node]: yield node for child in node.named_children: @@ -49,7 +77,13 @@ def _field_name(node: Node) -> str | None: def _declarator_identifier(node: Node | None) -> Node | None: if node is None: return None - if node.type == "identifier": + if node.type in { + "identifier", + "field_identifier", + "qualified_identifier", + "operator_name", + "destructor_name", + }: return node declarator = node.child_by_field_name("declarator") if declarator is not None: @@ -65,6 +99,50 @@ def _declarator_identifier(node: Node | None) -> Node | None: def _function_name(node: Node, source: bytes) -> str: identifier = _declarator_identifier(node.child_by_field_name("declarator")) + name = _text(identifier, source) if identifier is not None else "" + if not name or name.startswith("::"): + return name + scopes = [] + parent = node.parent + while parent is not None: + if parent.type in {"namespace_definition", "class_specifier", "struct_specifier"}: + scope = parent.child_by_field_name("name") + if scope is not None: + scopes.append(_text(scope, source)) + parent = parent.parent + prefix = "::".join(reversed(scopes)) + if prefix and not name.startswith(prefix + "::"): + return prefix + "::" + name + return name + + +def function_parameters(code: str, function_name: str) -> str | None: + """Read the real argument list, including operator() and cv/noexcept suffixes.""" + source = code.encode("utf-8", "replace") + tree = _parse_source(source) + candidates = [ + node + for node in _walk(tree.root_node) + if node.type == "function_definition" + and ( + _function_name(node, source) == function_name + or _function_leaf(node, source) == function_name + ) + ] + if len(candidates) != 1: + return None + declarator = candidates[0].child_by_field_name("declarator") + for node in _walk(declarator) if declarator is not None else (): + if node.type == "parameter_list": + text = _text(node, source) + return text[1:-1] + return None + + +def _function_leaf(node: Node, source: bytes) -> str: + identifier = _declarator_identifier(node.child_by_field_name("declarator")) + while identifier is not None and identifier.type == "qualified_identifier": + identifier = identifier.child_by_field_name("name") return _text(identifier, source) if identifier is not None else "" @@ -72,22 +150,51 @@ def index_c_functions(code: str) -> dict[str, tuple[str, ...]]: """Index every named function definition in a translation unit.""" source = code.encode("utf-8", "replace") - tree = _parser().parse(source) + tree = _parse_source(source) definitions: dict[str, list[str]] = defaultdict(list) + aliases: dict[str, set[str]] = defaultdict(set) for node in _walk(tree.root_node): if node.type != "function_definition": continue name = _function_name(node, source) if name: definitions[name].append(_text(node, source)) + aliases[_function_leaf(node, source)].add(name) + for leaf, names in aliases.items(): + if leaf and leaf not in definitions and len(names) == 1: + definitions[leaf] = list(definitions[next(iter(names))]) return {name: tuple(rows) for name, rows in definitions.items()} +def function_definition_at_line(code: str, function_name: str, line: int) -> str | None: + """Select one parsed definition containing a proven original-source line.""" + if line < 1: + return None + source = code.encode("utf-8", "replace") + tree = _parse_source(source) + definitions = [node for node in _walk(tree.root_node) if node.type == "function_definition"] + exact = [node for node in definitions if _function_name(node, source) == function_name] + candidates = exact or [ + node for node in definitions if _function_leaf(node, source) == function_name + ] + containing = [ + node for node in candidates if node.start_point.row < line <= node.end_point.row + 1 + ] + return _text(containing[0], source) if len(containing) == 1 else None + + def _select_function(root: Node, source: bytes, function_name: str) -> Node | None: definitions = [node for node in _walk(root) if node.type == "function_definition"] exact = [node for node in definitions if _function_name(node, source) == function_name] if len(exact) == 1: return exact[0] + if len(exact) > 1: + return None + leaves = [node for node in definitions if _function_leaf(node, source) == function_name] + if len(leaves) == 1: + return leaves[0] + if len(leaves) > 1: + return None return definitions[0] if len(definitions) == 1 else None @@ -185,12 +292,16 @@ def variable_occurrence_lines( """Locate unambiguous local-variable identifiers in exact rendered C.""" source = code.encode("utf-8", "replace") - tree = _parser().parse(source) + tree = _parse_source(source) if require_exact_function_name: exact = [ node for node in _walk(tree.root_node) - if node.type == "function_definition" and _function_name(node, source) == function_name + if node.type == "function_definition" + and ( + _function_name(node, source) == function_name + or _function_leaf(node, source) == function_name + ) ] function = exact[0] if len(exact) == 1 else None else: @@ -228,3 +339,72 @@ def variable_occurrence_lines( occurrences[name].add(bisect_right(line_starts, node.start_byte)) return {name: tuple(sorted(lines)) for name, lines in sorted(occurrences.items())} + + +@dataclass(frozen=True) +class FunctionDefinition: + """One syntactic definition; display-name collisions retain separate rows.""" + + name: str + leaf: str + code: str + start_line: int + end_line: int + has_error: bool + is_template: bool + + +def function_definitions(code: str) -> tuple[FunctionDefinition, ...]: + """Retain source spans for exact native-origin joins, without name reduction.""" + source = code.encode("utf-8", "replace") + parser = Parser(_CPP_LANGUAGE) + tree = parser.parse(source) + line_breaks = [index for index, byte in enumerate(source) if byte == 10] + definitions = [] + for node in _definition_nodes(tree.root_node): + if node.type != "function_definition": + continue + parent = node.parent + template = False + scope_error = False + while parent is not None: + template |= parent.type == "template_declaration" + if parent.type in {"namespace_definition", "class_specifier", "struct_specifier"}: + name = parent.child_by_field_name("name") + body = parent.child_by_field_name("body") + scope_error |= name is not None and name.has_error + end = ( + body.child(body.child_count - 1) + if body is not None and body.child_count + else None + ) + scope_error |= end is None + if end is not None: + scope_error |= end.type != "}" or end.is_missing + parent = parent.parent + definitions.append( + FunctionDefinition( + _function_name(node, source), + _function_leaf(node, source), + _text(node, source), + bisect_right(line_breaks, node.start_byte) + 1, + bisect_right(line_breaks, max(node.start_byte, node.end_byte - 1)) + 1, + node.has_error or scope_error, + template, + ) + ) + return tuple(definitions) + + +def _definition_nodes(root: Node) -> Iterator[Node]: + cursor = root.walk() + while True: + node = cursor.node + is_definition = node.type == "function_definition" + if is_definition: + yield node + if not is_definition and cursor.goto_first_child(): + continue + while not cursor.goto_next_sibling(): + if not cursor.goto_parent(): + return diff --git a/decbench/metrics/variable_match.py b/decbench/metrics/variable_match.py index 31ac2d2..c81bb65 100644 --- a/decbench/metrics/variable_match.py +++ b/decbench/metrics/variable_match.py @@ -5,7 +5,7 @@ import bisect import contextlib import re -from collections import defaultdict +from collections import Counter, defaultdict from collections.abc import Iterable, Mapping from dataclasses import dataclass, field from pathlib import Path @@ -692,19 +692,29 @@ def extract_decompiler_evidence( include_unnamed: bool = False, ) -> list[VariableEvidence]: evidence_prefix = backend if identity_prefix is None else identity_prefix - line_addresses = { - int(mapping.line_number): frozenset(int(address) for address in mapping.addresses) - for mapping in (getattr(function, "line_mappings", []) or []) - } + mappings = getattr(function, "line_mappings", []) or [] + recovered = getattr(function, "variables", []) or [] + name_counts = Counter(variable.name for variable in recovered) variables: list[VariableEvidence] = [] - for index, variable in enumerate(getattr(function, "variables", []) or []): + for index, variable in enumerate(recovered): if not variable.name and not include_unnamed: continue lines = {int(line) for line in getattr(variable, "line_numbers", [])} addresses = {int(address) for address in getattr(variable, "addresses", [])} if not addresses: addresses = { - address for line in lines for address in line_addresses.get(line, frozenset()) + int(address) + for mapping in mappings + if int(mapping.line_number) in lines + and ( + getattr(mapping, "defined_variables", None) is None + or ( + bool(variable.name) + and name_counts[variable.name] == 1 + and variable.name in mapping.defined_variables + ) + ) + for address in mapping.addresses } stack_offsets = (int(variable.stack_offset),) if variable.stack_offset is not None else () variables.append( diff --git a/decbench/models/decompilation.py b/decbench/models/decompilation.py index c9b1221..0ae6dac 100644 --- a/decbench/models/decompilation.py +++ b/decbench/models/decompilation.py @@ -3,9 +3,19 @@ from __future__ import annotations from pathlib import Path -from typing import Any +from typing import Any, Literal -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field, SerializerFunctionWrapHandler, model_serializer + + +class ProducerLanguageContract(BaseModel): + """Producer assertions about text, never a certificate of source validity.""" + + model_config = ConfigDict(extra="forbid", frozen=True) + version: Literal["producer-language-v1"] = "producer-language-v1" + surface: Literal["c", "cpp", "c-like-pseudocode", "unknown"] = "unknown" + declarations: Literal["partial", "complete", "unknown"] = "unknown" + dialect: str | None = None class LineMapping(BaseModel): @@ -17,6 +27,21 @@ class LineMapping(BaseModel): description="Binary addresses corresponding to this line", ) + defined_variables: list[str] | None = Field( + default=None, + description=( + "Optional occurrence scope: only variables with these nonempty, " + "function-unique display names own these PCs" + ), + ) + + @model_serializer(mode="wrap") + def serialize_mapping(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]: + data: dict[str, Any] = handler(self) + if self.defined_variables is None: + data.pop("defined_variables", None) + return data + class VariableInfo(BaseModel): """Structured variable info recovered by a decompiler. @@ -126,7 +151,10 @@ class DecompilationResult(BaseModel): functions: dict[str, FunctionDecompilation] = Field( default_factory=dict, - description="Decompilation results keyed by function name", + description=( + "Decompilation results keyed by plain function name while unique; " + "same-name/different-address collisions may use an address-qualified key" + ), ) combined_source: str | None = Field( @@ -147,11 +175,28 @@ def function_count(self) -> int: def successful_count(self) -> int: return len(self.functions) - len(self.decompiler.failed_functions) + def add_function(self, function: FunctionDecompilation) -> str: + """Insert one function without collapsing a C++ name collision. + + Non-colliding functions keep the historical plain-name key. If another + function with the same name but a different address is inserted, all + colliding entries are re-keyed with their binary-local addresses. + + Returns the storage key used for ``function``. + """ + from decbench.utils.function_identity import insert_function + + return insert_function(self.functions, function) + + def functions_named(self, name: str) -> list[FunctionDecompilation]: + """Return every stored function whose semantic name equals ``name``.""" + return [function for function in self.functions.values() if function.name == name] + def to_c_file(self, path: Path) -> None: """Write combined decompilation to a C file.""" with open(path, "w") as f: - for func in self.functions.values(): - f.write(f"// Function: {func.name} @ 0x{func.address:x}\n") + for storage_key, func in self.functions.items(): + f.write(f"// Function: {storage_key} @ 0x{func.address:x}\n") f.write(func.decompiled_code) f.write("\n\n") @@ -159,7 +204,7 @@ def to_toml(self, path: Path) -> None: """Save decompilation result metadata to TOML.""" import toml - data = { + data: dict[str, Any] = { "binary": self.binary_name, "decompiler": self.decompiler.decompiler_name, "version": self.decompiler.decompiler_version, @@ -188,7 +233,7 @@ def to_toml(self, path: Path) -> None: @classmethod def from_toml(cls, path: Path) -> DecompilationResult: - """Load decompilation result from TOML (metadata only).""" + """Load decompilation result metadata from TOML (metadata only).""" import toml data = toml.load(path) diff --git a/decbench/pipeline/cpp_ged.py b/decbench/pipeline/cpp_ged.py new file mode 100644 index 0000000..b4a598d --- /dev/null +++ b/decbench/pipeline/cpp_ged.py @@ -0,0 +1,136 @@ +"""Opt-in C++ GED connection diagnostics, separate from default metrics/Union.""" + +from __future__ import annotations + +import hashlib +import math +from collections import Counter +from pathlib import Path + +from pydantic import BaseModel, Field + +from decbench.metrics.ged import GEDMetric +from decbench.models.decompilation import DecompilationResult, FunctionDecompilation +from decbench.models.metrics import MetricResult +from decbench.utils.cfg_identity import ( + NativeSourceIndex, + SourceCfgIndex, + decompiled_cfgs, + native_sources, +) + + +class CppGedEvaluation(BaseModel): + """Coverage is explicit; missing CFGs are never silently perfect scores.""" + + profile: str = "cpp-ged-origin-v3-diagnostic" + official_score: bool = False + requested: int + compilation_database_sha256: str | None = None + input_binary_sha256: str + producer_code_sha256: dict[str, str] + source_eligible: int + decompiled_connected: int + paired: int + source_statuses: dict[str, int] + decompiled_statuses: dict[str, int] + source_evidence: dict[str, dict] = Field(default_factory=dict) + decompiled_evidence: dict[str, dict] = Field(default_factory=dict) + source_inputs: list[dict] = Field(default_factory=list) + bounds: list[str] = Field( + default_factory=lambda: [ + "Isolated definition CFG; full-TU callee context is not qualified", + "No template specialization or implicit lifetime/exception-edge qualification", + "Compilation database is asserted provenance, not independent link qualification", + "Diagnostic only; no corpus qualification, default GED or Union score", + "Producer CFG connection does not establish C++ validity; bounded defects reject", + ] + ) + metric: MetricResult + + +def evaluate_cpp_ged( + decompilation: DecompilationResult, + preprocessed_sources: list[Path] | None = None, + *, + source_index: SourceCfgIndex | None = None, + native_index: NativeSourceIndex | None = None, + requested_functions: list[tuple[str, int]] | None = None, + compilation_database: Path | None = None, +) -> CppGedEvaluation: + """Join exact native entries, original locations and producer-record CFGs. + + Source CFGs are strictly parsed isolated definitions. Template specialization, + implicit object/exception effects and full-TU callee context are not qualified + by this diagnostic profile. It does not alter default GED or its arithmetic. + """ + if requested_functions is not None: + addresses = {address for _, address in requested_functions} + if len(addresses) != len(requested_functions) or any(address < 0 for address in addresses): + raise ValueError("requested entries must have distinct nonnegative addresses") + working = decompilation.model_copy(deep=True) + working.functions = { + key: f for key, f in working.functions.items() if f.address in addresses + } + returned = [f.address for f in working.functions.values()] + if len(returned) != len(set(returned)): + raise ValueError("ambiguous producer records at one requested address") + for name, address in requested_functions: + if address not in returned: + working.add_function( + FunctionDecompilation(name=name, address=address, decompiled_code="") + ) + decompilation = working + if any(path.suffix != ".ii" for path in preprocessed_sources or []): + raise ValueError("C++ GED diagnostics require preprocessed .ii inputs") + if native_index is None: + native_index = native_sources( + decompilation.binary_path, + {function.address for function in decompilation.functions.values()}, + ) + if compilation_database is not None: + native_index = native_index.with_compilation_database(compilation_database) + native_index.validate(decompilation.binary_path) + if source_index is None: + source_index = SourceCfgIndex() + records = tuple(rows[0] for rows in native_index.values() if len(rows) == 1) + for path in preprocessed_sources or []: + data = path.read_bytes() + source_index.add(data.decode("utf-8", "replace"), str(path), native_records=records) + source_index.inputs[-1]["sha256"] = hashlib.sha256(data).hexdigest() + source = source_index.resolve(decompilation, native_index) + decompiled = decompiled_cfgs(decompilation) + metric = MetricResult( + metric_name="cpp_ged_diagnostic", + decompiler_name=decompilation.decompiler.decompiler_name, + binary_name=decompilation.binary_name, + ) + evaluator = GEDMetric() + for key, function in decompilation.functions.items(): + if key not in source.cfgs or key not in decompiled.cfgs: + continue + value = evaluator.compute_for_function( + function, source_cfg=source.cfgs[key], decompiled_cfg=decompiled.cfgs[key] + ) + if not math.isfinite(value.value): + raise RuntimeError(f"GED evaluator failed for {key}: {value.metadata}") + metric.function_results[key] = value + metric.compute_aggregates() + return CppGedEvaluation( + requested=len(decompilation.functions), + input_binary_sha256=native_index.binary_sha256, + compilation_database_sha256=native_index.compilation_database_sha256, + producer_code_sha256={ + key: hashlib.sha256(f.decompiled_code.encode("utf-8")).hexdigest() + for key, f in decompilation.functions.items() + }, + source_eligible=len(source.cfgs), + decompiled_connected=len(decompiled.cfgs), + paired=len(source.cfgs.keys() & decompiled.cfgs.keys()), + source_statuses=dict(Counter(row["status"] for row in source.evidence.values())), + decompiled_statuses=dict(Counter(row["status"] for row in decompiled.evidence.values())), + source_evidence=source.evidence, + decompiled_evidence=decompiled.evidence, + source_inputs=source_index.inputs, + metric=metric, + ) diff --git a/decbench/pipeline/materialized.py b/decbench/pipeline/materialized.py index 0e6bb24..758ca33 100644 --- a/decbench/pipeline/materialized.py +++ b/decbench/pipeline/materialized.py @@ -25,7 +25,6 @@ from __future__ import annotations import logging -import re from pathlib import Path from typing import TYPE_CHECKING @@ -34,17 +33,19 @@ DecompilerMetadata, FunctionDecompilation, ) -from decbench.utils.results_tree import OPT_LEVELS, compiled_dir, resolve_binary +from decbench.utils.function_identity import parse_function_storage_key +from decbench.utils.results_tree import ( + OPT_LEVELS, + compiled_dir, + resolve_binary, + split_functions, +) if TYPE_CHECKING: from networkx import DiGraph logger = logging.getLogger(__name__) -# Same marker the decompile stage writes and rebuild_function_data.py parses. -MARKER = re.compile(r"^// Function: (\S+) @ (0x[0-9a-fA-F]+)\s*$", re.M) - - def load_decompilation( c_path: Path, dec_name: str, @@ -54,18 +55,21 @@ def load_decompilation( Function bodies come from the marker-delimited ``.c``; version/timing/failed functions come from the sibling ``.toml`` when present (leniently parsed). + + Markers are read through :func:`~decbench.utils.results_tree.split_functions` + rather than a second local regex, so this path sees exactly what the writer + produced: collision-qualified keys such as ``same@0x1000``, and DWARF names + containing spaces such as ``operator new``. The storage key becomes the + dictionary key while ``FunctionDecompilation.name`` keeps the *semantic* + name -- ByteMatch feeds that field to ``function_bytes`` and to the compile + fixup as a real C identifier, so an address-qualified key must not reach it. """ - text = c_path.read_text(errors="replace") functions: dict[str, FunctionDecompilation] = {} - marks = list(MARKER.finditer(text)) - for i, m in enumerate(marks): - start = m.end() - end = marks[i + 1].start() if i + 1 < len(marks) else len(text) - code = text[start:end].strip() - name = m.group(1) - functions[name] = FunctionDecompilation( - name=name, - address=int(m.group(2), 16), + for storage_key, (address, code) in split_functions(c_path).items(): + semantic_name, _keyed_address = parse_function_storage_key(storage_key) + functions[storage_key] = FunctionDecompilation( + name=semantic_name, + address=address, decompiled_code=code, line_count=len(code.splitlines()), ) diff --git a/decbench/scoring/report_extras.py b/decbench/scoring/report_extras.py index cbe4e4d..77955a7 100644 --- a/decbench/scoring/report_extras.py +++ b/decbench/scoring/report_extras.py @@ -284,6 +284,36 @@ def _lookup_binary_path( return None +def _lookup_function_by_storage_key( + decompile_results: Any, + project: str, + opt_level: Any, + binary: str, + storage_key: str, +) -> Any: + """Resolve a serialized metric/storage key back to its semantic function.""" + if not decompile_results: + return None + try: + opt_results = decompile_results.get(project) or {} + binary_results = opt_results.get(opt_level) + if binary_results is None: + ov = _opt_value(opt_level) + for key, val in opt_results.items(): + if _opt_value(key) == ov: + binary_results = val + break + if not binary_results: + return None + for dec_result in (binary_results.get(binary) or {}).values(): + func = (getattr(dec_result, "functions", None) or {}).get(storage_key) + if func is not None: + return func + except Exception: + return None + return None + + def _lookup_source_ex( decompile_results: Any, project: str, @@ -291,14 +321,35 @@ def _lookup_source_ex( binary: str, func_name: str, ) -> tuple[str | None, str]: - """Best-effort source text + provenance/miss status for one function.""" + """Best-effort source text + provenance/miss status for one function. + + ``func_name`` is the benchmark storage key. For collision-qualified C++ + keys, recover the semantic source name and canonical address before asking + the source extractor to disambiguate DWARF. + """ bp = _lookup_binary_path(decompile_results, project, opt_level, binary) if bp is None: return None, "binary_not_found" try: + from decbench.utils.function_identity import parse_function_storage_key from decbench.utils.source_extract import function_source_ex - return function_source_ex(Path(bp), func_name) + func = _lookup_function_by_storage_key( + decompile_results, project, opt_level, binary, func_name + ) + semantic_name, keyed_address = parse_function_storage_key(func_name) + if keyed_address is not None: + source_name = getattr(func, "name", semantic_name) + source_address = getattr(func, "address", keyed_address) + return function_source_ex(Path(bp), source_name, source_address) + + # Preserve the historical name-only path for unique/plain keys. Some + # imported or synthetic results carry no semantic metadata at all, or a + # backend address that is not a usable DWARF low_pc; address + # disambiguation is required only when the storage key itself is + # collision-qualified. + source_name = getattr(func, "name", semantic_name) + return function_source_ex(Path(bp), source_name) except Exception: return None, "extract_failed" diff --git a/decbench/utils/binfmt.py b/decbench/utils/binfmt.py index b53cc65..87c52e1 100644 --- a/decbench/utils/binfmt.py +++ b/decbench/utils/binfmt.py @@ -1,20 +1,21 @@ """Binary-format helpers shared by the type_match and byte_match metrics. -Lets the metrics work on **PE** (MinGW-built Windows malware) as well as **ELF**, +Lets DWARF consumers read **ELF**, **PE/MinGW** and **thin Mach-O/dSYM**, and — critically for byte_match — recompile the decompiled code *the same way the source was compiled*: with the toolchain and arch/opt flags that match the original binary's own format and architecture. What's here: - * :func:`detect` — format (elf/pe/coff) + arch of a binary. + * :func:`detect` — format (elf/pe/macho) + arch of a linked binary. * :func:`recompiler_for` / :func:`producer_flags` — the matching compiler and the original `-m*/-O*` flags (from the DWARF producer), so a recompile is "the same way as source"; :func:`tool_available` gates on it being installed. * :func:`capstone_arch_mode` — the right capstone arch for disassembly. - * :func:`dwarf_info` / :func:`pe_dwarf_info` — a pyelftools ``DWARFInfo`` for an - ELF *or* a PE (PE: read the `.debug_*` sections via objdump file offsets and + * :func:`dwarf_info` / :func:`pe_dwarf_info` / :func:`mach_o_dwarf_info` — + self-contained ``DWARFInfo``; dSYM companions require matching UUIDs. + For PE, read the `.debug_*` sections via objdump file offsets and build the DWARFInfo by hand — LIEF's community build has no DWARF reader and - PE COFF long section names defeat name lookups). + PE COFF long section names defeat name lookups. * :func:`function_bytes` / :func:`object_text_bytes` — original function bytes from a final ELF/PE, and the `.text` of a single-function recompiled object (ELF or COFF), for byte_match. @@ -58,6 +59,20 @@ 0x1C2: "arm", 0x1C4: "arm", } +_MACH_MAGICS = { + b"\xce\xfa\xed\xfe": ("<", 32), + b"\xcf\xfa\xed\xfe": ("<", 64), + b"\xfe\xed\xfa\xce": (">", 32), + b"\xfe\xed\xfa\xcf": (">", 64), +} +_MACH_CPUS = { + 7: "x86", + 0x1000007: "x86-64", + 12: "arm", + 0x100000C: "aarch64", + 18: "ppc", + 0x1000012: "ppc64", +} @dataclass @@ -71,6 +86,12 @@ def detect(path: Path) -> BinInfo | None: """Detect (format, arch, bits) of a linked binary, or None if unrecognized.""" try: with open(path, "rb") as f: + magic = f.read(4) + if magic in _MACH_MAGICS: + endian, bits = _MACH_MAGICS[magic] + cpu = struct.unpack(endian + "I", f.read(4))[0] + return BinInfo("macho", _MACH_CPUS.get(cpu, "other"), bits) + f.seek(0) head = f.read(2) if head == b"\x7fE": f.seek(0) @@ -113,22 +134,26 @@ def detect(path: Path) -> BinInfo | None: } -def recompiler_for(info: BinInfo) -> str | None: +def recompiler_for(info: BinInfo, path: Path | None = None) -> str | None: """The compiler that builds for this binary's format+arch (the 'same way'). Returns the compiler executable name, or None for arch/format we can't recompile to. Callers should also check :func:`tool_available`. - Bare ``gcc`` is the answer only where the host builds that architecture - natively. Elsewhere it is the cross triplet, so a corpus built for a + Bare ``gcc`` is selected only on a Linux host with a native architecture. + Elsewhere it is the cross triplet, so a corpus built for a different architecture than the host abstains (no toolchain) instead of recompiling every function with the host's own ``-march``. """ if info.fmt == "elf": - if info.arch in _HOST_NATIVE_ARCHES.get(platform.machine().lower(), ()): + if platform.system() == "Linux" and info.arch in _HOST_NATIVE_ARCHES.get( + platform.machine().lower(), () + ): return "gcc" return _CROSS_ELF_GCC.get(info.arch) if info.fmt == "pe": + if path is not None and dwarf_info(path) is None: + return None return { "x86": "i686-w64-mingw32-gcc", "x86-64": "x86_64-w64-mingw32-gcc", @@ -136,10 +161,126 @@ def recompiler_for(info: BinInfo) -> str | None: return None +def native_debug_paths(path: Path) -> tuple[Path, ...]: + """Candidate external debug files used by the default native readers. + + Missing canonical companions remain inputs: appearing later invalidates + preparation too. No debug search path or arbitrary file discovery is added. + """ + info = detect(path) + if info is not None and info.fmt == "macho": + image = _mach_o_binary(path) + if image is not None and any( + section.segment_name == "__DWARF" and section.name == "__debug_info" and section.content + for section in image.sections + ): + return () + companion = path.with_name(path.name + ".dSYM") + if companion.is_dir(): + companion = companion / "Contents" / "Resources" / "DWARF" / path.name + return (companion,) + if info is not None and info.fmt == "pe": + if pe_dwarf_info(path) is not None: + return () + from decbench.utils.pdb_info import pdb_path + + pdb_companion = pdb_path(path) + return (pdb_companion,) if pdb_companion is not None else () + return () + + +def _file_version(path: Path) -> tuple[int, int, int, int, int] | None: + try: + stat = path.stat() + except FileNotFoundError: + return None + return stat.st_dev, stat.st_ino, stat.st_size, stat.st_mtime_ns, stat.st_ctime_ns + + +@dataclass(frozen=True) +class NativeDebugSnapshot: + """Filesystem-version guard for reused external debug preparation. + + This is not a cryptographic identity or a replacement for UUID/GUID checks. + """ + + files: tuple[tuple[Path, tuple[int, int, int, int, int] | None], ...] + + @classmethod + def capture(cls, binary: Path) -> NativeDebugSnapshot: + return cls(tuple((path, _file_version(path)) for path in native_debug_paths(binary))) + + def validate(self) -> None: + if any(_file_version(path) != version for path, version in self.files): + raise ValueError("native debug inputs changed after oracle preparation") + + +def pdb_info(path: Path, *, debug_path: Path | None = None): + """Native PE oracle transport; mismatches are errors, absence is None.""" + info = detect(path) + if info is None or info.fmt != "pe": + return None + from decbench.utils.pdb_info import read_pdb + + return read_pdb(path, debug_path) + + def tool_available(name: str) -> bool: return shutil.which(name) is not None +def executable_address_status(path: Path, address: int) -> bool | None: + """Whether ``address`` belongs to a concrete executable ELF/Mach-O section. + + ``False`` is returned only when executable sections are available and none + contains the address. ``None`` means the format/section table could not + provide a reliable answer. + """ + info = detect(path) + if info is not None and info.fmt == "macho": + regions = executable_regions(path) + return ( + any(start <= address < start + len(data) for start, data in regions) + if regions + else None + ) + if info is None or info.fmt != "elf": + return None + try: + from elftools.elf.elffile import ELFFile + + with path.open("rb") as stream: + elf = ELFFile(stream) + saw_executable = False + for section in elf.iter_sections(): + if section.header["sh_type"] == "SHT_NOBITS": + continue + if not int(section.header["sh_flags"]) & 0x4: # SHF_EXECINSTR + continue + size = int(section.header["sh_size"]) + if size <= 0: + continue + saw_executable = True + start = int(section.header["sh_addr"]) + if start <= address < start + size: + return True + return False if saw_executable else None + except Exception: + return None + + +def dwarf_low_pc_is_concrete(path: Path, address: int) -> bool: + """Whether a DWARF ``low_pc`` should be treated as a concrete code address. + + Linker ICF can leave a superseded subprogram DIE with ``low_pc == 0`` even + though normal linked ELF code starts elsewhere. Reject zero only when the + ELF section table proves that address zero is not executable. If the image + genuinely has executable code at zero (embedded/bare-metal) or the format + cannot answer reliably, preserve the historical behavior. + """ + return address != 0 or executable_address_status(path, 0) is not False + + # Codegen-relevant flags carried over from the original build (never -g). Only # codegen-only, header-independent flags: dropping them made byte_match # unwinnable for whole projects (e.g. -fzero-call-used-regs, -fomit-frame-pointer). @@ -197,7 +338,7 @@ def capstone_arch_mode( def executable_regions(path: Path) -> tuple[tuple[int, bytes], ...]: - """Return executable virtual-address regions from an ELF or PE binary.""" + """Return file-backed executable regions from ELF, PE or thin Mach-O.""" info = detect(path) if info is None: return () @@ -232,6 +373,17 @@ def executable_regions(path: Path) -> tuple[tuple[int, bytes], ...]: return tuple(sorted(regions)) except Exception: return () + if info.fmt == "macho": + binary = _mach_o_binary(path) + if binary is None: + return () + return tuple( + sorted( + (int(section.virtual_address), bytes(section.content)) + for section in binary.sections + if int(section.flags) & (0x80000000 | 0x400) and section.content + ) + ) return () @@ -410,8 +562,87 @@ def pe_dwarf_info(path: Path): return _build_dwarfinfo(secs, little_endian=True, addr_size=addr_size, march=march) -def dwarf_info(path: Path): - """Return a pyelftools DWARFInfo for an ELF or PE binary, or None. +def _mach_o_binary(path: Path): + """Read one thin Mach-O without implicitly choosing a universal slice.""" + info = detect(path) + if info is None or info.fmt != "macho": + return None + try: + import lief + + image = lief.MachO.parse(str(path)) + return image[0] if image is not None and len(image) == 1 else None + except Exception: + return None + + +def mach_o_image_base(path: Path) -> int | None: + """Linked Mach-O load base, excluding unmapped __PAGEZERO segments.""" + binary = _mach_o_binary(path) + if binary is None: + return None + bases = [int(segment.virtual_address) for segment in binary.segments if segment.file_size] + return min(bases) if bases else None + + +def mach_o_dwarf_info(path: Path, *, debug_path: Path | None = None): + """Read embedded DWARF or a UUID/architecture-matched dSYM companion. + + Without an explicit debug path, only the canonical adjacent dSYM bundle + is considered. A dSYM belongs to the evaluator; this does not copy it next + to a stripped producer input or search unrelated debug files. + """ + image = _mach_o_binary(path) + if image is None: + return None + info = detect(path) + + def sections(binary): + result = {} + for section in binary.sections: + if section.segment_name != "__DWARF" or not section.name.startswith("__debug_"): + continue + name = "." + section.name[2:] + if name == ".debug_str_offs": + name = ".debug_str_offsets" + if name in _DWARF_SECS: + result[name] = bytes(section.content) + return result + + secs = sections(image) + if not secs.get(".debug_info"): + companion = debug_path or path.with_name(path.name + ".dSYM") + if companion.is_dir(): + companion = companion / "Contents" / "Resources" / "DWARF" / path.name + debug = _mach_o_binary(companion) + if debug is None or detect(companion) != info: + return None + if ( + not image.has_uuid + or not debug.has_uuid + or bytes(image.uuid.uuid) != bytes(debug.uuid.uuid) + ): + return None + secs = sections(debug) + if not secs.get(".debug_info") or not secs.get(".debug_abbrev"): + return None + with path.open("rb") as stream: + endian, bits = _MACH_MAGICS[stream.read(4)] + march = { + "x86": "x86", + "x86-64": "x64", + "arm": "ARM", + "aarch64": "AArch64", + "ppc": "PowerPC", + "ppc64": "PowerPC64", + }.get(info.arch) + if march is None: + return None + return _build_dwarfinfo(secs, endian == "<", bits // 8, march) + + +def dwarf_info(path: Path, *, debug_path: Path | None = None): + """Return self-contained DWARF from ELF, PE or UUID-matched Mach-O/dSYM. For ELF, sections are read into memory so the result is self-contained (no dependence on an open file handle). @@ -421,6 +652,8 @@ def dwarf_info(path: Path): return None if info.fmt == "pe": return pe_dwarf_info(path) + if info.fmt == "macho": + return mach_o_dwarf_info(path, debug_path=debug_path) try: from elftools.elf.elffile import ELFFile @@ -576,6 +809,30 @@ def source_function_owners( except Exception: # noqa: BLE001 return {} if dw is None: + native = pdb_info(path) + if native is not None: + owners = {} + stem_index = build_stem_index(source_stems) + for function in native.functions: + if not function.source_file: + continue + decl_stem = strip_source_ext( + function.source_file.replace("\\", "/").rsplit("/", 1)[-1] + ) + matches = [ + original + for normalized, original in stem_index.items() + if normalized == decl_stem + or normalized.endswith("-" + decl_stem) + or normalized.endswith("_" + decl_stem) + ] + if len(set(matches)) == 1: + value = (function.name, matches[0]) + if function.address in owners and owners[function.address] != value: + owners[function.address] = None + else: + owners.setdefault(function.address, value) + return {address: owner for address, owner in owners.items() if owner is not None} return {} owners: dict[int, tuple[str, str]] = {} @@ -586,6 +843,9 @@ def source_function_owners( for die in cu.iter_DIEs(): if die.tag != "DW_TAG_subprogram" or "DW_AT_low_pc" not in die.attributes: continue + address = int(die.attributes["DW_AT_low_pc"].value) + if not dwarf_low_pc_is_concrete(path, address): + continue name = die_str_attr( die, "DW_AT_name", follow_abstract_origin=follow_abstract_origin ) @@ -610,32 +870,46 @@ def source_function_owners( ] matched = suffix_matches[0] if len(suffix_matches) == 1 else None if matched is not None: - owners[int(die.attributes["DW_AT_low_pc"].value)] = (name, matched) + owners[address] = (name, matched) except Exception: # noqa: BLE001 return {} return owners -def _dwarf_function_range(path: Path, func_name: str) -> tuple[int, int] | None: - """(low_pc, high_pc) absolute VA for a function, from DWARF.""" +def _dwarf_function_range( + path: Path, func_name: str, address: int | None = None +) -> tuple[int, int] | None: + """``(low_pc, high_pc)`` for one function, by exact address when given. + + An ``address`` identifies exactly one subprogram, so it is matched or the + lookup fails: falling back to the first DIE sharing the short name would + hand back a *different* overload, which is the collision this module exists + to prevent. The name-only search runs only when no address is supplied, as + the historical compatibility path. + """ di = dwarf_info(path) if di is None: return None + name_fallback: tuple[int, int] | None = None for cu in di.iter_CUs(): for die in cu.iter_DIEs(): if die.tag != "DW_TAG_subprogram" or "DW_AT_low_pc" not in die.attributes: continue - nm = die.attributes.get("DW_AT_name") - name = nm.value.decode() if nm and isinstance(nm.value, bytes) else None - if name != func_name: + lo = int(die.attributes["DW_AT_low_pc"].value) + if not dwarf_low_pc_is_concrete(path, lo): continue - lo = die.attributes["DW_AT_low_pc"].value hi_at = die.attributes.get("DW_AT_high_pc") if hi_at is None: - return None - hi = lo + hi_at.value if hi_at.form != "DW_FORM_addr" else hi_at.value - return (lo, hi) - return None + continue + hi = lo + int(hi_at.value) if hi_at.form != "DW_FORM_addr" else int(hi_at.value) + rng = (lo, hi) + if address is not None: + if lo == address: + return rng + continue + if name_fallback is None and die_str_attr(die, "DW_AT_name") == func_name: + name_fallback = rng + return None if address is not None else name_fallback def function_bytes(path: Path, func_name: str, address: int) -> bytes | None: @@ -647,7 +921,7 @@ def function_bytes(path: Path, func_name: str, address: int) -> bytes | None: b = _elf_function_bytes(path, func_name, address) if b is not None: return b - rng = _dwarf_function_range(path, func_name) + rng = _dwarf_function_range(path, func_name, address) if rng is None: return None lo, hi = rng @@ -690,6 +964,45 @@ def _elf_function_bytes(path: Path, func_name: str, address: int) -> bytes | Non return None +def object_info(obj_path: Path) -> BinInfo | None: + """Identify a compiled ELF/Mach-O or ordinary COFF object before byte comparison. + + COFF objects use the PE target family but have no DOS/PE image wrapper. + Compiler executable names alone do not prove the format or architecture. + """ + info = detect(obj_path) + if info is not None: + with obj_path.open("rb") as stream: + header = stream.read(20) + if info.fmt == "elf": + if len(header) < 20 or header[4] not in {1, 2} or header[5] not in {1, 2}: + return None + endian = "<" if header[5] == 1 else ">" + if struct.unpack(endian + "H", header[16:18])[0] != 1: + return None + info = BinInfo("elf", info.arch, 32 if header[4] == 1 else 64) + elif info.fmt == "macho": + endian, _bits = _MACH_MAGICS[header[:4]] + if len(header) < 16 or struct.unpack(endian + "I", header[12:16])[0] != 1: + return None + else: + return None + return info + with obj_path.open("rb") as stream: + header = stream.read(20) + if len(header) < 20: + return None + machine, sections, _time, _symbols, _count, optional_size, _flags = struct.unpack( + " bytes | None: """`.text` of a recompiled single-function object (ELF .o or COFF .o). diff --git a/decbench/utils/cfg_identity.py b/decbench/utils/cfg_identity.py new file mode 100644 index 0000000..07585d1 --- /dev/null +++ b/decbench/utils/cfg_identity.py @@ -0,0 +1,571 @@ +"""Experimental native-address/source-origin joins for C++ GED. + +This index retains definitions instead of reducing them by display name. Failed +or ambiguous evidence never falls back to a same-named function in another TU. +""" + +from __future__ import annotations + +import hashlib +import re +from collections import defaultdict +from dataclasses import asdict, dataclass, field, replace +from pathlib import Path +from typing import Any + +from networkx import DiGraph + +from decbench.metrics.variable_features import FunctionDefinition, function_definitions +from decbench.models.decompilation import DecompilationResult +from decbench.utils import binfmt +from decbench.utils.cfg import ( + _is_system_header, + is_degenerate_source_cfg, + preprocess_decompiled_c, + sanitize_decompiled_c, +) +from decbench.utils.compilation_units import ( + CompilationUnitBinding, + compilation_path, + saved_temp_units, +) +from decbench.utils.native_code import die_ranges +from decbench.utils.producer_validation import inspect_producer + +_MARKER = re.compile(r'^\s*#\s*(?:line\s+)?(\d+)\s+"((?:\\.|[^"\\])*)"(.*)$') + + +def source_path(path: str) -> str: + """Normalize separators without interpreting C escapes as Unicode escapes.""" + return compilation_path(path) + + +def _text(value: Any) -> str: + return value.decode("utf-8", "replace") if isinstance(value, bytes) else str(value) + + +def _absolute(path: str, directory: str) -> str: + return compilation_path(path, directory) + + +@dataclass(frozen=True) +class SourceLocation: + path: str + line: int + + +@dataclass +class OriginSource: + """Physical lines remain aligned with the preprocessed input.""" + + text: str + origins: dict[int, SourceLocation] + primary: str | None + filtered_paths: frozenset[str] = frozenset() + + +def origin_source(text: str, *, retain_paths: frozenset[str] = frozenset()) -> OriginSource: + lines: list[str] = [] + origins: dict[int, SourceLocation] = {} + current = "" + logical = 0 + system = True + primary = None + filtered: set[str] = set() + for physical, line in enumerate(text.splitlines(), 1): + marker = _MARKER.match(line) + if marker: + logical = int(marker[1]) + current = source_path(re.sub(r'\\([\\"])', r"\1", marker[2])) + system = _is_system_header(current) or "3" in marker[3].split() + if primary is None and not system and logical <= 1 and not current.endswith("/"): + primary = current + if system and current and not current.startswith("<") and current in retain_paths: + system = False + lines.append("") + continue + if not system and current and logical > 0: + lines.append(line) + origins[physical] = SourceLocation(current, logical) + else: + lines.append("") + if current and logical > 0: + filtered.add(current) + logical += 1 + return OriginSource("\n".join(lines) + "\n", origins, primary, frozenset(filtered)) + + +@dataclass +class CfgDefinition: + name: str + fullname: str + signature: str + start: int + end: int + cfg: DiGraph + + +class CfgParseError(ValueError): + """Explicit front-end error diagnostics, distinct from evaluator failures.""" + + +def parse_definitions(text: str, filename: str, language: str) -> list[CfgDefinition]: + """Use the non-reducing Rust Joern API and exclude declaration-only methods.""" + from rust_joern import parse_code + + parsed = parse_code(text, filename=filename, language=language, strict=False, preprocessed=True) + errors = [row for row in parsed.diagnostics if row["severity"] == "error"] + if errors: + raise CfgParseError( + "\n".join( + f"{row['filename']}:{row['line']}:{row['column']}: {row['message']}" + for row in errors + ) + ) + definitions = [] + for function in parsed.functions: + nodes = function.raw["cpg"]["nodes"] + bodies = [node for node in nodes if node["kind"] == "BLOCK" and node.get("code")] + if not bodies or function.name == "": + continue + definitions.append( + CfgDefinition( + function.name, + function.fullname, + function.signature, + function.start_line, + function.end_line, + function.cfg, + ) + ) + return definitions + + +@dataclass +class CfgJoin: + cfgs: dict[str, DiGraph] = field(default_factory=dict) + evidence: dict[str, dict[str, Any]] = field(default_factory=dict) + + +def decompiled_cfgs(result: DecompilationResult) -> CfgJoin: + """A unique definition belongs to its record even when display names differ.""" + joined = CfgJoin() + for key, function in result.functions.items(): + code = function.decompiled_code + if not code.strip(): + joined.evidence[key] = {"status": "empty_output"} + continue + code = preprocess_decompiled_c(sanitize_decompiled_c(code)) + inspection = inspect_producer(code, function.metadata) + validation = inspection.evidence() + if inspection.invalid: + joined.evidence[key] = { + "status": "invalid_producer_output", + "validation": validation, + } + continue + try: + definitions = parse_definitions(code, "decompiled.cpp", "c++") + except CfgParseError as error: + joined.evidence[key] = { + "status": "parser_failure", + "error": str(error), + "validation": validation, + "failure_class": ( + "unresolved_syntax_or_dialect" + if inspection.grammar_errors + else "frontend_rejected_recognized_syntax" + ), + } + continue + if len(definitions) != 1: + joined.evidence[key] = { + "status": "ambiguous_definitions" if definitions else "missing_definition", + "definitions": [definition.fullname for definition in definitions], + "validation": validation, + } + continue + definition = definitions[0] + joined.cfgs[key] = definition.cfg + joined.evidence[key] = { + "status": "matched", + "address": function.address, + "definition": definition.fullname, + "span": [definition.start, definition.end], + "validation": validation, + } + return joined + + +@dataclass(frozen=True) +class NativeSource: + address: int + name: str + units: tuple[str, ...] + location: SourceLocation | None + identity: str + qualified_name: str | None = None + scope_known: bool = True + compilation_directory: str | None = None + physical_unit: str | None = None + unit_binding: CompilationUnitBinding | None = None + candidate_locations: tuple[SourceLocation, ...] = () + + @property + def locations(self) -> tuple[SourceLocation, ...]: + return (self.location,) if self.location is not None else self.candidate_locations + + +class NativeSourceIndex(dict[int, list[NativeSource]]): + """Reusable native facts are bound to one immutable binary image.""" + + def __init__(self, binary: Path, rows: dict[int, list[NativeSource]]) -> None: + super().__init__(rows) + self.binary_path = binary.resolve() + self.binary_sha256 = hashlib.sha256(binary.read_bytes()).hexdigest() + self.debug_snapshot = binfmt.NativeDebugSnapshot.capture(binary.absolute()) + self.compilation_database: Path | None = None + self.compilation_database_sha256: str | None = None + + def validate(self, binary: Path) -> None: + self.debug_snapshot.validate() + if binary.resolve() != self.binary_path: + raise ValueError("native source index belongs to another binary") + if hashlib.sha256(binary.read_bytes()).hexdigest() != self.binary_sha256: + raise ValueError("native source index binary contents changed") + if ( + self.compilation_database is not None + and hashlib.sha256(self.compilation_database.read_bytes()).hexdigest() + != self.compilation_database_sha256 + ): + raise ValueError("native source index compilation database changed") + + def with_compilation_database(self, database: Path) -> NativeSourceIndex: + self.validate(self.binary_path) + bindings = saved_temp_units(database) + rows: dict[int, list[NativeSource]] = {} + for address, records in self.items(): + rows[address] = [] + for record in records: + binding = bindings.get(record.physical_unit or "") + if binding is not None and binding.directory == record.compilation_directory: + record = replace( + record, + units=tuple(dict.fromkeys((*record.units, binding.source_file))), + unit_binding=binding, + ) + rows[address].append(record) + result = NativeSourceIndex(self.binary_path, rows) + result.debug_snapshot = self.debug_snapshot + result.compilation_database = database.resolve() + result.compilation_database_sha256 = hashlib.sha256(database.read_bytes()).hexdigest() + return result + + +def _dwarf_files(dwarf: Any, cu: Any) -> dict[int, str]: + top = cu.get_top_DIE() + directory_attr = top.attributes.get("DW_AT_comp_dir") + directory = _text(directory_attr.value) if directory_attr else "" + program = dwarf.line_program_for_CU(cu) + if program is None: + return {} + version = int(program.header.version) + dirs = [_text(value) for value in program["include_directory"]] + files = {} + for index, entry in enumerate(program["file_entry"], 0 if version >= 5 else 1): + folder_index = int(entry.dir_index) + folder = ( + dirs[folder_index] + if version >= 5 and folder_index < len(dirs) + else dirs[folder_index - 1] if 0 < folder_index <= len(dirs) else directory + ) + files[index] = _absolute(_text(entry.name), _absolute(folder, directory)) + return files + + +def _dwarf_qualified_name(die: Any) -> str | None: + attr, owner = binfmt.die_attr_owner(die, "DW_AT_name") + if attr is None or owner is None: + return None + names = [_text(attr.value)] + parent = owner.get_parent() + while parent is not None: + if parent.tag in { + "DW_TAG_namespace", + "DW_TAG_class_type", + "DW_TAG_structure_type", + "DW_TAG_union_type", + }: + name = parent.attributes.get("DW_AT_name") + if name is None: + return None + names.append(_text(name.value)) + elif parent.tag in {"DW_TAG_subprogram", "DW_TAG_lexical_block"}: + return None + parent = parent.get_parent() + return "::".join(reversed(names)) + + +def native_sources(binary: Path, addresses: set[int]) -> NativeSourceIndex: + """Retain concrete records and reject ambiguity at resolution, including ICF.""" + index = NativeSourceIndex(binary, {}) + rows: dict[int, list[NativeSource]] = defaultdict(list) + dwarf = binfmt.dwarf_info(binary) + if dwarf is None: + pdb = binfmt.pdb_info(binary) + if pdb is not None: + for function in pdb.functions: + if function.address in addresses: + rows[function.address].append( + NativeSource( + function.address, + function.name, + tuple(source_path(path) for path in function.source_files), + ( + SourceLocation( + source_path(function.source_file), function.source_line + ) + if function.source_file and function.source_line > 0 + else None + ), + f"pdb:{function.module}:{function.address:x}:{function.name}", + candidate_locations=tuple( + SourceLocation(source_path(path), line) + for path, line in function.first_source_locations + ), + ) + ) + index.update(rows) + index.validate(binary) + return index + for cu in dwarf.iter_CUs(): + top = cu.get_top_DIE() + unit_attr = top.attributes.get("DW_AT_name") + directory_attr = top.attributes.get("DW_AT_comp_dir") + unit = _absolute( + _text(unit_attr.value) if unit_attr else "", + _text(directory_attr.value) if directory_attr else "", + ) + file_tables: dict[int, dict[int, str]] = {} + for die in cu.iter_DIEs(): + if die.tag != "DW_TAG_subprogram": + continue + ranges = die_ranges(die, dwarf) + low = die.attributes.get("DW_AT_entry_pc") or die.attributes.get("DW_AT_low_pc") + address = int(low.value) if low else ranges[0][0] if ranges else None + if address not in addresses or not ranges: + continue + if not binfmt.dwarf_low_pc_is_concrete(binary, address): + continue + name = binfmt.die_str_attr(die, "DW_AT_name") + if not name: + continue + file_attr, owner = binfmt.die_attr_owner(die, "DW_AT_decl_file") + line_attr = binfmt.die_attr(die, "DW_AT_decl_line") + location = None + if file_attr is not None and owner is not None and line_attr is not None: + if owner.cu.cu_offset not in file_tables: + file_tables[owner.cu.cu_offset] = _dwarf_files(dwarf, owner.cu) + path = file_tables[owner.cu.cu_offset].get(int(file_attr.value)) + if path and int(line_attr.value) > 0: + location = SourceLocation(path, int(line_attr.value)) + qualified_name = _dwarf_qualified_name(die) + rows[address].append( + NativeSource( + address, + name, + (unit,), + location, + f"dwarf:{die.offset:x}", + qualified_name, + qualified_name is not None, + source_path(_text(directory_attr.value)) if directory_attr else None, + unit, + ) + ) + index.update(rows) + index.validate(binary) + return index + + +def source_unit_matches(unit: str | None, record: NativeSource) -> bool: + if unit is None: + return False + return compilation_path(unit, record.compilation_directory) in record.units + + +def _location_matches( + location: SourceLocation, locations: frozenset[SourceLocation], record: NativeSource +) -> bool: + return any( + candidate.line == location.line + and compilation_path(candidate.path, record.compilation_directory) == location.path + for candidate in locations + ) + + +@dataclass +class SourceCfgIndex: + definitions: list[tuple[str, str | None, FunctionDefinition, frozenset[SourceLocation]]] = ( + field(default_factory=list) + ) + inputs: list[dict[str, Any]] = field(default_factory=list) + + def add( + self, text: str, filename: str, *, native_records: tuple[NativeSource, ...] = () + ) -> None: + """Index a preprocessed C++ TU with full physical/logical origin evidence.""" + source = origin_source(text) + retained = frozenset( + path + for path in source.filtered_paths + for record in native_records + for location in record.locations + if location.line > 0 + and source_unit_matches(source.primary, record) + and compilation_path(path, record.compilation_directory) == location.path + ) + if retained: + source = origin_source(text, retain_paths=retained) + report: dict[str, Any] = { + "input": filename, + "text_sha256": hashlib.sha256(text.encode("utf-8")).hexdigest(), + "unit": source.primary, + "origin_paths": sorted({location.path for location in source.origins.values()}), + "filtered_origin_paths": sorted(source.filtered_paths), + "retained_origin_paths": sorted(retained), + } + definitions = function_definitions(source.text) + for definition in definitions: + locations = frozenset( + source.origins[line] + for line in range(definition.start_line, definition.end_line + 1) + if line in source.origins + ) + self.definitions.append((filename, source.primary, definition, locations)) + report.update(status="indexed", definitions=len(definitions)) + self.inputs.append(report) + + def resolve( + self, result: DecompilationResult, native: dict[int, list[NativeSource]] + ) -> CfgJoin: + joined = CfgJoin() + for key, function in result.functions.items(): + records = native.get(function.address, []) + if len(records) != 1: + joined.evidence[key] = { + "status": "ambiguous_native_identity" if records else "missing_native_identity", + "identities": [record.identity for record in records], + } + continue + record = records[0] + evidence: dict[str, Any] = { + "address": function.address, + "native": record.identity, + "native_name": record.name, + "native_units": list(record.units), + "physical_unit": record.physical_unit, + "compilation_directory": record.compilation_directory, + } + locations = record.locations + if not locations: + evidence["status"] = "missing_native_source_location" + joined.evidence[key] = evidence + continue + location = locations[0] + evidence.update( + path=location.path, + line=location.line, + native_name=record.name, + native_qualified_name=record.qualified_name, + native_units=list(record.units), + compilation_directory=record.compilation_directory, + unit_binding=asdict(record.unit_binding) if record.unit_binding else None, + ) + matches = [ + [ + (filename, definition) + for filename, unit, definition, origins in self.definitions + if source_unit_matches(unit, record) + and _location_matches(candidate_location, origins, record) + ] + for candidate_location in locations + ] + if len(locations) > 1: + evidence["native_location_candidates"] = [asdict(row) for row in locations] + if not all(len(rows) == 1 for rows in matches) or any( + rows[0][0] != matches[0][0][0] or rows[0][1] is not matches[0][0][1] + for rows in matches[1:] + ): + evidence.update( + status="ambiguous_native_source_location", + location_candidate_matches=[ + [(file, definition.name) for file, definition in rows] + for rows in matches + ], + ) + joined.evidence[key] = evidence + continue + evidence["native_location_policy"] = "all_first_pc_locations_same_definition" + candidates = matches[0] + if len(candidates) != 1: + units = [row for row in self.inputs if source_unit_matches(row["unit"], record)] + missing = ( + "missing_source_tu" + if not units + else ( + ( + "filtered_source_origin" + if any( + compilation_path(path, record.compilation_directory) + == location.path + for row in units + for path in row["filtered_origin_paths"] + ) + else "missing_source_origin" + ) + if not any( + compilation_path(path, record.compilation_directory) == location.path + for row in units + for path in row["origin_paths"] + ) + else "missing_source_definition" + ) + ) + evidence.update( + status=("ambiguous_source_definition" if candidates else missing), + candidates=[(file, definition.name) for file, definition in candidates], + ) + else: + filename, definition = candidates[0] + evidence.update( + input=filename, + definition=definition.name, + definition_sha256=hashlib.sha256(definition.code.encode("utf-8")).hexdigest(), + span=[definition.start_line, definition.end_line], + parser_scope="definition_only", + ) + native_name = record.qualified_name or record.name + if not record.scope_known: + evidence["status"] = "missing_native_scope" + elif definition.has_error: + evidence["status"] = "source_syntax_error" + elif definition.is_template: + evidence["status"] = "unsupported_template_specialization" + elif native_name != definition.name: + evidence["status"] = "source_name_mismatch" + else: + try: + parsed = parse_definitions(definition.code, filename, "c++") + except CfgParseError as error: + evidence.update(status="parser_failure", error=str(error)) + else: + if len(parsed) != 1: + evidence["status"] = "ambiguous_parser_definition" + elif is_degenerate_source_cfg(parsed[0].cfg): + evidence["status"] = "degenerate_source_cfg" + else: + joined.cfgs[key] = parsed[0].cfg + evidence.update(status="matched", signature=parsed[0].signature) + joined.evidence[key] = evidence + return joined diff --git a/decbench/utils/compilation_units.py b/decbench/utils/compilation_units.py new file mode 100644 index 0000000..42b9ed2 --- /dev/null +++ b/decbench/utils/compilation_units.py @@ -0,0 +1,73 @@ +"""Recorded compilation-unit provenance; never execute compile database commands.""" + +from __future__ import annotations + +import hashlib +import json +import posixpath +import re +import shlex +from dataclasses import dataclass +from pathlib import Path + + +def compilation_path(path: str, directory: str | None = None) -> str: + path = path.replace("\\", "/") + if directory is not None and not (path.startswith("/") or re.match(r"^[a-zA-Z]:/", path)): + path = directory.replace("\\", "/") + "/" + path + path = posixpath.normpath(path) + return path.casefold() if re.match(r"^[a-zA-Z]:/", path) else path + + +@dataclass(frozen=True) +class CompilationUnitBinding: + native_unit: str + source_file: str + directory: str + command_sha256: str + + +def saved_temp_units(database: Path) -> dict[str, CompilationUnitBinding]: + """Connect object-relative compiler unit names to recorded original inputs. + + With -save-temps=obj, the compiler can record the object output without its + final .o as the DWARF CU name. Only exact native-unit matches consume these + bindings. Reused outputs with conflicting commands are deliberately omitted. + """ + entries = json.loads(database.read_text()) + if not isinstance(entries, list): + raise ValueError("compilation database must be a JSON list") + candidates: dict[str, set[CompilationUnitBinding]] = {} + for entry in entries: + arguments = entry.get("arguments") + if arguments is None: + command = entry.get("command", "") + if "-save-temps=obj" not in command: + continue + arguments = shlex.split(command) + if "-save-temps=obj" not in arguments or "-c" not in arguments or "-o" not in arguments: + continue + directory = entry["directory"].replace("\\", "/") + if not (directory.startswith("/") or re.match(r"^[a-zA-Z]:/", directory)): + raise ValueError("recorded compilation directory must be absolute") + source = compilation_path(entry["file"], directory) + inputs = { + compilation_path(argument, directory) + for argument in arguments + if not argument.startswith("-") + } + if source not in inputs: + raise ValueError("compilation database source is absent from recorded arguments") + output_position = arguments.index("-o") + 1 + if output_position >= len(arguments): + raise ValueError("recorded -o has no output") + output = compilation_path(arguments[output_position], directory) + if not output.endswith(".o"): + continue + native_unit = output[:-2] + command_sha256 = hashlib.sha256(json.dumps(arguments).encode()).hexdigest() + binding = CompilationUnitBinding( + native_unit, source, compilation_path(directory), command_sha256 + ) + candidates.setdefault(native_unit, set()).add(binding) + return {key: next(iter(rows)) for key, rows in candidates.items() if len(rows) == 1} diff --git a/decbench/utils/function_identity.py b/decbench/utils/function_identity.py new file mode 100644 index 0000000..4787b52 --- /dev/null +++ b/decbench/utils/function_identity.py @@ -0,0 +1,170 @@ +"""Collision-safe function identity helpers. + +DecBench historically stores decompilation results and several DWARF-derived +maps by unqualified function name. That is sufficient for the current C corpus, +but it collapses C++ overloads and same-named functions in different scopes. + +This module provides two small building blocks for migrating that behavior +without changing non-colliding results: + +* :func:`dwarf_function_identities` extracts concrete DWARF subprograms keyed by + binary-local address while retaining the human-readable and linkage names. +* :func:`insert_function` keeps the legacy plain-name key while it is unique, + and upgrades every colliding entry to ``@0x
`` only when a real + same-name/different-address collision appears. + +Address is the canonical identity primitive *within one binary*. Linkage names +are retained as a strong C++ source/oracle join key, but are not required (for +example ``main`` normally has no C++ linkage name). +""" + +from __future__ import annotations + +import re +from collections.abc import MutableMapping +from dataclasses import dataclass +from pathlib import Path +from typing import Protocol, TypeVar + +from decbench.utils import binfmt + + +@dataclass(frozen=True, slots=True) +class FunctionIdentity: + """Concrete function identity recovered from a binary's DWARF.""" + + address: int + name: str + linkage_name: str | None = None + + @property + def address_key(self) -> str: + """Stable binary-local key suitable for serialization and diagnostics.""" + return f"0x{self.address:x}" + + +class _FunctionLike(Protocol): + name: str + address: int + + +_FunctionT = TypeVar("_FunctionT", bound=_FunctionLike) + + +def function_storage_key(function: _FunctionLike) -> str: + """Collision-qualified storage key for one function.""" + return f"{function.name}@0x{function.address:x}" + + +def parse_function_storage_key(storage_key: str) -> tuple[str, int | None]: + """Return ``(semantic_name, address)`` encoded by a benchmark storage key. + + Unique functions keep the historical plain-name key and therefore return + ``None`` for the address. Collision-qualified keys are generated only by + DecBench and use ``@0x``. + """ + match = re.fullmatch(r"(.+)@0x([0-9a-fA-F]+)", storage_key) + if match is None: + return storage_key, None + return match.group(1), int(match.group(2), 16) + + +def insert_function( + functions: MutableMapping[str, _FunctionT], + function: _FunctionT, +) -> str: + """Insert ``function`` without collapsing a same-name/different-address peer. + + The plain function name is retained while it is unique, preserving the + historical representation for the C corpus and ordinary non-colliding + binaries. On the first collision, the existing plain-name entry is re-keyed + by address and all subsequent colliders use address-qualified keys as well. + + A repeated observation of the same ``(name, address)`` replaces the existing + value rather than creating a duplicate. + + Returns the key under which ``function`` was stored. + """ + same_name = [(key, value) for key, value in functions.items() if value.name == function.name] + + for key, value in same_name: + if value.address == function.address: + functions[key] = function + return key + + if not same_name: + functions[function.name] = function + return function.name + + # The first time a collision is discovered, remove the ambiguous plain-name + # slot before inserting the new function. Entries already qualified by + # address are left untouched. + for key, value in same_name: + if key == function.name: + del functions[key] + functions[function_storage_key(value)] = value + + key = function_storage_key(function) + functions[key] = function + return key + + +def dwarf_function_identities(binary_path: Path) -> list[FunctionIdentity]: + """Return concrete DWARF functions without collapsing C++ name collisions. + + Only ``DW_TAG_subprogram`` DIEs with a concrete ``DW_AT_low_pc`` are kept. + Name/linkage attributes are read through DecBench's existing + ``DW_AT_specification`` / C++ ``DW_AT_abstract_origin`` chase so out-of-line + member definitions resolve the same way as the existing C++ support. + + Producers can emit more than one DIE description for one code address; those + are deduplicated by address, preferring a row that carries a linkage name. + """ + dwarf = binfmt.dwarf_info(binary_path) + if dwarf is None: + native = binfmt.pdb_info(binary_path) + if native is not None: + # Keep ICF aliases as separate named rows; do not pick their types + # by address. The PDB type oracle rejects ambiguous addresses. + rows = sorted({(f.address, f.name) for f in native.functions}) + return [FunctionIdentity(address=address, name=name) for address, name in rows] + return [] + + by_address: dict[int, FunctionIdentity] = {} + for cu in dwarf.iter_CUs(): + for die in cu.iter_DIEs(): + if die.tag != "DW_TAG_subprogram" or "DW_AT_low_pc" not in die.attributes: + continue + + name = binfmt.die_str_attr(die, "DW_AT_name") + if not name: + continue + + address = int(die.attributes["DW_AT_low_pc"].value) + if not binfmt.dwarf_low_pc_is_concrete(binary_path, address): + continue + + linkage = binfmt.die_str_attr(die, "DW_AT_linkage_name") + if linkage is None: + linkage = binfmt.die_str_attr(die, "DW_AT_MIPS_linkage_name") + + identity = FunctionIdentity( + address=address, + name=name, + linkage_name=linkage, + ) + current = by_address.get(identity.address) + if current is None or (current.linkage_name is None and linkage is not None): + by_address[identity.address] = identity + + return sorted(by_address.values(), key=lambda identity: identity.address) + + +def identities_by_name( + identities: list[FunctionIdentity], +) -> dict[str, list[FunctionIdentity]]: + """Group identities by unqualified name without discarding collisions.""" + grouped: dict[str, list[FunctionIdentity]] = {} + for identity in identities: + grouped.setdefault(identity.name, []).append(identity) + return grouped diff --git a/decbench/utils/native_code.py b/decbench/utils/native_code.py index b55ff07..ffa7788 100644 --- a/decbench/utils/native_code.py +++ b/decbench/utils/native_code.py @@ -11,7 +11,7 @@ from decbench.utils import binfmt -SUPPORTED_FORMATS = frozenset({"elf", "pe"}) +SUPPORTED_FORMATS = frozenset({"elf", "pe", "macho"}) SUPPORTED_ARCHITECTURES = frozenset({"x86", "x86-64", "arm", "aarch64"}) @@ -177,8 +177,9 @@ def __init__(self, binary_path: Path): if info.fmt not in SUPPORTED_FORMATS or info.arch not in SUPPORTED_ARCHITECTURES: raise ValueError(f"unsupported binary format/architecture {info.fmt}/{info.arch}") dwarfinfo = binfmt.dwarf_info(self.binary_path) - if dwarfinfo is None: - raise ValueError("binary has no readable DWARF") + pdb = binfmt.pdb_info(self.binary_path) if dwarfinfo is None else None + if dwarfinfo is None and pdb is None: + raise ValueError("binary has no readable DWARF or matching PDB") self.info = info self.regions = binfmt.executable_regions(self.binary_path) self.mclass = ( @@ -193,6 +194,12 @@ def __init__(self, binary_path: Path): set ) self._resolved: dict[tuple[str, int], FunctionCode | str] = {} + if pdb is not None: + for function in pdb.functions: + self._ranges[(function.name, function.address)].add( + ((function.address, function.end),) + ) + return for cu in dwarfinfo.iter_CUs(): for die in cu.iter_DIEs(): if die.tag != "DW_TAG_subprogram": @@ -288,6 +295,19 @@ def resolve(self, function_name: str, function_address: int) -> FunctionCode: def _resolve_uncached(self, function_name: str, function_address: int) -> FunctionCode: expected = function_address & ~1 if self.info.arch == "arm" else function_address candidates = self._ranges.get((function_name, expected), set()) + if not candidates: + # A stripped producer may emit sub_, or a PDB demangler may + # include a signature the oracle's procedure name does not carry. + # Instruction membership is keyed by the physical entry, not the + # spelling of that label. Admit only one identity at that entry; + # aliases/ICF collisions must not select another function's range. + at_entry = [ + ranges for (_name, entry), ranges in self._ranges.items() if entry == expected + ] + if len(at_entry) == 1: + candidates = at_entry[0] + elif len(at_entry) > 1: + raise ValueError(f"ambiguous DWARF function at entry 0x{function_address:x}") if not candidates: raise ValueError( f"no DWARF function matches {function_name!r} at 0x{function_address:x}" diff --git a/decbench/utils/pdb_info.py b/decbench/utils/pdb_info.py new file mode 100644 index 0000000..965c488 --- /dev/null +++ b/decbench/utils/pdb_info.py @@ -0,0 +1,1069 @@ +"""Bounded native PDB 7 transport for PE evaluation, independent of LLVM tools. + +Only documented MSF/DBI/TPI/CodeView records are interpreted. Unsupported +types, storage pieces, ICF conflicts and unproven parameter classification +remain explicit exclusions. This is not a full PDB debugger or C++ type policy. +""" + +from __future__ import annotations + +import struct +import uuid +from bisect import bisect_left +from dataclasses import dataclass, field +from pathlib import Path, PureWindowsPath +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from lief.PE import Binary, CodeViewPDB + +_MAGIC = b"Microsoft C/C++ MSF 7.00\r\n\x1aDS\0\0\0" +_MAX_SIZE = 512 * 1024 * 1024 +_PROC = {0x110F, 0x1110, 0x1146, 0x1147} +_REGISTERS = { + **dict( + enumerate( + ( + "al", + "cl", + "dl", + "bl", + "ah", + "ch", + "dh", + "bh", + "ax", + "cx", + "dx", + "bx", + "sp", + "bp", + "si", + "di", + "eax", + "ecx", + "edx", + "ebx", + "esp", + "ebp", + "esi", + "edi", + ), + 1, + ) + ), + **dict(enumerate(("rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "rsp"), 328)), + **{336 + n: f"r{8+n}" for n in range(8)}, + **{344 + n: f"r{8+n}b" for n in range(8)}, + **{352 + n: f"r{8+n}w" for n in range(8)}, + **{360 + n: f"r{8+n}d" for n in range(8)}, + **{154 + n: f"xmm{n}" for n in range(8)}, +} +_SIMPLE = { + 0x03: ("void", "void", 0), + 0x08: ("HRESULT", "int", 4), + 0x10: ("signed char", "signed char", 1), + 0x20: ("unsigned char", "unsigned char", 1), + 0x70: ("char", "char", 1), + 0x71: ("wchar_t", "unsigned short", 2), + 0x68: ("signed char", "signed char", 1), + 0x69: ("unsigned char", "unsigned char", 1), + 0x11: ("short", "short", 2), + 0x21: ("unsigned short", "unsigned short", 2), + 0x72: ("short", "short", 2), + 0x73: ("unsigned short", "unsigned short", 2), + # Preserve LLP64 source spelling independently of the existing LP64 matcher. + 0x12: ("long", "int", 4), + 0x22: ("unsigned long", "unsigned int", 4), + 0x74: ("int", "int", 4), + 0x75: ("unsigned int", "unsigned int", 4), + 0x13: ("long long", "long long", 8), + 0x23: ("unsigned long long", "unsigned long long", 8), + 0x76: ("long long", "long long", 8), + 0x77: ("unsigned long long", "unsigned long long", 8), + 0x30: ("bool", "bool", 1), + 0x40: ("float", "float", 4), + 0x41: ("double", "double", 8), +} + + +class PdbFormatError(ValueError): + """A recognized unsupported or invalid native PDB/CodeView representation.""" + + +def _unpack(fmt: str, data: bytes, offset: int = 0): + if offset < 0 or offset + struct.calcsize(fmt) > len(data): + raise PdbFormatError("truncated PDB record") + return struct.unpack_from(fmt, data, offset) + + +def _string(data: bytes, offset: int) -> tuple[str, int]: + end = data.find(b"\0", offset) + if not 0 <= offset <= end: + raise PdbFormatError("unterminated PDB string") + return data[offset:end].decode("utf-8", "replace"), end + 1 + + +def _numeric(data: bytes, offset: int) -> tuple[int, int]: + tag = _unpack(" end: + raise PdbFormatError("invalid CodeView record length") + yield offset, kind, data[offset + 4 : following] + offset = following + + +class _MSF: + def __init__(self, path: Path): + if path.stat().st_size > _MAX_SIZE: + raise PdbFormatError("PDB exceeds the supported 512 MiB reader bound") + self.data = path.read_bytes() + if self.data[:32] != _MAGIC: + raise PdbFormatError("not an MSF 7 PDB") + self.block_size, _free, count, directory_size, _unknown, directory_map = _unpack( + "<6I", self.data, 32 + ) + if ( + self.block_size not in {512, 1024, 2048, 4096} + or len(self.data) != count * self.block_size + ): + raise PdbFormatError("invalid MSF block geometry") + n = (directory_size + self.block_size - 1) // self.block_size + if n > self.block_size // 4 or directory_size > len(self.data): + raise PdbFormatError("unsupported MSF directory extent") + blocks = _unpack(f"<{n}I", self.data, directory_map * self.block_size) + directory = self._join(blocks, directory_size) + nstreams = _unpack(" len(directory): + raise PdbFormatError("invalid MSF stream count") + sizes = _unpack(f"<{nstreams}I", directory, 4) + self.streams = [] + self.named_streams: dict[str, bytes] = {} + offset = 4 + 4 * nstreams + for size in sizes: + if size == 0xFFFFFFFF: + self.streams.append(None) + continue + if size > len(self.data): + raise PdbFormatError("invalid MSF stream size") + nblocks = (size + self.block_size - 1) // self.block_size + blocks = _unpack(f"<{nblocks}I", directory, offset) + offset += nblocks * 4 + self.streams.append((blocks, size)) + + def _join(self, blocks, size): + if any(b * self.block_size + self.block_size > len(self.data) for b in blocks): + raise PdbFormatError("MSF block outside file") + return b"".join(self.data[b * self.block_size : (b + 1) * self.block_size] for b in blocks)[ + :size + ] + + def stream(self, index: int) -> bytes: + if not 0 <= index < len(self.streams) or self.streams[index] is None: + raise PdbFormatError(f"missing PDB stream {index}") + return self._join(*self.streams[index]) + + +@dataclass(frozen=True) +class PdbType: + source_name: str + abi_name: str | None + size: int | None + category: str = "scalar" + + +class _Types: + def __init__(self, data: bytes): + self.records: dict[int, tuple[int, bytes]] = {} + self.cache: dict[int, PdbType | None] = {} + self._full_records: dict[tuple[int, str, str], list[int]] | None = None + self._layout_cache: dict[int, dict[str, Any]] = {} + if data: + _version, header, first, last, size = _unpack("<5I", data) + if header < 20 or first < 0x1000 or last < first: + raise PdbFormatError("invalid PDB type header") + for index, (_, kind, payload) in enumerate( + _records(data, header, header + size), first + ): + self.records[index] = kind, payload + if len(self.records) != last - first: + raise PdbFormatError("PDB type index/record count mismatch") + + def resolve(self, index: int, depth: int = 0) -> PdbType | None: + if depth > 32: + return None + if index in self.cache: + return self.cache[index] + if index < 0x1000: + base = _SIMPLE.get(index & 0xFF) + mode = (index >> 8) & 0xF + if base is None: + return None + source, abi, size = base + if mode: + pointer_size = {4: 4, 6: 8}.get(mode) + return ( + PdbType(source + "*", abi + "*", pointer_size, "pointer") + if pointer_size + else None + ) + return PdbType(source, abi, size) + self.cache[index] = None # cycles must never manufacture a type + record = self.records.get(index) + result = None + if record: + kind, p = record + if kind == 0x1001: # modifier + base, flags = _unpack("> 5) & 7 + t = self.resolve(base, depth + 1) + size = (attrs >> 13) & 63 or {10: 4, 12: 8}.get(attrs & 31) + if t and mode in {0, 1, 4} and size in {4, 8}: + suffix = {0: "*", 1: "&", 4: "&&"}[mode] + source_qualifiers = (" const" if attrs & 0x400 else "") + ( + " volatile" if attrs & 0x200 else "" + ) + result = PdbType( + t.source_name + suffix + source_qualifiers, + t.abi_name + "*" if t.abi_name else None, + size, + "pointer" if mode == 0 else "reference", + ) + elif kind in {0x1504, 0x1505, 0x1519, 0x1506}: # named aggregate + _count, flags = _unpack("= 0 and size % t.size == 0: + suffix = f"[{size // t.size}]" + result = PdbType( + t.source_name + suffix, + t.abi_name + suffix if t.abi_name else None, + size, + "array", + ) + self.cache[index] = result + return result + + def parameter_count(self, index: int) -> int | None: + record = self.records.get(index) + if record is None: + return None + kind, p = record + if kind == 0x1008: + options = _unpack(" tuple[dict, ...]: + """Bounded CodeView instance data, base and virtual-table facts. + + Methods/static members/nested declarations are not instance storage. + An unknown leaf invalidates the list instead of yielding partial data. + """ + if not index: + return () + if index in seen or len(seen) > 32: + raise PdbFormatError("cyclic CodeView field-list continuation") + record = self.records.get(index) + if record is None or record[0] != 0x1203: + raise PdbFormatError("missing CodeView field list") + data = record[1] + offset = 0 + result = [] + while offset < len(data): + if data[offset] >= 0xF0: + padding = data[offset] & 15 + if not padding or data[offset : offset + padding] != bytes( + range(0xF0 + padding, 0xF0, -1) + ): + raise PdbFormatError("invalid CodeView field padding") + offset += padding + continue + kind = _unpack("> 2) & 7 in {4, 6}: + _unpack(" 16384: + raise PdbFormatError("CodeView field list exceeds bound") + return tuple(result) + + def full_record_index(self, index: int) -> int: + """Resolve a CodeView forward by exact unique identity, with ambiguity abstention.""" + record = self.records.get(index) + if record is None or record[0] not in {0x1504, 0x1505, 0x1519, 0x1506}: + raise PdbFormatError("not a CodeView record type") + kind, data = record + _count, flags = _unpack(" dict[str, Any]: + """Expose raw instance layout without a scoring policy or name-only join.""" + index = self.full_record_index(index) + if index not in self._layout_cache: + kind, data = self.records[index] + count, flags, fields = _unpack(" bool: + return not self.exclusions and all(not v.exclusions for v in self.variables) + + +@dataclass +class PdbInfo: + path: Path + guid: str + age: int + functions: tuple[PdbFunction, ...] + types: _Types | None = field(default=None, repr=False) + _by_address: dict[int, PdbFunction | None] = field(default_factory=dict, init=False, repr=False) + + def __post_init__(self): + for function in self.functions: + if function.address in self._by_address: + self._by_address[function.address] = None + else: + self._by_address[function.address] = function + + def at_address(self, address: int) -> PdbFunction | None: + return self._by_address.get(address) + + +def _source_files(dbi: bytes, offset: int, size: int, nmodules: int) -> list[tuple[str, ...]]: + p = dbi[offset : offset + size] + count, _unused = _unpack(" bytes: + if name in msf.named_streams: + return msf.named_streams[name] + info = msf.stream(1) + length = _unpack(" capacity + 31: + raise PdbFormatError("invalid PDB named-stream bit vector") + values = _unpack(f"<{words}I", info, offset) + offset += words * 4 + if vector == 0: + present = [ + word * 32 + bit + for word, value in enumerate(values) + for bit in range(32) + if value & (1 << bit) + ] + if len(present) != count or any(v >= capacity for v in present): + raise PdbFormatError("invalid PDB named-stream count") + found = [] + for _ in present: + key, index = _unpack(" start + size: + raise PdbFormatError("PDB C13 subsection outside stream") + subsections.append((kind, data[offset + 8 : offset + 8 + length])) + offset = (offset + 8 + length + 3) & ~3 + checksums = {} + for kind, p in subsections: + if kind != 0xF4: + continue + pos = 0 + while pos < len(p): + name, length, _kind = _unpack(" len(p): + raise PdbFormatError("truncated PDB file checksum") + checksums[pos] = name + pos = (pos + 6 + length + 3) & ~3 + if not checksums: + return [] + strings = _named_stream(msf, "/names") + signature, version, length = _unpack(" len(strings): + raise PdbFormatError("unsupported PDB string table") + strings = strings[12 : 12 + length] + rows = [] + for kind, p in subsections: + if kind != 0xF2: + continue + base, segment, flags, size = _unpack(" len(p): + raise PdbFormatError("invalid PDB line block") + if file_id not in checksums: + raise PdbFormatError("PDB line block has no file checksum") + name = _string(strings, checksums[file_id])[0] + for n in range(count): + pc, line = _unpack("= size: + raise PdbFormatError("PDB source line PC outside code contribution") + rows.append((begin + pc, name, line & 0xFFFFFF)) + pos += block_size + return rows + + +def _pdb_input( + binary_path: Path, debug_path: Path | None = None +) -> tuple[Binary, CodeViewPDB, Path] | None: + """Resolve the sole CodeView companion without reading its contents.""" + import lief + + binary = lief.PE.parse(str(binary_path)) + if binary is None: + return None + codeviews = [d for d in binary.debug if isinstance(d, lief.PE.CodeViewPDB)] + if not codeviews: + return None + if len(codeviews) != 1: + raise PdbFormatError("ambiguous PE CodeView identity") + cv = codeviews[0] + filename = cv.filename + if isinstance(filename, bytes): + filename = filename.decode("utf-8", "replace") + path = ( + Path(debug_path) + if debug_path is not None + else binary_path.parent / PureWindowsPath(filename).name + ) + return binary, cv, path + + +def pdb_path(binary_path: Path) -> Path | None: + """Return the canonical CodeView path, including an absent companion.""" + inputs = _pdb_input(binary_path) + return inputs[2] if inputs is not None else None + + +def read_pdb(binary_path: Path, debug_path: Path | None = None) -> PdbInfo | None: + """Read only a matching adjacent (or explicitly supplied) native PE PDB.""" + inputs = _pdb_input(binary_path, debug_path) + if inputs is None: + return None + binary, cv, path = inputs + if not path.is_file(): + return None + msf = _MSF(path) + info = msf.stream(1) + age = _unpack(" len(dbi): + raise PdbFormatError("invalid PDB DBI substream bounds") + mod_size, contrib, secmap, files_size, _typeserver = sizes + debug_header = 64 + sum(sizes) + ec + indices = _unpack(f"<{optional // 2}H", dbi, debug_header) + if len(indices) < 6 or indices[5] == 0xFFFF: + raise PdbFormatError("PDB has no section headers") + if any(i != 0xFFFF and msf.stream(i) for i in indices[3:5]): + raise PdbFormatError("OMAP address translation is not supported") + sections = msf.stream(indices[5]) + if len(sections) % 40 or len(sections) // 40 != len(binary.sections): + raise PdbFormatError("PE/PDB section count mismatch") + for n, section in enumerate(binary.sections): + name, vsize, rva, raw_size, raw_pointer = _unpack("<8s4I", sections, n * 40) + if (name.rstrip(b"\0").decode(errors="replace"), vsize, rva, raw_size, raw_pointer) != ( + section.name, + section.virtual_size, + section.virtual_address, + section.sizeof_raw_data, + section.pointerto_raw_data, + ): + raise PdbFormatError("PE/PDB section mapping mismatch") + + def address(segment: int, offset: int, size: int) -> int: + if not 1 <= segment <= len(binary.sections): + raise PdbFormatError("CodeView section index outside image") + section = binary.sections[segment - 1] + if ( + offset < 0 + or size <= 0 + or offset + size > len(section.content) + or not int(section.characteristics) & 0x20000000 + ): + raise PdbFormatError( + f"CodeView range outside executable section {segment}: {offset}+{size}" + ) + return int(binary.imagebase) + section.virtual_address + offset + + modules = [] + cursor = 64 + while cursor < 64 + mod_size: + stream, sym_size, c11, c13 = _unpack(" 64 + mod_size: + raise PdbFormatError("PDB module record outside module substream") + modules.append((module, stream, sym_size, c11, c13)) + files = _source_files(dbi, 64 + mod_size + contrib + secmap, files_size, len(modules)) + types = _Types(msf.stream(2)) + ids = _Types(msf.stream(4)) if len(msf.streams) > 4 and msf.streams[4] else _Types(b"") + functions = [] + for module_index, (module, stream, sym_size, _c11, _c13) in enumerate(modules): + if stream == 0xFFFF or sym_size <= 4: + continue + data = msf.stream(stream) + if _unpack("= end_offset: + current = None + last_var = None + if kind in _PROC: + ( + parent, + end_offset, + _next, + length, + dbg_start, + dbg_end, + signature, + offset, + segment, + ) = _unpack("<8IH", p) + name, _ = _string(p, 35) + if parent or not position < end_offset < sym_size: + raise PdbFormatError("unsupported or invalid procedure scope") + begin = address(segment, offset, length) + if not 0 <= dbg_start <= dbg_end <= length: + raise PdbFormatError("invalid procedure debug bounds") + if kind in {0x1146, 0x1147}: + id_record = ids.records.get(signature) + signature = ( + _unpack("> shift) & 3 + register = ( + {1: 335, 2: 334}.get(frame) + if machine == 0x8664 + else {1: 21, 2: 22}.get(frame) + ) + range_offset = 4 + elif kind == 0x1145: + register, flags, offset = _unpack(" length: + raise PdbFormatError("CodeView gap outside storage range") + gaps.append((begin + gap, begin + gap + size)) + spans = [] + cursor = begin + for start, stop in sorted(gaps): + if start < cursor: + raise PdbFormatError("overlapping CodeView storage gaps") + if cursor < start: + spans.append((cursor, start)) + cursor = stop + if cursor < end: + spans.append((cursor, end)) + last_var.storage.extend(PdbStorage(register, offset, a, b) for a, b in spans) + continue + if kind not in { + 0x0006, + 0x000A, + 0x1101, + 0x1105, + 0x1108, + 0x1116, + 0x1124, + 0x1125, + 0x1127, + 0x1139, + 0x113A, + 0x113C, + 0x114C, + 0x114E, + 0x114F, + 0x115A, + 0x115B, + 0x115E, + 0x1168, + }: + current.exclusions.append(f"unsupported symbol in procedure 0x{kind:x}") + return PdbInfo(path, guid, age, tuple(functions), types) + + +def reference_addresses(binary_path: Path, function: PdbFunction) -> dict[str, list[int]]: + """Actual decoded operand references, never every PC in a live range.""" + import capstone + + from decbench.utils import binfmt + + info = binfmt.detect(binary_path) + mode = binfmt.capstone_arch_mode(info) + decoder = capstone.Cs(*mode) + decoder.detail = True + regions = binfmt.executable_regions(binary_path) + region = next( + ( + data[function.address - base : function.end - base] + for base, data in regions + if base <= function.address < function.end <= base + len(data) + ), + None, + ) + if region is None: + raise PdbFormatError("PDB procedure is not file backed") + result = {v.identity: [] for v in function.variables} + for instruction in decoder.disasm(region, function.address): + for variable in function.variables: + for storage in variable.storage: + if not storage.begin <= instruction.address < storage.end: + continue + name = _REGISTERS.get(storage.register) + if storage.offset is None: + read, written = instruction.regs_access() + matched = any(instruction.reg_name(reg) == name for reg in (*read, *written)) + else: + matched = any( + op.type == capstone.CS_OP_MEM + and instruction.reg_name(op.mem.base) == name + and not op.mem.index + and op.mem.disp == storage.offset + for op in instruction.operands + ) + if matched: + result[variable.identity].append(instruction.address) + break + return {identity: sorted(set(addresses)) for identity, addresses in result.items()} + + +def ground_truth_variables( + binary_path: Path, function: PdbFunction, *, include_addresses: bool = True +) -> list[dict[str, Any]]: + if not function.measurable: + return [] + references = reference_addresses(binary_path, function) if include_addresses else {} + result = [] + for variable in function.variables: + # Storage offsets with different bases cannot share a scalar calibration. + bases = {s.register for s in variable.storage if s.offset is not None} + # No stack-origin conversion has been proven between CodeView frame + # bases and backend stack coordinates. Preserve bases in metadata only. + offsets = [] + result.append( + { + "identity": variable.identity, + "name": variable.name, + "type": [variable.type.abi_name], + "source_type": variable.type.source_name, + "size": variable.type.size, + "rbp_offset": offsets, + "storage_base": sorted(bases), + "addresses": references.get(variable.identity, []), + "is_arg": variable.is_parameter, + "arg_index": None, + "debug_format": "pdb", + } + ) + return result diff --git a/decbench/utils/producer_validation.py b/decbench/utils/producer_validation.py new file mode 100644 index 0000000..372606d --- /dev/null +++ b/decbench/utils/producer_validation.py @@ -0,0 +1,204 @@ +"""Bounded, oracle-free source defects; a clean result never proves C++ validity.""" + +from __future__ import annotations + +import hashlib +from bisect import bisect_right +from collections.abc import Iterator +from dataclasses import asdict, dataclass +from typing import Any + +from tree_sitter import Node + +from decbench.metrics.variable_features import _parse_source +from decbench.models.decompilation import ProducerLanguageContract + + +@dataclass(frozen=True) +class ProducerFinding: + kind: str + line: int + column: int + text: str + previous_line: int | None = None + + +@dataclass(frozen=True) +class ProducerInspection: + contract: ProducerLanguageContract + findings: tuple[ProducerFinding, ...] + grammar_errors: bool + text_sha256: str + + @property + def invalid(self) -> bool: + return bool(self.findings) + + def evidence(self) -> dict[str, Any]: + return { + "version": "producer-inspection-v1", + "contract": self.contract.model_dump(), + "validity": "invalid_source_output" if self.invalid else "not_established", + "grammar_errors": self.grammar_errors, + "text_sha256": self.text_sha256, + "findings": [asdict(finding) for finding in self.findings], + "bounds": "Bounded source checks, not compiler validation or pseudocode semantics", + } + + +def _nodes(root: Node) -> Iterator[Node]: + pending = [root] + while pending: + node = pending.pop() + yield node + pending.extend(reversed(node.named_children)) + + +def _tokens(root: Node, source: bytes) -> Iterator[tuple[Node, bytes]]: + pending = [root] + opaque = {"comment", "string_literal", "raw_string_literal", "char_literal"} + while pending: + node = pending.pop() + if node.is_missing or node.type in opaque or node.type.startswith("preproc_"): + continue + if not node.child_count or node.type == "number_literal": + yield node, source[node.start_byte : node.end_byte] + else: + pending.extend(reversed(node.children)) + + +def _object_name(node: Node) -> Node | None: + while node.type not in {"identifier", "field_identifier"}: + if node.has_error or node.type in {"function_declarator", "structured_binding_declarator"}: + return None + child = node.child_by_field_name("declarator") + if child is None: + if node.type == "parenthesized_declarator" and len(node.named_children) == 1: + child = node.named_children[0] + else: + return None + node = child + return node + + +def _parameters(function: Node) -> list[Node]: + declarator = function.child_by_field_name("declarator") + while declarator is not None: + parameters = declarator.child_by_field_name("parameters") + if parameters is not None: + return [ + name + for parameter in parameters.named_children + if (decl := parameter.child_by_field_name("declarator")) is not None + and (name := _object_name(decl)) is not None + ] + declarator = declarator.child_by_field_name("declarator") + return [] + + +def _declarations(block: Node, source: bytes) -> Iterator[Node]: + explicit_types = { + "primitive_type", + "sized_type_specifier", + "struct_specifier", + "union_specifier", + "enum_specifier", + "class_specifier", + } + for declaration in block.named_children: + if declaration.type != "declaration" or declaration.has_error: + continue + ty = declaration.child_by_field_name("type") + if ty is None or ty.type not in explicit_types: + continue + if any( + child.type == "storage_class_specifier" + and source[child.start_byte : child.end_byte] == b"extern" + for child in declaration.named_children + ): + continue + for index, child in enumerate(declaration.children): + if declaration.field_name_for_child(index) == "declarator": + name = _object_name(child) + if name is not None: + yield name + + +def inspect_producer(code: str, metadata: dict[str, Any] | None = None) -> ProducerInspection: + """Reject only established defects; malformed contracts/faults propagate.""" + payload = (metadata or {}).get("output_contract") + contract = ( + ProducerLanguageContract.model_validate(payload) + if payload is not None + else ProducerLanguageContract() + ) + source = code.encode("utf-8") + line_starts = [0] + [index + 1 for index, byte in enumerate(source) if byte == 10] + tree = _parse_source(source) + findings: list[ProducerFinding] = [] + + def finding(kind: str, node: Node, previous: Node | None = None) -> None: + # Byte coordinates avoid py-tree-sitter 0.26 Point.column heap corruption (#487). + line = bisect_right(line_starts, node.start_byte) + findings.append( + ProducerFinding( + kind, + line, + node.start_byte - line_starts[line - 1] + 1, + source[node.start_byte : node.end_byte].decode("utf-8", "replace"), + bisect_right(line_starts, previous.start_byte) if previous is not None else None, + ) + ) + + tokens: list[tuple[Node, bytes]] = [] + for node, text in _tokens(tree.root_node, source): + if ( + text == b":" + and tokens + and tokens[-1][1] == b":" + and tokens[-1][0].end_byte == node.start_byte + ): + tokens[-1] = (tokens[-1][0], b"::") + else: + tokens.append((node, text)) + for index, (node, text) in enumerate(tokens): + if ( + node.type == "number_literal" + and index > 0 + and index + 1 < len(tokens) + and tokens[index - 1][1] == tokens[index + 1][1] == b"::" + ): + finding("numeric_qualified_identifier", node) + if ( + text.startswith(b"unsupported_Iop_") + and index >= 3 + and [token for _, token in tokens[index - 3 : index]] == [b"[", b"D", b"]"] + and index + 1 < len(tokens) + and tokens[index + 1][1] == b"(" + ): + finding("unsupported_ir_placeholder", node) + + for function in _nodes(tree.root_node): + if function.type != "function_definition": + continue + body = function.child_by_field_name("body") + if body is None: + continue + for block in _nodes(body): + if block.type != "compound_statement": + continue + seen: dict[bytes, Node] = {} + names = (_parameters(function) if block == body else []) + list( + _declarations(block, source) + ) + for name in names: + spelling = source[name.start_byte : name.end_byte] + previous = seen.get(spelling) + if previous is not None: + finding("duplicate_local_binding", name, previous) + else: + seen[spelling] = name + findings.sort(key=lambda item: (item.line, item.column, item.kind)) + return ProducerInspection( + contract, tuple(findings), tree.root_node.has_error, hashlib.sha256(source).hexdigest() + ) diff --git a/decbench/utils/results_tree.py b/decbench/utils/results_tree.py index b994a7f..3732f74 100644 --- a/decbench/utils/results_tree.py +++ b/decbench/utils/results_tree.py @@ -25,8 +25,9 @@ from pathlib import Path from decbench.utils import binfmt +from decbench.utils.function_identity import parse_function_storage_key -FUNCTION_MARKER = re.compile(r"^// Function: (\S+) @ (0x[0-9a-fA-F]+)\s*$", re.M) +FUNCTION_MARKER = re.compile(r"^// Function: (.+?) @ (0x[0-9a-fA-F]+)\s*$", re.M) OPT_LEVELS = ("O0", "O2", "O2-noinline") @@ -51,19 +52,56 @@ def decompiled_c_path(root: Path, opt: str, project: str, decompiler: str, stem: def split_functions(c_path: Path) -> dict[str, tuple[int, str]]: - """``name -> (address, decompiled block)`` for one decompiled ``.c`` file.""" + """``storage key -> (address, decompiled block)`` for one ``.c`` artifact. + + New artifacts write collision-qualified storage keys directly. Historical + artifacts wrote only semantic names, so if two markers share a name at + different addresses, upgrade both to ``@0x
`` while reading. + """ text = c_path.read_text(errors="replace") out: dict[str, tuple[int, str]] = {} matches = list(FUNCTION_MARKER.finditer(text)) for i, m in enumerate(matches): start = m.end() end = matches[i + 1].start() if i + 1 < len(matches) else len(text) - out[m.group(1)] = (int(m.group(2), 16), text[start:end].strip()) + marker_key = m.group(1) + address = int(m.group(2), 16) + code = text[start:end].strip() + semantic_name, encoded_address = parse_function_storage_key(marker_key) + + if encoded_address is not None: + out[marker_key] = (address, code) + continue + + same_name = [ + (key, value) + for key, value in out.items() + if parse_function_storage_key(key)[0] == semantic_name + ] + duplicate = next( + (key for key, (existing_address, _code) in same_name if existing_address == address), + None, + ) + if duplicate is not None: + out[duplicate] = (address, code) + continue + if not same_name: + out[semantic_name] = (address, code) + continue + + for key, (existing_address, existing_code) in same_name: + if key == semantic_name: + del out[key] + out[f"{semantic_name}@0x{existing_address:x}"] = ( + existing_address, + existing_code, + ) + out[f"{semantic_name}@0x{address:x}"] = (address, code) return out def function_addresses(c_path: Path) -> dict[str, int]: - """``name -> address`` parsed from a decompiled ``.c`` header (``{}`` if absent).""" + """``storage key -> address`` parsed from a decompiled artifact.""" if not c_path.is_file(): return {} return {name: addr for name, (addr, _code) in split_functions(c_path).items()} diff --git a/decbench/utils/source_extract.py b/decbench/utils/source_extract.py index eb78352..f2cc144 100644 --- a/decbench/utils/source_extract.py +++ b/decbench/utils/source_extract.py @@ -33,48 +33,75 @@ from decbench.utils import binfmt from decbench.utils.langs import PREPROC_EXTS, SOURCE_EXTS, strip_source_ext +DeclHint = tuple[str, int] -def _dwarf_decl(binary_path: Path) -> dict[str, tuple[str, int]]: - """Map function name -> (decl_file basename, decl_line) from DWARF. - Attributes are read through ``DW_AT_specification``/``DW_AT_abstract_origin`` - so a C++ out-of-line member definition, which carries none of them itself, - still resolves to its declaring file and line. +def _dwarf_decl_maps(binary_path: Path) -> tuple[dict[str, DeclHint], dict[int, DeclHint]]: + """Return DWARF declaration hints keyed by name and concrete address. - Empty dict when DWARF is missing/unreadable (callers then search all - sibling sources without a line hint). + The name map preserves the historical API. The address map is the + collision-safe C++ path: overloads and same-named methods can share a + ``DW_AT_name`` but cannot share a concrete ``DW_AT_low_pc`` within one + binary. """ - out: dict[str, tuple[str, int]] = {} + by_name: dict[str, DeclHint] = {} + by_address: dict[int, DeclHint] = {} try: - from elftools.elf.elffile import ELFFile - except Exception: # noqa: BLE001 - return out - try: - with open(binary_path, "rb") as f: - elf = ELFFile(f) - if not elf.has_dwarf_info(): - return out - dw = elf.get_dwarf_info() - file_tables: dict[int, list] = {} - for cu in dw.iter_CUs(): - for die in cu.iter_DIEs(): - if die.tag != "DW_TAG_subprogram" or "DW_AT_low_pc" not in die.attributes: - continue - name = binfmt.die_str_attr(die, "DW_AT_name") - if name is None: - continue - fi, fi_owner = binfmt.die_attr_owner(die, "DW_AT_decl_file") - ln = binfmt.die_attr(die, "DW_AT_decl_line") - fname = None - if fi is not None: - files = binfmt.cu_file_table(dw, fi_owner.cu, file_tables) - if 0 <= fi.value < len(files): - fname = files[fi.value] - line = int(ln.value) if ln is not None else 0 - out[name] = (os.path.basename(fname) if fname else "", line) + dw = binfmt.dwarf_info(binary_path) + if dw is None: + native = binfmt.pdb_info(binary_path) + if native is not None: + from collections import Counter + + names = Counter(f.name for f in native.functions) + for function in native.functions: + if function.source_file: + hint = ( + function.source_file.replace("\\", "/").rsplit("/", 1)[-1], + function.source_line, + ) + if names[function.name] == 1: + by_name[function.name] = hint + if native.at_address(function.address) is not None: + by_address[function.address] = hint + return by_name, by_address + file_tables: dict[int, list] = {} + for cu in dw.iter_CUs(): + for die in cu.iter_DIEs(): + if die.tag != "DW_TAG_subprogram" or "DW_AT_low_pc" not in die.attributes: + continue + name = binfmt.die_str_attr(die, "DW_AT_name") + if name is None: + continue + fi, fi_owner = binfmt.die_attr_owner(die, "DW_AT_decl_file") + ln = binfmt.die_attr(die, "DW_AT_decl_line") + fname = None + if fi is not None: + files = binfmt.cu_file_table(dw, fi_owner.cu, file_tables) + if 0 <= fi.value < len(files): + fname = files[fi.value] + line = int(ln.value) if ln is not None else 0 + hint = (os.path.basename(fname) if fname else "", line) + by_name[name] = hint + by_address[int(die.attributes["DW_AT_low_pc"].value)] = hint except Exception: # noqa: BLE001 - return out - return out + return by_name, by_address + return by_name, by_address + + +def _dwarf_decl(binary_path: Path) -> dict[str, DeclHint]: + """Map function name -> ``(decl_file basename, decl_line)`` from DWARF. + + This is the backward-compatible name-keyed view. New C++ call sites should + pass a function address to :func:`function_source_ex`, which uses the + collision-safe address map instead. + """ + return _dwarf_decl_maps(binary_path)[0] + + +def _dwarf_decl_by_address(binary_path: Path) -> dict[int, DeclHint]: + """Map concrete function address -> declaration hint from DWARF.""" + return _dwarf_decl_maps(binary_path)[1] def _match_braces(text: str, open_idx: int) -> int | None: @@ -170,6 +197,16 @@ def extract_from_text(text: str, func_name: str, decl_line: int = 0) -> str | No When ``decl_line`` (1-based) is given, prefer the candidate nearest it. Returns the signature + body, or ``None`` if no definition is found. """ + from decbench.metrics.variable_features import function_definition_at_line, index_c_functions + + definitions = index_c_functions(text) + if any("::" in key or key.startswith(("operator", "~")) for key in definitions): + if decl_line: + selected = function_definition_at_line(text, func_name, decl_line) + if selected is not None: + return selected + candidates = definitions.get(func_name, ()) + return candidates[0] if len(candidates) == 1 else None name = re.escape(func_name) pat = re.compile(r"(? str | No continue j = body line_no = text.count("\n", 0, m.start()) - sig_start = text.rfind("\n", 0, m.start()) k = line_no while k > 0: prev = lines[k - 1].strip() @@ -225,8 +261,16 @@ def extract_from_text(text: str, func_name: str, decl_line: int = 0) -> str | No return snippet or None -def function_source_ex(binary_path: Path | None, func_name: str) -> tuple[str | None, str]: - """Best-effort source text for ``func_name`` plus a provenance/miss status. +def function_source_ex( + binary_path: Path | None, + func_name: str, + func_address: int | None = None, +) -> tuple[str | None, str]: + """Best-effort source text for one function plus a provenance/miss status. + + ``func_address`` is optional for backward compatibility. When supplied it + selects the DWARF declaration hint by concrete address, avoiding C++ + overload/scope collisions in the historical name-keyed declaration map. Searches the sources kept next to the binary (the compile stage writes them into ``compiled/``), guided by DWARF when available: the original ``.c``/ @@ -236,17 +280,28 @@ def function_source_ex(binary_path: Path | None, func_name: str) -> tuple[str | Returns ``(code, status)`` where ``status`` is ``""`` when ``code`` came from an original source, ``"preprocessed"`` when from a preprocessed unit, and one - of ``"binary_not_found"`` / ``"no_source_files"`` / ``"func_not_in_sources"`` - / ``"extract_failed"`` when ``code`` is ``None``. + of ``"binary_not_found"`` / ``"no_source_files"`` / ``"func_not_in_sources"`` / + ``"extract_failed"`` when ``code`` is ``None``. """ + # Report/evaluation layers may carry DecBench's canonical storage key + # instead of the semantic source name. Decode it here so existing callers + # automatically get address-aware C++ source recovery. + if func_address is None: + storage_match = re.fullmatch(r"(.+)@0x([0-9a-fA-F]+)", func_name) + if storage_match is not None: + func_name = storage_match.group(1) + func_address = int(storage_match.group(2), 16) + if binary_path is None: return None, "binary_not_found" binary_path = Path(binary_path) if not binary_path.parent.is_dir(): return None, "binary_not_found" - decl = _dwarf_decl(binary_path) - decl_file, decl_line = decl.get(func_name, ("", 0)) + if func_address is not None: + decl_file, decl_line = _dwarf_decl_by_address(binary_path).get(func_address, ("", 0)) + else: + decl_file, decl_line = _dwarf_decl(binary_path).get(func_name, ("", 0)) decl_stem = os.path.splitext(decl_file)[0] if decl_file else "" search_dir = binary_path.parent @@ -284,9 +339,10 @@ def function_source_ex(binary_path: Path | None, func_name: str) -> tuple[str | return None, "extract_failed" -def function_source(binary_path: Path, func_name: str) -> str | None: - """Best-effort source text for ``func_name`` (back-compat wrapper). - - Thin wrapper over :func:`function_source_ex` that drops the status code. - """ - return function_source_ex(binary_path, func_name)[0] +def function_source( + binary_path: Path, + func_name: str, + func_address: int | None = None, +) -> str | None: + """Best-effort source text for one function (back-compat wrapper).""" + return function_source_ex(binary_path, func_name, func_address)[0] diff --git a/docs/cpp-evaluation-qualification.md b/docs/cpp-evaluation-qualification.md new file mode 100644 index 0000000..a0b46fa --- /dev/null +++ b/docs/cpp-evaluation-qualification.md @@ -0,0 +1,127 @@ +# C++ evaluation qualification gates (proposal) + +This is an experimental qualification proposal for PR #103. It does not adopt +an official C++ metric, expand a scoring population, or change the default +GED/type/byte metrics or Union. Maintainers must approve a policy before adoption. + +## Keep four evidence levels distinct + +1. **Packaging:** immutable image, matching debug companion, preprocessed input + and effective build commands. Preserve hashes, UUID/GUID/Age and original paths. +2. **Definition identity:** one native entry, one TU and one exact qualified definition. + Every retained first-PC file/line candidate must resolve to that same definition. No basename or same-name fallback, including overloads. +3. **Syntactic CFG:** a parsed isolated definition with directed entry/exit roles. + This is what `cpp-ged-origin-v3-diagnostic` currently reports. Its eligible + population is not a population qualified for full C++ semantics. +4. **Semantic/effect qualification:** concrete instantiated definition, resolved + object lifetimes and exceptional exits under the original build configuration. + A syntactic match or compiler acceptance cannot supply this evidence alone. + +### Reused native preparation + +The C++ GED and type contexts now track canonical PDB/dSYM filesystem versions, +including an absent companion. Replacement, removal, appearance or modification +invalidates preparation; changes during reader preparation also fail evaluation. +The readers still perform UUID/GUID/Age validation. Filesystem versions are not a +cryptographic attestation against same-metadata tampering. No new debug search +path, copied debug input, or alternate companion is introduced. + +Source indexes retain the exact decoded preprocessed input-text digest. Missing file/line evidence +now still reports native units, physical unit and compilation directory when +available, so recollection can target the original build. These fields do not +make an unproven source match eligible. When a PDB retains multiple file/line +rows at the same first PC, every candidate must uniquely resolve within the +proven TU to the same exact parsed definition. An unmapped candidate, different +definition or ambiguity abstains. The reader preserves the complete candidate +set separately from its legacy singular file/line hint; it does not select the +smallest line, use the symbol name to choose a candidate, or bypass template +qualification. See [the frozen replay](validation/cpp-first-pc-locations.md). Explicit debug-path overrides are not +accepted by these contexts; their snapshot covers the default native readers. + +## Template policy + +Generic template syntax is never substituted for a concrete binary instance. +A future instantiated-CFG profile must retain: + +- exact native identity and range, specialization arguments and selected overload; +- complete original TU, macros, includes and compiler/target configuration; +- concrete semantic AST and instantiated body after `if constexpr`, substitution, + overload resolution and dependent lookup; +- explicit handling of shared/ICF entries and compiler-generated variants. + +Partial/explicit specialization and non-type arguments need the same proof. +No generic textual replacement of `T`, no choosing the largest body, and no +reselection of easier functions. Current unsupported instances remain excluded +from the diagnostic source population, with the original request retained. + +## Lifetime and exceptional-path policy + +The current diagnostic does not qualify implicit construction/destruction, +member/base initialization, temporaries, defaulted/deleted special members, +cleanup on early return, exception propagation, landing pads or unwinding. +Absence of `throw` in an isolated body does not prove that its calls cannot throw. + +A future effect profile must use the same representation on source and producer +sides and retain normal/exceptional successors, ordered effect/call sites, object +construction/destruction and cleanup on every exit. Compiler ABI entry variants +and synthesized thunks require explicit identity. Do not compare a source graph +with implicit effects to an ordinary pseudocode CFG and call the difference a +decompiler error. Until both sides are qualified, report this dimension as +unmeasurable separately from the existing syntax-only diagnostic. + +The existing source/ABI object type graph is a separate storage/type dimension; +complete field layout does not prove lifetime, RTTI or exception correctness. + +## Recompilation contract + +An eventual C++ byte comparison requires, independently of compilability: + +- original compiler family/version and target triple, ABI and data model; +- exact per-TU compile/link flags, optimization/inlining, CPU features and macros; +- runtime/standard-library ABI, exception/unwind, RTTI, packing and visibility; +- exact source/header provenance and native function/entry variant correspondence; +- object format, architecture and bitness validation before byte extraction; +- an explicit policy for generated declarations/scaffolding, relocations and + link-time changes (including LTO and ICF). + +Missing/incompatible oracle or toolchain evidence abstains explicitly. For an +otherwise qualified population, producer compile failure remains a producer +failure rather than disappearing from the denominator. Clang targeting Windows +is not native MSVC; Clang targeting ELF is not the corpus's GCC 13.3. A matching +object triple alone is insufficient. The legacy byte metric remains unchanged. + +### Reproducible bounded compiler control + +```sh +PYTHONPATH=. python scripts/audit_cpp_toolchains.py \ + --compiler clang++ --source tests/fixtures/cpp_qualification.cpp \ + --output /absolute/path/to/artifacts/toolchain-fixture +``` + +The optional development script compiles a header-free explicit fixture for +ELF/AArch64, COFF/x86-64 MSVC ABI and Mach-O/arm64, at O0, O2 and O2-noinline. +It preserves compiler commands, source/preprocessing/AST/IR hashes and object +identities. It verifies data-model assertions, template instantiation, construction, +throw/exception IR and identical code sections after preprocessing roundtrip. +It neither links nor executes the fixture and produces no corpus score. +No compiler is added to the runtime dependency set. + +The fixture proves only this bounded compiler path. It does not qualify original +MSVC/GCC versions, standard-library integration, recovered C++ compilation or +execution parity. Clang's [AST documentation](https://clang.llvm.org/docs/IntroductionToTheClangAST.html), +[cross-compilation guide](https://clang.llvm.org/docs/CrossCompilation.html) and +[toolchain components](https://clang.llvm.org/docs/Toolchain.html) describe the +compiler facilities used by this development probe. + +## Rust-Joern candidate adoption gate + +Replay identical prepared bytes with proven producer defects rejected first. +Retain every changed source/producer graph, directed topology and entry/exit +roles; graph counts alone cannot establish parity. Capture original Joern +references in isolated interpreters without the replacement shim. Differences +require an owner-level explanation and regression coverage before pin adoption. +Previously parsed invalid outputs stay rejected regardless of candidate syntax +support. Original-Joern agreement itself is not a C++ semantic certificate. + +Keep the existing dependency pin until this review is complete. Candidate replay +results remain counterfactual diagnostics, separate from official scores. diff --git a/docs/cpp-native-debug.md b/docs/cpp-native-debug.md new file mode 100644 index 0000000..3129082 --- /dev/null +++ b/docs/cpp-native-debug.md @@ -0,0 +1,93 @@ +# C++ corpus: native debug transport + +## Scope of this change + +The qualification pilot contains 18 ELF/DWARF, 18 native MSVC PE/PDB and 9 +Mach-O/dSYM images. The first implementation step adds thin Mach-O detection, +file-backed executable regions, linked address bases and UUID-matched dSYM +transport to the existing `binfmt` owner. Existing DWARF function identities, +source declaration hints and `NativeCodeResolver` consume that same reader. + +This change depends on PR #91's collision-safe C++ function identity work and +includes upstream main through `14e14b87429043aa06f64e067847ac0a925e41a6`. +It adds no production dependency: LIEF, pyelftools and Capstone already exist. + +## Reader contract + +`binfmt.dwarf_info(image)` reads embedded DWARF or the canonical companion: + +```text +image.dSYM/Contents/Resources/DWARF/image +``` + +An evaluator can also pass `debug_path=` pointing to that bundle or its DWARF +file. A companion must be a thin Mach-O with the image's architecture, address +width and LC_UUID. Mismatching or missing companions return no DWARF. Universal +images are not selected implicitly. `__debug_str_offs` is translated to DWARF's +full `.debug_str_offsets` name. + +The reader does not search unrelated directories or copy debug files to +producer inputs. The isolated qualification inputs have no adjacent dSYM. +Mach-O address bases exclude `__PAGEZERO` and use file-backed load segments; +both dylib base zero and executable base `0x100000000` occur in the measured +corpus. Function bounds and instruction starts come from the native image, +not the dSYM's placeholder code sections. + +TypeMatch cache generation moves from 19 to 20 because Mach-O ground truth that +previously appeared absent can now be read. The C++ type normalization and +correspondence policy are unchanged. + +## Measured corpus evidence + +Data snapshot: `sjkim1127/decbench-cpp-qualification` at +`07dd95dbe6214b8a78fc1e4a55961ab515986489`. + +| Check | Before | After | +|---|---:|---:| +| macOS images recognized / readable dSYM | 0 / 9 | 9 / 9 | +| recorded macOS source-function ranges resolved | unavailable | 4,129 / 4,129 | +| ELF images with readable DWARF | 18 / 18 | 18 / 18 | +| MSVC images with readable DWARF | 0 / 18 | 0 / 18 | + +All nine macOS images cover three programs and O0/O2/O2-noinline. Every recorded +source function was resolved to its native instruction entry and exact extent. +A different project's dSYM was rejected by UUID. All isolated macOS inputs +returned no evaluator DWARF. ELF/PE format detection and CU counts match the +45-image baseline. Snappy ELF address-keyed type ground truth and source +declaration fingerprints are identical before/after across all three modes. + +These are real-corpus transport/provenance checks, not recovered-code execution +parity, a full test-suite run, C++ type-policy validation or a decompiler score. +The local JSON reports, logs and reproduction probes are under Fission's +`benchmark/artifacts/cpp-native-debug/2026-10-13/` in the operator workspace. +The portable [evidence summary](validation/cpp-native-debug-summary.json) +records the immutable data revision, validated code commit, per-image counts, +ELF comparison fingerprints and validation limits, without local binary paths. + +## Review and submission + +The feature is prepared separately from PR #91. While #91 remains open, a +`main`-based PR from this branch would include its prerequisite changes. +After #91 merges, replay this feature and its evidence-only follow-up onto +the then-current upstream main and inspect the resulting diff before opening +the separate PR. Do not merge the prerequisite into upstream main from this +branch or publish scores as part of the reader change. + +Full repository regression tests and an end-to-end metric run remain pending. +The repository's Pages workflow deploys existing site files; it is not a +Python regression-test gate. Native debug transport checks should be reviewed +separately from those remaining validations. + +## Remaining evaluation work + +1. Native PDB function/storage/type ground truth, using CodeView GUID/Age and + address-qualified identity. A PE parser is not a PDB type reader. +2. MSVC compiler/ABI provenance and a matching recompile lane; a MinGW compiler + must not be treated as an MSVC recompiler merely because both emit PE. +3. Explicit unsupported/missing-oracle eligibility, rather than interpreting + unavailable truth as a decompiler failure or perfect result. +4. A versioned C++ scoring contract separating source-level types, object + layout, ABI representation and legacy C-oriented correspondence. +5. End-to-end source/decompiler CFG integration and corpus scoring with an + agreed population. Native transport availability alone does not make any + qualification record scoring-ready. diff --git a/docs/cpp-pdb-ground-truth.md b/docs/cpp-pdb-ground-truth.md new file mode 100644 index 0000000..51f02ea --- /dev/null +++ b/docs/cpp-pdb-ground-truth.md @@ -0,0 +1,87 @@ +# C++ definition and native PDB transport + +## Measured motivation + +The qualification pilot at data revision +`07dd95dbe6214b8a78fc1e4a55961ab515986489` contains 45 linked images. +Its fixed 250-address native run exposed three qualified C++ definitions +(constructor, assignment operator and comparison operator) that the C grammar +did not index. All 18 MSVC images were recognized as PE but had no readable +DWARF, leaving 97 requested Windows functions outside type measurement. + +## Contracts before implementation + +* Keep the existing C grammar and C identities for C input. Parse C++ + declarators with a C++ grammar, retain namespace/class qualification and + reject ambiguous short-name joins. Do not rename code to manufacture C. +* Read PDB 7/MSF, DBI module procedures and TPI/IPI records through one native + ground-truth owner. No text-dump regex or corpus manifest is an oracle. +* A sibling PDB must match the PE CodeView RSDS GUID and Age, DBI architecture + and section mapping. Do not follow an embedded absolute producer path. + Missing, truncated, mismatched or unsupported debug information is unmeasurable. +* Preserve binary VA, RVA, module provenance, exact procedure ranges, type + identity and storage base separately. Never borrow another overload or ICF + alias's variable types. Unknown records are retained as exclusion reasons. +* Parameter flags are not parameter ordinals. Do not infer ordinals from record + order or types. Legacy records without a parameter flag and procedures with + unsupported variables must not become partially perfect type scores. +* Live ranges are not variable references. Address evidence requires actual + decoded register/memory references within supported CodeView storage ranges. + Do not assign every instruction in a lifetime to a variable. +* PDB types expose source spelling and ABI storage. This change connects the + existing diagnostic matcher; it does not define a new C++ correctness policy. +* A PE format alone does not establish a MinGW compiler. Native MSVC/PDB + byte_match must abstain until matching compiler provenance/recompilation exists. + +## Validation scope + +Rerun definition indexing on both tools' fixed 250 outputs. Compare the +unchanged C definitions before/after and inspect the three motivating C++ rows. +Read all 18 real PE/PDB pairs, compare procedure ranges with independently +packaged LLVM inventories, reject wrong GUID/Age and truncated input, and +confirm isolated stripped inputs cannot acquire evaluator PDBs. Run the +unchanged type matcher on the same 97 Windows requests with caches disabled; +report unsupported/ambiguous functions separately from decompiler failures. +Full repository tests and official C++ GED/recompile/Union remain separate gates. + +## Current support and eligibility + +`binfmt.pdb_info(image)` returns the matching sibling PDB's exact procedures, +source/module provenance, scalar/pointer/reference/array/named aggregate type +records and supported CodeView storage ranges. `NativeCodeResolver`, address +identities, source ownership and source hints consume that same reader. C13's +first-PC source rows are preserved as a candidate set, not a recovered C++ +declaration. The legacy singular file/line hint remains present only for one +unambiguous pair. The experimental CFG join may accept multiple pairs only +when all uniquely resolve to the same exact definition in the proven TU; see +[the bounded first-PC qualification](validation/cpp-first-pc-locations.md). Original PDB names are preserved, including backticks and templates. + +The reader merges an MSVC legacy home-copy record only with one S_LOCAL having +the same source name, type index and lexical scope. This is an oracle-internal +symbol join; decompiler correspondence remains type-blind. Shadowed variables, +unknown symbols/types/storage pieces, dynamic frames and ICF ambiguities abstain. +PDB frame offsets are retained in transport metadata, not converted to backend +stack coordinates. Parameter ordinals are never invented. + +The existing matcher can be exercised with +`MetricConfig(use_cache=False, extra_options={"pdb_abi_diagnostic": True})`. +This explicit diagnostic compares the ABI projection; source spelling remains +separate (for example MSVC `long` is 32 bits and a source reference is passed +as a pointer). Default type scoring abstains because the source-level C++ +policy is not qualified. The diagnostic also abstains for producers without +native address evidence. No supported subset of a partially unreadable function +is graded. Cache generation is 21; ELF and Mach-O retain their existing policy. + +This is not complete PDB support: parameter classification without S_LOCAL, +member/function pointers, partial storage pieces, OMAP, ARM PDBs, FASTLINK/type +server records and object-field correctness need further work. Neither default +Windows scores nor an official C++ Union/ranking are enabled by this transport. + +## Format references + +The transport implements public on-disk contracts described by LLVM: +[MSF](https://llvm.org/docs/PDB/MsfFile.html), +[DBI](https://llvm.org/docs/PDB/DbiStream.html), +[CodeView types](https://llvm.org/docs/PDB/CodeViewTypes.html). +LLVM tools are used as independent inspection references, not as a required +runtime parser. The only new package is the C++ Tree-sitter grammar. diff --git a/docs/cpp-producer-contract.md b/docs/cpp-producer-contract.md new file mode 100644 index 0000000..d7a7b6c --- /dev/null +++ b/docs/cpp-producer-contract.md @@ -0,0 +1,82 @@ +# Experimental C++ producer inspection + +This contract applies to the opt-in `cpp-ged-origin-v3-diagnostic` and +`cpp_type_match` supported-subset proposal. Default GED, legacy `type_match`, +byte matching, Union and the existing Rust-Joern dependency pin are unchanged. +It is not official C++ admission policy or a compiler validation service. + +## Language and declaration assertions + +A producer can add `metadata.output_contract` to a function: + +```json +{"version":"producer-language-v1", "surface":"c-like-pseudocode", + "declarations":"partial", "dialect":"ghidra-c"} +``` + +`ProducerLanguageContract` validates this optional payload. `surface` is one of +`c`, `cpp`, `c-like-pseudocode`, `unknown`; `declarations` is `partial`, +`complete`, or `unknown`. Unknown versions, fields and values raise a transport +validation error rather than silently assuming a language. Legacy records +without the payload remain `unknown`. `complete` is a producer assertion, not +a certificate of valid source. The four native adapters label their current +text as C-like pseudocode with partial declarations; no tool is assumed to +produce complete C++ merely because names contain `::`. + +These fields are additive metadata. The adapters do not change source strings, +variable lists, native PCs or line maps to satisfy the contract. + +## Bounded defects and explicit uncertainty + +The shared inspection reads the existing declaration grammar and detects only: + +- Numeric literals used as qualified-name components, outside comments, + strings and preprocessing directives. +- The explicit `[D] unsupported_Iop_…(...)` IR placeholder in executable text. +- Unambiguous repeated ordinary object/parameter bindings in one compound + block. Only builtin/elaborated local declarations and simple formal + declarators participate; custom-type ambiguity, prototypes, extern and type + aliases do not establish a duplicate. Nested-block shadowing is preserved. + +Inspection retains exact text SHA256, 1-based byte coordinates, the previous +binding's line, grammar error state, and the producer contract. It does not +rewrite expressions, invent declarations, rename aliases, delete effects or +use source/debug oracle information. Byte-offset coordinates avoid the +Tree-sitter 0.26 `Point.column` crash reported in +[py-tree-sitter #487](https://github.com/tree-sitter/py-tree-sitter/issues/487). + +A finding yields `validity=invalid_source_output`. **No findings yields +`validity=not_established`**, including when the grammar reports no errors. +This is not a full C/C++ binding/type check, declaration completeness check, +ABI pseudocode semantics validator, or proof of CFG/execution parity. + +## Consumers and denominator integrity + +C++ GED inspects the same sanitized/preprocessed text handed to the CFG +frontend. A proven defect yields `invalid_producer_output` and no CFG. Parser +rejection remains `parser_failure` with either +`frontend_rejected_recognized_syntax` or `unresolved_syntax_or_dialect`; neither +means that the entire function is valid C++ or that the frontend is at fault. +A `matched` record means only CFG connection, with validity still explicit. +`source_eligible`, producer statuses and `paired` remain separate. The metric +remains paired-only diagnostic data and must not be presented as an official +rank or an improvement when an invalid producer is removed. + +C++ types inspect raw recovered text after oracle eligibility has already been +established. A proven-invalid producer receives zero with +`producer_failure=invalid_producer_output`, retaining that function and every +oracle variable in the metric population. It is not an oracle abstention. +Unsupported oracle evidence remains a separate exclusion. Inspection faults, +unexpected parser exceptions and invalid transport contracts propagate as +failures instead of shrinking eligibility. The experimental type cache +version is 5; its result metadata includes the inspection evidence. + +A future official policy still needs decisions on pseudocode dialects, +producer-owned declaration context, broad semantic validation and unsupported +C++ features. Do not admit every new native parse merely by updating the +Rust-Joern pin. No whitelist/blacklist of functions, addresses or sample IDs is +used by these checks. + +See [C++ qualification gates](cpp-evaluation-qualification.md) for separate +source identity, instantiated/effect semantics, candidate adoption and compiler +qualification requirements. diff --git a/docs/cpp-type-policy-proposal.md b/docs/cpp-type-policy-proposal.md new file mode 100644 index 0000000..eae3c24 --- /dev/null +++ b/docs/cpp-type-policy-proposal.md @@ -0,0 +1,238 @@ +# C++ type policy v2 — proposal for upstream review + +Status: **experimental proposal, not an official DecBench policy**. This is a +separate, explicitly configured `CppTypeMatchMetric`. It is not registered among +default metrics and does not enter `type_match`, the public Union, or rankings. +Existing C scoring remains under its existing policy. Adoption requires upstream +review, a qualified population, and a new policy-versioned full rescore. + +## Why separate views + +A source `int&` and an ABI pointer are different answers to different questions. +MSVC `long` and GCC LP64 `long` have different widths. C++ `long` and `int` have +different language identities even where both have 32-bit storage. Removing +namespace qualification or treating any object with the same size as the same +type would lose more information than a C++ metric should permit. + +The ground truth retains source information independently from scoring. We +propose publishing **source** and **ABI** results side by side; neither is silently +substituted for the other. Which view, if either, enters Union is an upstream +policy decision. This implementation does not make that decision. + +## Implemented v2 comparison contract + +| Feature | Source view | ABI storage view | +| --- | --- | --- | +| Integer | signedness, width, fundamental rank | signedness and width | +| Float | category and width | category and width | +| `bool`, plain/UTF character types | distinct categories | distinct categories retained | +| `const`, `volatile` | compared at each indirection level | erased as no storage change | +| Ordinary pointer | level and pointee type | level and pointee representation | +| `T&`, `T&&` | distinct from each other and from `T*` | explicit pointer projection | +| Fixed arrays | all dimensions and element type | all dimensions retained; no decay | +| Oracle typedef | resolved by the debug type graph | resolved / projected | +| Recovered fixed-width/platform alias | no inferred fundamental rank credit | documented storage aliases supported | +| Unknown `undefinedN` | incorrect | incorrect; width alone earns no credit | +| Named complete objects | qualified identity and complete instance graph | complete instance layout graph; names erased | +| Enum | nominal identity, values, underlying type | underlying storage | +| Fields / ordinary bases | field names, offsets, types, access and base identity | offsets, field/base storage and relation | +| Virtual bases | explicit debug locator and base relation | locator and base storage; never offset zero | +| Vtables / RTTI | not a complete behavior/API metric | missing physical vptr evidence abstains | +| Function/member pointers | unsupported in v2 | unsupported in v2 | + +Plain `char` deliberately remains distinct from `signed char`/`unsigned char` in +both views; this first profile does not infer compiler-default character +signedness. Long double, wchar_t, nonstandard scalars and vector types abstain. +An explicit `LP64`, `LLP64`, or `ILP32` data model is mandatory. Oracle scalar and +pointer widths must agree with it. A recovered supplied size inconsistent with +its own type is incorrect. C++ RTTI and exception behavior are not type accuracy. + +The DWARF reader follows actual type references instead of the legacy flattened +type-name sets, retaining reference category, nested cv and array dimensions. +PDB source and ABI spelling remain separate. PDB modifiers on a pointer now +describe the pointer (`int* const`), not its pointee (`const int*`). + +## Identity, correspondence, and eligibility + +1. Select an oracle function by exact image entry address. Ambiguous/ICF identities + abstain; no name-only overload borrowing. +2. Exclude optimized-out variables uniformly. An unsupported oracle type, + partial/computed storage, inline identity, incomplete formal classification, + or unavailable correspondence makes the **whole function** unmeasurable. + Never grade only its easy variables and call it perfect. Explicit + `UnsupportedType` and native `PdbFormatError` are oracle abstentions only + during oracle preparation. An unrelated parser/evaluator `ValueError` must + surface as an evaluation failure; recovery scoring runs outside that boundary. +3. Reuse type-blind ABI argument, calibrated stack, and native address matching. + PDB exposes no proven argument ordinals or backend stack origins; it therefore + uses native addresses only. DWARF aggregate returns disable unproven + formal ordinals; remaining correspondence must stand independently. Thresholds stay at overlap 0.1 / ambiguity margin 0.03. +4. Validate every producer PC against the exact function's decoded instruction + starts. Validation proves instruction membership, not that the producer's + variable association is semantically correct. The producer owns that claim. + A generated/demangled name may differ from the oracle spelling; the shared + resolver admits a unique exact entry identity, and rejects ambiguous entries. +5. Missing code/evidence or unsupported recovered types are **incorrect**, not + oracle exclusions. Producer names/types never choose variable matches. +6. Score correct oracle variables / all eligible oracle variables. Perfect means + every one is correct. Extra recovery variables are reported, not used to pad + the denominator. Report eligible count, exclusions by reason, correspondence + coverage, and type decisions alongside perfect rates. + +The two producer runs must share exactly the same oracle-eligible function set. +These supported-subset results must not be extrapolated to all C++ or combined +with the legacy C headline. Unmeasurable is not zero; missing recovery on an +otherwise eligible function is zero. + +Virtual-base locators still compare native debug encodings. This can be stricter +than binary-observable semantic equivalence across DWARF and CodeView; no +cross-format normalization is claimed. Maintainers must resolve this policy +question before official adoption. + +## API + +```python +from decbench.metrics.base import MetricConfig +from decbench.metrics.cpp_type_match import CppTypeMatchMetric + +metric = CppTypeMatchMetric(MetricConfig( + use_cache=False, + extra_options={"view": "source", "data_model": "LLP64"}, +)) +score = metric.compute_for_binary(result) +# Repeat with view="abi"; preserve separate result labels and populations. +``` + +A reproducible single-image entry point consumes canonical producer JSON and a +fixed requested population, without invoking a producer: + +```bash +python scripts/evaluate_cpp_type_policy.py --binary original-with-debug \ + --decompilation producer-result.json --requested-functions functions.json \ + --view source --data-model LP64 --output cpp-source.json +# Repeat with --view abi and a distinct output file. +``` + +Optional `preprocessed_sources=[Path(...)]` supplies evaluator-only TU/source +line correspondence. It must never be passed to the producer. This branch adds +`tree-sitter-cpp>=0.23,<0.24` as a normal runtime dependency for C++ definition +extraction. The type profile itself adds no further runtime dependency. It does +not use disk score caches; its policy version is `cpp-types-v2-proposal`, and +its evaluator/cache revision is 3. + +For a fixed comparison, pass the same `requested_functions=[(name, address), ...]` +to both producers' `compute_for_binary` calls. Missing entries become empty +recoveries before oracle eligibility is decided. An in-memory `CppOracleContext` +can share debug indexing and native instruction validation across views/tools; +it is not a persisted score cache. Keep the image and companion debug artifacts +immutable for that evaluation run. + +## Fission producer integration + +The raw `fission` backend replaces the previously non-runnable external entry. +Set `FISSION_BIN`, optionally `FISSION_RESOURCE_ROOT` and +`FISSION_WORKING_DIR`. Resolve the executable (including a relative +`FISSION_BIN` or PATH entry) to an absolute path before changing the child +working directory. It runs NIR, uses one load per requested image, sends only +entry addresses to `--addresses-file`, enforces timeouts, and forwards native +variable `addresses` unchanged into `VariableInfo`. + +Fission's existing conservative scalar-SSA variable definition/use PCs remain +primary evidence. The companion Fission change also exports `line_mappings_nir` +and `line_mappings_hir`, each containing 1-based physical `line_number`, native +instruction `addresses`, and `defined_variables`. The adapter takes NIR maps +only. These are **partial assignment-result definition origins**, not all RHS +references or complete effect lineage. Header, predicate, ambiguous/cloned, +phi/input, repeated-definition and unproved lines remain unmapped. Old Fission +versions continue to transport direct variable PCs with no invented line maps. + +`LineMapping.defined_variables=None` preserves the historical whole-line +contract. A supplied list scopes ownership: only those bindings can inherit +that line's PCs. Even on a scoped row a direct variable PC list takes precedence. +The adapter gives only those variables line occurrences; evaluation repeats the +same scope check. A scoped display name must be nonempty and unique among +all variables in the function. Shadowed/duplicate names inherit no scoped PCs; +direct per-variable addresses remain primary and legacy whole-line maps retain +their historical contract. A future stable binding-ID transport would be needed +to resolve those ambiguous names. Scoped rows outside the returned code are +rejected. Native instruction validation proves membership, not the producer's +semantic claim. + +The legacy metric now recognizes direct variable PCs as well as line PCs when +choosing the native path (cache generation 24, including ambiguous-scope +rejection). Functions with neither retain their prior fallback behavior. Address validation remains mandatory at producer +and final evaluator boundaries. PDB default scoring still abstains; +`pdb_abi_diagnostic=True` remains a separate legacy diagnostic. + +## Complete object graph contract + +The native DWARF reader follows DIE edges for instance members, literal byte +positions or `data_bit_offset`, ordinary and virtual inheritance, enums, arrays, +modifiers and recursive ordinary pointers. A forward-only or anonymous object, +unsupported method/member pointer, legacy bit offset or uncertain location +abstains. CodeView follows actual TPI indices, LF_FIELDLIST continuations, +LF_MEMBER/BCLASS/VBCLASS/IVBCLASS, bitfield leaves and enum constants. A forward +record must resolve to exactly one complete identity; conflicting definitions +abstain. Methods/static members are not instance storage. CodeView VFUNCTAB +lacks its physical position: such a graph abstains rather than guessing zero. + +Producer output can supply `metadata.type_graphs[complete_type_spelling]`: + +```json +{ + "schema": "decbench-cpp-type-graph-v1", + "root": "object", + "nodes": { + "object": {"kind": "record", "size": 4, "name": "demo::Object", "complete": true, + "edges": [{"role": "field", "target": "integer", "name": "value", + "offset_bits": 0, "access": 3}]}, + "integer": {"kind": "signed", "size": 4, "rank": "int", "complete": true} + } +} +``` + +Nodes use byte sizes; edges use bit offsets. All targets must exist. Partial +nodes, impossible extents, by-value cycles and missing virtual locators are +rejected. Pointer cycles are compared structurally; local node IDs and edge +ordering do not earn credit. Source comparison retains qualified names, cv, +rank, field names/access, enum values and inheritance. ABI comparison erases +names/cv/rank/access, projects references to pointers and enums to their +underlying storage. It retains base relations: it does not infer a flattened +base from a similarly named or sized recovered field. Virtual locators retain +their debug encoding; cross-format locator equivalence is not inferred. +Instance fields and bases are compared; this is not a complete class API metric. + +Complete ordinary emitted C `struct`/`union` and typedef definitions are also +read with Tree-sitter. Natural layout is limited to explicit LP64/LLP64, +supported scalar/pointer/array fields, and unambiguous complete definitions. +Unknown class ABI, inheritance, packing, bitfields and alignment attributes +require explicit producer graphs. ILP32 record alignment is not guessed. +Opaque nominal spellings are incorrect recovery, never oracle exclusions. + +## Validation and adoption + +The `C++ evaluation contracts` workflow runs focused policy/provenance/source +identity regressions on Ubuntu, compiling actual DWARF and CodeView/PDB fixtures +with Clang and lld. Rust-Joern is the existing CFG dependency, pinned for this +lane. Style and type checks cover new policy owners. Full local repository +results and baseline failures are reported separately; skipped real-decompiler +or platform tests are not end-to-end success. + +The fixed qualification measurements and exclusion inventory live in +`docs/validation/`. They describe a supported subset, not an official C++ +ranking. Upstream must agree on source versus ABI headline policy, templates, +anonymous/ODR types, split/inline locations, hidden parameters, source +provenance, and a balanced qualified population before adoption and full rescore. +A contribution must follow DecBench's mandatory human review policy. + +References: [DecBench methodology](https://decbench.com/about/), +[LLVM CodeView type records](https://llvm.org/docs/PDB/CodeViewTypes.html). + +### Producer inspection + +The supported-subset proposal now retains an optional producer language and +declaration contract plus bounded, oracle-free defect evidence. Invalid +recovered text receives zero after oracle eligibility; it never removes an +oracle-qualified function. See [producer inspection](cpp-producer-contract.md) +for scope, uncertainty and cache generation. CFG parsing remains separate from +source validity, and the diagnostic GED profile is version 2. diff --git a/docs/decompilers.md b/docs/decompilers.md index b9681bd..bdcd2bd 100644 --- a/docs/decompilers.md +++ b/docs/decompilers.md @@ -40,13 +40,18 @@ Backends subclass the `Decompiler` ABC (`base.py`) and register via `glaurung` results came from an external sample-set submission (Part III) at `git-fb4ee6b`, flagged `external_submission` in its metadata; the new sample-set pass uses this runnable backend. +- **Fission CLI** (`raw/fission_raw.py`: `fission`): set `FISSION_BIN` to + `fission_cli`, optionally `FISSION_RESOURCE_ROOT` and `FISSION_WORKING_DIR`. + The backend selects NIR, forwards per-variable scalar-SSA definition/use + addresses, and leaves line mappings empty when the producer has no + occurrence-level trace. See [C++ policy proposal](cpp-type-policy-proposal.md). - **Dockerized** (`dockerized.py`: `reko`/`retdec`/`r2dec`): Reko and RetDec split container-produced C into functions. r2dec uses `pdd` through its versioned image and returns address-keyed functions. Build images with `decbench decompiler-build `; Dockerfiles live in `docker/`. - **LLM / coding-agent** (`llm_dec.py`: `codex`, `claude-code`, `kimi-code`): Part II below. -- **External / not runnable here** (`external.py`: `fission`, `ventris`): ids we +- **External / not runnable here** (`external.py`: `ventris`): ids we publish but can never execute — their numbers arrive as sample-set submissions (Part III) and there is no tool to drive. Each is one row in `EXTERNAL_DECOMPILERS`; the generated class has `runnable = False`, @@ -869,3 +874,11 @@ submission zip out of git too (`private/` is ignored for exactly this). --base results/full_run/sample_set_manifest.json --drop-unscoreable \ -o /tmp/manifest_repaired.json # inspect the diff, then install it ``` + +### Fission scoped definition lines + +The native Fission adapter forwards `line_mappings_nir` when supplied by the +producer. Each `defined_variables` list scopes an assignment-result definition +PC to its LHS; it must not be assigned to RHS names. Maps are partial and +1-based in the returned NIR text. Old CLIs with direct variable addresses remain +supported. See [the C++ policy proposal](cpp-type-policy-proposal.md). diff --git a/docs/metrics.md b/docs/metrics.md index d728c5a..1a80b32 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -114,7 +114,10 @@ Compares decompiled variable types against DWARF ground truth (read via pyelftools). Works at **all opt levels**: ground truth keeps every variable with ANY DWARF location (register loclists included; only fully optimized-out vars are dropped). -Current `cache_version="17"`, bumped for agent-reported line-address support. +Current `cache_version="25"`. The latest bump covers the shared source/variable +parser choosing an error-free C++ AST when the C AST rejects valid reference +declarators. Existing valid C inputs retain their C grammar. Earlier generations +added agent-reported line addresses and native provenance validation. The per-function key covers source and decompiler address evidence, argument/stack anchors, recovered types, source selection diagnostics, and the matching thresholds. @@ -235,6 +238,21 @@ scoring-policy changes and must bump `cache_version`. variable was `void` and unmatchable for all decompilers (17.5% of leveldb's). C has no reference DIEs, so this rule cannot move a C result. +### Experimental C++ object/reference proposal + +The opt-in `CppTypeMatchMetric` remains separate from default `type_match` and +Union. Its cache generation is **4**. After filtering to the requested population, +multiple producer records at one native address are rejected before oracle +preparation or scoring; records outside the plan cannot expand the denominator. +Missing requested output remains empty recovery rather than an oracle exclusion. + +Its source view distinguishes `T&`, `T&&` and `T*`. Nominal object spellings are +tokenized with `&&` as one reference layer; invalid nested references are rejected +rather than interpreted as multiple ABI pointers. ABI projection maps one reference +layer to one pointer layer, preserving the remaining pointee structure. Recovered +object layout still requires the existing complete LP64/LLP64 evidence; these fixes +do not qualify new target ABIs, templates, lifetime or exception semantics. + ### Argument positions must be ABI positions Pass 1 pairs a DWARF formal parameter with the decompiled variable carrying the @@ -285,11 +303,20 @@ Assembly similarity after recompiling the decompiled C **the same way the source was compiled** — the toolchain and `-m*/-O*` flags matching the original binary's own format+arch (PE→MinGW, ARM→arm-none-eabi, and bare `gcc` only for an architecture the host builds natively — the cross triplet -otherwise; flags read from the DWARF producer), via `decbench/utils/binfmt.py`. +otherwise; bare `gcc` is selected only on Linux, and flags are read from the +DWARF producer), via `decbench/utils/binfmt.py`. Returns a non-scoring result (an **abstention**, not a 0) if that toolchain isn't installed — don't fake a wrong-arch recompile. Works on ELF and PE. -### The two fairness passes (`cache_version="7"`) +The emitted relocatable object must match the original format family, +architecture and bitness before byte extraction. Ordinary COFF objects map to +the PE family. Unknown/incompatible objects abstain and are excluded from the +binary aggregate, rather than scored as a failed decompilation. The selection +and object-validation boundary uses byte-match cache generation **9**. This does +not prove a matching C++ compiler version, standard library, exception model or +ABI options; it is not a native MSVC/Clang/GCC C++ qualification policy. + +### The two fairness passes Raw decompiler output rarely recompiles as-is (pseudo-types like `undefined4`, illegal tokens like `GLIBC_2.2.5::stderr`), so naive @@ -378,3 +405,69 @@ semantics — the fairness contract shared with the site — are specified in [site.md](site.md#denominator-semantics-must-not-drift). The published numbers come from the reeval overlays + guarded finalize, not raw checkpoint values — see [benchmarking.md](benchmarking.md#overlays-finalize-and-rebuilds--where-the-published-numbers-come-from). + +### Experimental C++ GED connection diagnostics + +`decbench.pipeline.cpp_ged.evaluate_cpp_ged()` and +`scripts/evaluate_cpp_ged.py` provide an opt-in **diagnostic** path. They do not +register a default metric, write result overlays, qualify a corpus, or contribute +to Union. GED graph arithmetic is unchanged; existing C extraction is unchanged. + +The path indexes every syntactic definition in a preprocessed `.ii` file and +retains physical-to-logical file/line origins for GCC/Clang `# N` and MSVC `#line N`. +A binary-local native entry must have one concrete DWARF/PDB identity and a +uniquely corresponding TU/file/line definition. Duplicate native entries (including +ICF), duplicate source definitions and missing evidence abstain explicitly. +No short-name, basename, largest-CFG or cross-TU fallback is used. + +When building a source index, a unique native request may retain its exact +defining header even when preprocessor system-header filtering would drop it. +Retention requires that header's complete logical path and a proven TU, and +does not change the primary TU or retain unrelated headers. Source input +evidence records `retained_origin_paths` and `filtered_origin_paths`; absent +source and filtered source are distinct failures. Prebuilt indexes can opt into +the same path with `SourceCfgIndex.add(..., native_records=...)`. No template +instantiation or source rewriting is performed by retention. + +Producer records are parsed independently with the non-reducing Rust Joern +`parse_code` API. One real definition, excluding prototypes, is associated with +the record's existing storage key; its emitted name may differ from the transport +name. Multiple real definitions are ambiguous and are not guessed by size/name. +A failing record does not discard unrelated records. + +Source CFG parsing is **definition-only** and accepts no parser error diagnostics. +An error-bearing Tree-sitter definition is reported as `source_syntax_error` +before comparing its recovered name or template shape. Error recovery can swallow +unrelated declarations into one apparent definition; that name is not reliable +evidence of a source/native mismatch. Such a span never reaches CFG parsing. +This preserves the selected preprocessed definition's explicit control flow but +has not qualified full-TU callee context, implicit object lifetime/exception +edges, or template specialization. Templates abstain. A source-eligible function +can still lack a measurable producer CFG; reports keep those populations separate. +The resulting `cpp_ged_diagnostic` values are not an official C++ GED score. + +```bash +DECBENCH_NO_CACHE=1 python scripts/evaluate_cpp_ged.py decompilation.json \ + --requested-functions requested.json --source captured.ii --output cpp-ged-diagnostic.json +``` + +The JSON must be a `DecompilationResult` whose `binary_path` points at the matching +native debug binary (with its matching PDB/dSYM where required). Reports include +per-function connection evidence, exclusions, source input hashes and the metric +results. No original program or decompiler is executed by this entry point. + +The requested manifest is a JSON list of `{ "name": "...", "address": 1234 }` +entries with unique nonnegative native entry addresses. Missing producer records +become empty recovery at those entries; extra records do not expand the population. +Reusable native indexes reject a different path or changed binary hash. Parser +error diagnostics are explicit failures; unavailable native parser infrastructure +and unexpected evaluator exceptions propagate instead of reducing eligibility. + +Relative preprocessor origins are resolved only against the native +`DW_AT_comp_dir`. For object-relative generated CU names, the optional +`--compilation-database compile_commands.json` transports a recorded original +input only when `-save-temps=obj`, `-c`, the input, `-o` object path, native CU and +compilation directory agree exactly. Conflicting commands reusing an output +provide no binding. Database/command hashes are retained; commands are never +executed. This evidence connects source units but does not independently prove +link membership or satisfy the corpus's remaining build-qualification gates. diff --git a/docs/validation/cpp-first-pc-locations.md b/docs/validation/cpp-first-pc-locations.md new file mode 100644 index 0000000..2ce73fa --- /dev/null +++ b/docs/validation/cpp-first-pc-locations.md @@ -0,0 +1,89 @@ +# PDB first-PC source location convergence + +Status: unsubmitted experimental follow-up to PR #103 at +`ad8ceafcbbefb3c9efa795a9bc443968e0b165e2`. This expands diagnostic oracle +coverage, not decompiler quality or official C++ scores. No producer code changes. + +## Measured failure and owner + +The frozen qualification pilot has 250 requests across 45 linked images at +dataset revision `07dd95dbe6214b8a78fc1e4a55961ab515986489`, sample SHA256 +`f432c528e3071efa60fdaecc132aafa8c351e386bf7203d252e2875d6de02620`. +Nineteen Windows requests lacked a singular native file/line hint. Inspection of +their original matching PDBs found seventeen with distinct C13 source rows at +one first PC and two with no line rows inside the procedure range. An independent +LLVM PDB line dump also retains duplicate-PC line records. This is native +provenance ambiguity, not permission to choose one line or infer it by name. + +The PDB owner now retains the complete deduplicated, sorted first-PC candidate +set. Its legacy singular `source_file`/`source_line` behavior is unchanged. +The experimental source-CFG join requires every retained candidate to resolve +uniquely, within its proven TU, to the same exact parser definition object. +Missing, ambiguous or conflicting candidates reject the join. Template, qualified +name, parser, ICF and source-unit gates still apply after convergence. No nearest +line, basename or same-name fallback is introduced. + +`cpp-ged-origin-v3-diagnostic` distinguishes the changed diagnostic contract. +The default GED/type/byte metrics, metric caches and Union remain unchanged. + +## Frozen replay + +All four tools reuse their saved output and the same 250 requested addresses. +No producer or corpus program is executed. Both evaluator versions freshly +parse the same source inputs with metric caches disabled and Rust-Joern pinned +to `9fd97f50885202596b125ebd2504e64910874bcb`. The proposed Rust-Joern syntax +candidate is not adopted by this experiment. + +| Tool | Source eligible before/after | Paired before/after | Distance zero before/after | +| --- | ---: | ---: | ---: | +| Fission | 54 / 56 | 54 / 56 | 18 / 19 | +| Kuna 1.808 | 54 / 56 | 53 / 55 | 21 / 23 | +| angr | 54 / 56 | 34 / 36 | 14 / 16 | +| Ghidra | 54 / 56 | 50 / 52 | 19 / 21 | + +The two newly connected native definitions are Windows OpenCC O2 +`opencc::UTF8Util::IdeographicDescriptionOperatorArity` (header lines 124/125) +and Windows Snappy O2-noinline `snappy::UNALIGNED_LOAD64` (lines 145/149). +Each pair resolves to one definition in its own proven TU. These rows were not +resampled and their raw producer texts and binary hashes are unchanged. + +Every previously matched source definition digest and every previous measured +GED result is identical. The complete source/ABI type profiles are identical +excluding computation time: 26 eligible functions per tool, perfect Fission 0, +Kuna 1, angr 0, Ghidra 0. No byte-match or ranking claim is made. + +Of the seventeen multi-location requests, two now match, eight reach the existing +unsupported-template boundary, one fails qualified-name correspondence, one has +a parser failure and five remain ambiguous. The two procedures without line +records still abstain. The original twenty-seven missing-TU requests also remain +unresolved: freshly rebuilt TUs cannot become evidence for older binary identities. + +A second fresh candidate replay is identical for all 45 cells and all four tools +after removing only `computation_time`. The compact population/evidence summary +is [retained here](cpp-first-pc-population.json); full source/CFG evidence, native +line audits, hashes and comparison scripts remain in local artifacts. + +## Validation and boundaries + +* Eleven new regressions exercise real C13 row transport, native candidate + transport, convergence, conflicting/unmapped/zero-line cases, overload ambiguity, + TU boundaries and retained template/name gates. +* Focused workflow locally: 327 passed, 5 platform-specific skips; selected + Black/Ruff and ten owner mypy checks pass. +* Full baseline: 997 passed / 19 failed / 24 errors / 28 skipped. +* Full candidate: 1,008 passed / 19 failed / 24 errors / 28 skipped. + All 43 failure/error identities are identical; no new identity remains. + A separate PDB-owner mypy audit retains its same eighteen pre-existing errors; + the focused mypy lane does not claim the entire repository is type-clean. +* No exact-head remote DecBench CI has run for this unsubmitted patch. Human + review under `docs/agents.md` is required before committing/submitting it. + +Multiple line rows proving one definition do not establish instantiated template +semantics, implicit lifetime/exception effects, source/ABI recompilation parity +or full PDB support. The corpus remains `qualified=false`, `scoring_ready=false`. + +Artifacts: Fission `benchmark/artifacts/cpp-evidence-recovery/2026-10-22/`. +The same frozen `qualification-v2/` files are now available on HF main as an +additive packaging promotion, while the immutable original dataset revision and +legacy corpus are preserved. That packaging operation is separate from this +evaluator change and does not alter any bytes used for this replay. diff --git a/docs/validation/cpp-first-pc-population.json b/docs/validation/cpp-first-pc-population.json new file mode 100644 index 0000000..683dc4c --- /dev/null +++ b/docs/validation/cpp-first-pc-population.json @@ -0,0 +1,415 @@ +{ + "summary": { + "baseline": { + "fission": { + "requested": 250, + "source_eligible": 54, + "paired": 54, + "perfect": 18, + "type_eligible": 26, + "type_perfect": 0 + }, + "kuna": { + "requested": 250, + "source_eligible": 54, + "paired": 53, + "perfect": 21, + "type_eligible": 26, + "type_perfect": 1 + }, + "angr": { + "requested": 250, + "source_eligible": 54, + "paired": 34, + "perfect": 14, + "type_eligible": 26, + "type_perfect": 0 + }, + "ghidra": { + "requested": 250, + "source_eligible": 54, + "paired": 50, + "perfect": 19, + "type_eligible": 26, + "type_perfect": 0 + } + }, + "candidate": { + "fission": { + "requested": 250, + "source_eligible": 56, + "paired": 56, + "perfect": 19, + "type_eligible": 26, + "type_perfect": 0 + }, + "kuna": { + "requested": 250, + "source_eligible": 56, + "paired": 55, + "perfect": 23, + "type_eligible": 26, + "type_perfect": 1 + }, + "angr": { + "requested": 250, + "source_eligible": 56, + "paired": 36, + "perfect": 16, + "type_eligible": 26, + "type_perfect": 0 + }, + "ghidra": { + "requested": 250, + "source_eligible": 56, + "paired": 52, + "perfect": 21, + "type_eligible": 26, + "type_perfect": 0 + } + } + }, + "new_rows": [ + { + "cell": "24", + "tool": "fission", + "name": "opencc::UTF8Util::IdeographicDescriptionOperatorArity", + "after": { + "address": 5369048736, + "native": "pdb:src\\CMakeFiles\\libopencc.dir\\Converter.cpp.obj:140052ea0:opencc::UTF8Util::IdeographicDescriptionOperatorArity", + "native_name": "opencc::UTF8Util::IdeographicDescriptionOperatorArity", + "physical_unit": null, + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/opencc/src/utf8util.hpp", + "line": 124, + "native_qualified_name": null, + "native_location_candidates": [ + { + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/opencc/src/utf8util.hpp", + "line": 124 + }, + { + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/opencc/src/utf8util.hpp", + "line": 125 + } + ], + "native_location_policy": "all_first_pc_locations_same_definition", + "input": "preprocessed/00014.ii", + "definition_sha256": "705c289275160bf527c39ec45ebca7287079454a6d7d6ff5119bda5b37499e51", + "span": [ + 402876, + 402900 + ], + "status": "matched", + "native_unit_count": 274, + "native_units_sha256": "52d79e36a4591a2072bbcbb8aba22ac13766cc06ac763ebe21653f6016ee50ab" + }, + "metric": { + "value": 6.0, + "raw_value": 6.0 + }, + "before_status": "missing_native_source_location" + }, + { + "cell": "24", + "tool": "kuna", + "name": "opencc::UTF8Util::IdeographicDescriptionOperatorArity", + "after": { + "address": 5369048736, + "native": "pdb:src\\CMakeFiles\\libopencc.dir\\Converter.cpp.obj:140052ea0:opencc::UTF8Util::IdeographicDescriptionOperatorArity", + "native_name": "opencc::UTF8Util::IdeographicDescriptionOperatorArity", + "physical_unit": null, + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/opencc/src/utf8util.hpp", + "line": 124, + "native_qualified_name": null, + "native_location_candidates": [ + { + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/opencc/src/utf8util.hpp", + "line": 124 + }, + { + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/opencc/src/utf8util.hpp", + "line": 125 + } + ], + "native_location_policy": "all_first_pc_locations_same_definition", + "input": "preprocessed/00014.ii", + "definition_sha256": "705c289275160bf527c39ec45ebca7287079454a6d7d6ff5119bda5b37499e51", + "span": [ + 402876, + 402900 + ], + "status": "matched", + "native_unit_count": 274, + "native_units_sha256": "52d79e36a4591a2072bbcbb8aba22ac13766cc06ac763ebe21653f6016ee50ab" + }, + "metric": { + "value": 0.0, + "raw_value": 0.0 + }, + "before_status": "missing_native_source_location" + }, + { + "cell": "24", + "tool": "angr", + "name": "opencc::UTF8Util::IdeographicDescriptionOperatorArity", + "after": { + "address": 5369048736, + "native": "pdb:src\\CMakeFiles\\libopencc.dir\\Converter.cpp.obj:140052ea0:opencc::UTF8Util::IdeographicDescriptionOperatorArity", + "native_name": "opencc::UTF8Util::IdeographicDescriptionOperatorArity", + "physical_unit": null, + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/opencc/src/utf8util.hpp", + "line": 124, + "native_qualified_name": null, + "native_location_candidates": [ + { + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/opencc/src/utf8util.hpp", + "line": 124 + }, + { + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/opencc/src/utf8util.hpp", + "line": 125 + } + ], + "native_location_policy": "all_first_pc_locations_same_definition", + "input": "preprocessed/00014.ii", + "definition_sha256": "705c289275160bf527c39ec45ebca7287079454a6d7d6ff5119bda5b37499e51", + "span": [ + 402876, + 402900 + ], + "status": "matched", + "native_unit_count": 274, + "native_units_sha256": "52d79e36a4591a2072bbcbb8aba22ac13766cc06ac763ebe21653f6016ee50ab" + }, + "metric": { + "value": 0.0, + "raw_value": 0.0 + }, + "before_status": "missing_native_source_location" + }, + { + "cell": "24", + "tool": "ghidra", + "name": "opencc::UTF8Util::IdeographicDescriptionOperatorArity", + "after": { + "address": 5369048736, + "native": "pdb:src\\CMakeFiles\\libopencc.dir\\Converter.cpp.obj:140052ea0:opencc::UTF8Util::IdeographicDescriptionOperatorArity", + "native_name": "opencc::UTF8Util::IdeographicDescriptionOperatorArity", + "physical_unit": null, + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/opencc/src/utf8util.hpp", + "line": 124, + "native_qualified_name": null, + "native_location_candidates": [ + { + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/opencc/src/utf8util.hpp", + "line": 124 + }, + { + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/opencc/src/utf8util.hpp", + "line": 125 + } + ], + "native_location_policy": "all_first_pc_locations_same_definition", + "input": "preprocessed/00014.ii", + "definition_sha256": "705c289275160bf527c39ec45ebca7287079454a6d7d6ff5119bda5b37499e51", + "span": [ + 402876, + 402900 + ], + "status": "matched", + "native_unit_count": 274, + "native_units_sha256": "52d79e36a4591a2072bbcbb8aba22ac13766cc06ac763ebe21653f6016ee50ab" + }, + "metric": { + "value": 0.0, + "raw_value": 0.0 + }, + "before_status": "missing_native_source_location" + }, + { + "cell": "33", + "tool": "fission", + "name": "snappy::UNALIGNED_LOAD64", + "after": { + "address": 6442481776, + "native": "pdb:D:\\a\\Decbench_SampleSet\\Decbench_SampleSet\\_qualification\\build\\snappy\\O2-noinline\\CMakeFiles\\snappy.dir\\snappy.cc.obj:180007870:snappy::UNALIGNED_LOAD64", + "native_name": "snappy::UNALIGNED_LOAD64", + "physical_unit": null, + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/snappy/snappy-stubs-internal.h", + "line": 145, + "native_qualified_name": null, + "native_location_candidates": [ + { + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/snappy/snappy-stubs-internal.h", + "line": 145 + }, + { + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/snappy/snappy-stubs-internal.h", + "line": 149 + } + ], + "native_location_policy": "all_first_pc_locations_same_definition", + "input": "preprocessed/00003.ii", + "definition_sha256": "6ead1d5a0b50ae1c77b5f2cba378700c9c8bac5f42b733447ceb9599be9f68a8", + "span": [ + 62141, + 62146 + ], + "status": "matched", + "native_unit_count": 107, + "native_units_sha256": "b68f77923827c2c618ec0ac99a68d574ad9f803518ee8a4d069b9befb1b8d8ab" + }, + "metric": { + "value": 0.0, + "raw_value": 0.0 + }, + "before_status": "missing_native_source_location" + }, + { + "cell": "33", + "tool": "kuna", + "name": "snappy::UNALIGNED_LOAD64", + "after": { + "address": 6442481776, + "native": "pdb:D:\\a\\Decbench_SampleSet\\Decbench_SampleSet\\_qualification\\build\\snappy\\O2-noinline\\CMakeFiles\\snappy.dir\\snappy.cc.obj:180007870:snappy::UNALIGNED_LOAD64", + "native_name": "snappy::UNALIGNED_LOAD64", + "physical_unit": null, + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/snappy/snappy-stubs-internal.h", + "line": 145, + "native_qualified_name": null, + "native_location_candidates": [ + { + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/snappy/snappy-stubs-internal.h", + "line": 145 + }, + { + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/snappy/snappy-stubs-internal.h", + "line": 149 + } + ], + "native_location_policy": "all_first_pc_locations_same_definition", + "input": "preprocessed/00003.ii", + "definition_sha256": "6ead1d5a0b50ae1c77b5f2cba378700c9c8bac5f42b733447ceb9599be9f68a8", + "span": [ + 62141, + 62146 + ], + "status": "matched", + "native_unit_count": 107, + "native_units_sha256": "b68f77923827c2c618ec0ac99a68d574ad9f803518ee8a4d069b9befb1b8d8ab" + }, + "metric": { + "value": 0.0, + "raw_value": 0.0 + }, + "before_status": "missing_native_source_location" + }, + { + "cell": "33", + "tool": "angr", + "name": "snappy::UNALIGNED_LOAD64", + "after": { + "address": 6442481776, + "native": "pdb:D:\\a\\Decbench_SampleSet\\Decbench_SampleSet\\_qualification\\build\\snappy\\O2-noinline\\CMakeFiles\\snappy.dir\\snappy.cc.obj:180007870:snappy::UNALIGNED_LOAD64", + "native_name": "snappy::UNALIGNED_LOAD64", + "physical_unit": null, + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/snappy/snappy-stubs-internal.h", + "line": 145, + "native_qualified_name": null, + "native_location_candidates": [ + { + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/snappy/snappy-stubs-internal.h", + "line": 145 + }, + { + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/snappy/snappy-stubs-internal.h", + "line": 149 + } + ], + "native_location_policy": "all_first_pc_locations_same_definition", + "input": "preprocessed/00003.ii", + "definition_sha256": "6ead1d5a0b50ae1c77b5f2cba378700c9c8bac5f42b733447ceb9599be9f68a8", + "span": [ + 62141, + 62146 + ], + "status": "matched", + "native_unit_count": 107, + "native_units_sha256": "b68f77923827c2c618ec0ac99a68d574ad9f803518ee8a4d069b9befb1b8d8ab" + }, + "metric": { + "value": 0.0, + "raw_value": 0.0 + }, + "before_status": "missing_native_source_location" + }, + { + "cell": "33", + "tool": "ghidra", + "name": "snappy::UNALIGNED_LOAD64", + "after": { + "address": 6442481776, + "native": "pdb:D:\\a\\Decbench_SampleSet\\Decbench_SampleSet\\_qualification\\build\\snappy\\O2-noinline\\CMakeFiles\\snappy.dir\\snappy.cc.obj:180007870:snappy::UNALIGNED_LOAD64", + "native_name": "snappy::UNALIGNED_LOAD64", + "physical_unit": null, + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/snappy/snappy-stubs-internal.h", + "line": 145, + "native_qualified_name": null, + "native_location_candidates": [ + { + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/snappy/snappy-stubs-internal.h", + "line": 145 + }, + { + "path": "d:/a/decbench_sampleset/decbench_sampleset/_qualification/src/snappy/snappy-stubs-internal.h", + "line": 149 + } + ], + "native_location_policy": "all_first_pc_locations_same_definition", + "input": "preprocessed/00003.ii", + "definition_sha256": "6ead1d5a0b50ae1c77b5f2cba378700c9c8bac5f42b733447ceb9599be9f68a8", + "span": [ + 62141, + 62146 + ], + "status": "matched", + "native_unit_count": 107, + "native_units_sha256": "b68f77923827c2c618ec0ac99a68d574ad9f803518ee8a4d069b9befb1b8d8ab" + }, + "metric": { + "value": 0.0, + "raw_value": 0.0 + }, + "before_status": "missing_native_source_location" + } + ], + "old_source_losses": [], + "old_result_changes": [], + "type_profile_changes": [], + "bounds": "Same frozen 250 requests per tool, saved producer output reused, fresh metrics; experimental evaluator coverage, no decompiler-quality claim.", + "determinism": { + "cells": 45, + "requested_functions_per_tool": 250, + "tools": [ + "fission", + "kuna", + "angr", + "ghidra" + ], + "ignored_fields": [ + "computation_time" + ], + "changed_cells": [], + "deterministic": true + }, + "dataset_revision": "07dd95dbe6214b8a78fc1e4a55961ab515986489", + "rust_joern_revision": "9fd97f50885202596b125ebd2504e64910874bcb", + "sample_sha256": "f432c528e3071efa60fdaecc132aafa8c351e386bf7203d252e2875d6de02620", + "test_comparison": { + "baseline_failures_and_errors": 43, + "candidate_failures_and_errors": 43, + "new_failure_identities": [], + "removed_failure_identities": [], + "identical_failure_identities": true + } +} diff --git a/docs/validation/cpp-ged-identity.md b/docs/validation/cpp-ged-identity.md new file mode 100644 index 0000000..ccacd65 --- /dev/null +++ b/docs/validation/cpp-ged-identity.md @@ -0,0 +1,142 @@ +# Experimental C++ GED identity validation + +Base: `fbb977571ef7737d982849351fa6f7c17aeb4bbb` (PR #103). + +See [metric contracts](../metrics.md#experimental-c-ged-connection-diagnostics). + +Frozen dataset: `sjkim1127/decbench-cpp-qualification` revision +`07dd95dbe6214b8a78fc1e4a55961ab515986489`. +Frozen sample SHA256: `f432c528e3071efa60fdaecc132aafa8c351e386bf7203d252e2875d6de02620`. +250 entries / 45 binary cells: Linux 108, Windows 97, macOS 45. +Frozen Fission and Kuna 1.808 output are re-ingested; these are **not fresh +producer runs** and are not a comparison against current/latest decompilers. +The corpus retains its staging/qualification limitations. + +## Measurements + +The legacy combined/name-keyed decompiled CFG path connected only **2/250 for +each tool** (`baseline.json`). Emitted identifiers and transport/oracle names +usually differ. This was a connection/grammar/identity failure, not a quality +measurement. + +The first exact-identity diagnostic had 21 source CFGs and 114 missing TU +connections. Resolving relative origins via native compilation directories and +exact generated-CU bindings from recorded object/source commands increased +source coverage to **48/250**, with **27** missing TU connections remaining. +These intermediate numbers measure evaluator coverage, not decompiler progress. + +| Final diagnostic | Fission frozen output | Kuna 1.808 frozen output | +| --- | ---: | ---: | +| Requested functions | 250 | 250 | +| Producer-record CFG connected | 249 | 246 | +| Common source CFG population | 48 | 48 | +| Source/producer CFG paired, GED measured | 48 | 47 | +| GED distance zero (diagnostic) | 18 | 20 | +| Mean distance, tool-specific measured population | 56.3125 | 48.9574 | +| Median distance, tool-specific measured population | 7.5 | 6.0 | + +The common 47 paired entries have 18 and 20 distance-zero results respectively. +Kuna's remaining source-eligible output fails Joern declaration recovery; it is a +**per-output parser-health failure**, not a new oracle exclusion. Full evidence is +in `producer-cfg-failures-on-eligible-source.json`. No full-corpus C++ perfect +percentage, Union, official ranking or Fission quality improvement is inferred. + +| Platform | Requested | Common source eligible | Fission paired | Kuna paired | +| --- | ---: | ---: | ---: | ---: | +| linux | 108 | 24 | 24 | 24 | +| windows | 97 | 8 | 8 | 8 | +| macos | 45 | 16 | 16 | 15 | + +## Source population and exclusions + +All 250 entries remain in the request plan. The source population is identical +between tools by exact `(cell, address)`, including the same 202 exclusions. + +| Source status | Count | +| --- | ---: | +| Multiple concrete native records at entry | 16 | +| Defining logical file absent from retained source origins | 42 | +| Unproven local/anonymous native scope | 9 | +| Source CFG admitted | 48 | +| Selected definition/scope syntax not proven | 4 | +| Template specialization policy absent | 46 | +| Joern did not produce exactly one real definition (zero or multiple) | 16 | +| Origin exists but no containing definition | 7 | +| Explicit source parser diagnostics | 7 | +| Native/source qualified names disagree | 9 | +| No exact TU connection | 27 | +| Native source file/line unavailable | 19 | + +Nine name/scope-disagreement candidates are **rejected**, rather than credited +by a matching leaf name or a larger/nearby CFG. Duplicate native entries, source +ambiguity and unsupported specialization do not borrow another function's CFG. +`rejected-name-mismatches.json` records the rejected candidates. + +## Identity audit and its limits + +Every admitted row retains its native entry/DIE or PDB identity, full native and +source names, TU evidence, original file/line, preprocessed physical span and +parser scope. Compilation-database aliases retain exact command and database +hashes. Relative paths use the recorded native directory, never evaluator cwd. + +An independent original-source-bundle check found 35 qualified-name+line matches, +11 leaf+line matches with raw-source parser scope recovery limitations, and two +macro-decorated class declarations that the raw-source parser could not index. +The 13 supplemental original declaration contexts were read directly; the two +macro cases are `Status::ok` and `DictEntry::GetDefault`. No accepted identity +contradiction was observed in this bounded inspection. The machine audit does +**not** establish fully automatic 250-function freedom from mismatches: 202 rows +abstain, raw-source scope recovery has limits, and build/link provenance remains +asserted rather than independently qualified. See `original-audit-final.json` +and `original-manual-context.txt`. + +## Determinism and inputs + +Two fresh source-AST/source-CFG/producer-CFG passes per cell, with metric caches +disabled, produced byte-identical complete JSON for **45/45 cells**. An immutable +native index is shared between the two passes and validated by binary/database +hashes. Raw outputs are read again; no producer is invoked. The earlier full +native-reader two-process replay was also identical for 45 cells before the TU +transport extension. These scopes are distinct. + +the retained input manifest binds 768 input files (500 raw producer JSON files, +45 original and stripped images, native debug artifacts, source archives, +compilation metadata and the sample). Each original binary hash agrees with the +frozen sample. the retained code manifest binds evaluator/test/workflow sources. + +## Validation + +- New GED regressions: **33 passed**, including real DWARF 4/5 and PDB fixtures, + actual saved-temp compiler inputs at O0/O2, relative and MSVC/GCC markers, + overloads, exact qualified scope, ICF ambiguity, wrong-TU/source disagreement, + one-record parse isolation, fixed population, stale-index rejection and CLI. +- Focused workflow's pytest lane locally: **202 passed / 5 platform skips**. +- Full repository baseline: **874 passed / 19 failed / 28 skipped / 24 errors**. +- Full candidate: **907 passed / 19 failed / 28 skipped / 24 errors**. +- The **43 pre-existing failure/error identities are identical**, no new or + removed identity. See `pytest-regression-comparison.json` and both JUnit logs. +- New owner mypy checks and scoped Black/Ruff checks pass. +- No remote CI run is claimed for this unsubmitted delta. The workflow is updated + for the new regressions and owners. The pinned native parser remains + `9fd97f50885202596b125ebd2504e64910874bcb`. +- No guest corpus binary was executed, no native producer was rerun, and no + website/snapshot/Hugging Face/leaderboard publication was performed. + +## Bounds and next work + +Strict definition-only explicit CFG parsing does not qualify full-TU callee +context, template specialization, implicit object lifetime or exception edges. +Generic evaluator/infrastructure errors propagate; only explicit parser error +diagnostics become parser-health failures, and nonfinite GED is an evaluator +failure. Existing GED arithmetic is reused without a new default metric. + +The next source-coverage blockers are missing TU evidence (27, Windows), native +file/line evidence (19, Windows), absent retained origins (42), and template +specialization policy (46). Collect/qualify those artifacts or policy separately; +do not shrink or replace the frozen 250 requests to improve a score. + + +Full reports, inputs/code hashes, producer/source audits and reproduction drivers +are retained locally in Fission `benchmark/artifacts/cpp-ged/2026-10-18/`. +This note is diagnostic evidence; it neither adopts an official C++ GED policy +nor marks the corpus scoring-ready. Remote CI must run after approved submission. diff --git a/docs/validation/cpp-native-debug-summary.json b/docs/validation/cpp-native-debug-summary.json new file mode 100644 index 0000000..cadd9ec --- /dev/null +++ b/docs/validation/cpp-native-debug-summary.json @@ -0,0 +1,202 @@ +{ + "schema": "cpp-native-debug-corpus-evidence-v1", + "data_repo": "sjkim1127/decbench-cpp-qualification", + "data_revision": "07dd95dbe6214b8a78fc1e4a55961ab515986489", + "validated_code_commit": "a8a4ae7a56c30bc894be2dd526026a285fe55bec", + "baseline_reader_commit": "8eeceb82088e97ace88d15a592db1ee76ab37384", + "comparison_scope": "native debug transport and address/range resolution; no decompiler scoring or runtime parity", + "baseline_macos_format_recognized": 0, + "baseline_macos_dwarf_readable": 0, + "macos_images": 9, + "macos_source_function_records_resolved": 4129, + "wrong_uuid_rejected": true, + "isolated_inputs_have_no_oracle": true, + "elf_pe_format_and_cu_baseline_unchanged": true, + "elf_type_and_source_declaration_fingerprints_equal": true, + "elf_fingerprints": [ + { + "mode": "O0", + "type_addresses": 357, + "declaration_addresses": 357, + "sha256": "17b5abf6639c9989f0ec0a02ddb6b1eee995ea3b5982ad3f126e9de9f3cc916d" + }, + { + "mode": "O2", + "type_addresses": 97, + "declaration_addresses": 97, + "sha256": "ccc0147f7f49d36789d14b6aca6e0e4646f82b34b7ccba44471decacf9c3155b" + }, + { + "mode": "O2-noinline", + "type_addresses": 348, + "declaration_addresses": 348, + "sha256": "9b2c48d015f55eaa4a039d97eb2adca937c8139771b9f2f6bce89f3337968d22" + } + ], + "full_suite_run": false, + "score_run": false, + "rows": [ + { + "platform": "macos", + "project": "OpenCC", + "mode": "O0", + "detected": { + "fmt": "macho", + "arch": "aarch64", + "bits": 64 + }, + "readable_dwarf": true, + "dwarf_cus": 33, + "source_function_records": 664, + "resolved": 664, + "function_identities": 8837, + "source_declaration_addresses": 8837, + "type_ground_truth_addresses": 8911, + "linked_base": 4294967296 + }, + { + "platform": "macos", + "project": "ninja", + "mode": "O0", + "detected": { + "fmt": "macho", + "arch": "aarch64", + "bits": 64 + }, + "readable_dwarf": true, + "dwarf_cus": 35, + "source_function_records": 814, + "resolved": 814, + "function_identities": 6371, + "source_declaration_addresses": 6371, + "type_ground_truth_addresses": 6372, + "linked_base": 4294967296 + }, + { + "platform": "macos", + "project": "snappy", + "mode": "O0", + "detected": { + "fmt": "macho", + "arch": "aarch64", + "bits": 64 + }, + "readable_dwarf": true, + "dwarf_cus": 4, + "source_function_records": 227, + "resolved": 227, + "function_identities": 442, + "source_declaration_addresses": 442, + "type_ground_truth_addresses": 442, + "linked_base": 0 + }, + { + "platform": "macos", + "project": "OpenCC", + "mode": "O2", + "detected": { + "fmt": "macho", + "arch": "aarch64", + "bits": 64 + }, + "readable_dwarf": true, + "dwarf_cus": 33, + "source_function_records": 323, + "resolved": 323, + "function_identities": 1296, + "source_declaration_addresses": 1296, + "type_ground_truth_addresses": 1365, + "linked_base": 4294967296 + }, + { + "platform": "macos", + "project": "ninja", + "mode": "O2", + "detected": { + "fmt": "macho", + "arch": "aarch64", + "bits": 64 + }, + "readable_dwarf": true, + "dwarf_cus": 35, + "source_function_records": 437, + "resolved": 437, + "function_identities": 571, + "source_declaration_addresses": 571, + "type_ground_truth_addresses": 572, + "linked_base": 4294967296 + }, + { + "platform": "macos", + "project": "snappy", + "mode": "O2", + "detected": { + "fmt": "macho", + "arch": "aarch64", + "bits": 64 + }, + "readable_dwarf": true, + "dwarf_cus": 4, + "source_function_records": 105, + "resolved": 105, + "function_identities": 134, + "source_declaration_addresses": 134, + "type_ground_truth_addresses": 134, + "linked_base": 0 + }, + { + "platform": "macos", + "project": "OpenCC", + "mode": "O2-noinline", + "detected": { + "fmt": "macho", + "arch": "aarch64", + "bits": 64 + }, + "readable_dwarf": true, + "dwarf_cus": 33, + "source_function_records": 586, + "resolved": 586, + "function_identities": 4529, + "source_declaration_addresses": 4529, + "type_ground_truth_addresses": 4599, + "linked_base": 4294967296 + }, + { + "platform": "macos", + "project": "ninja", + "mode": "O2-noinline", + "detected": { + "fmt": "macho", + "arch": "aarch64", + "bits": 64 + }, + "readable_dwarf": true, + "dwarf_cus": 35, + "source_function_records": 759, + "resolved": 759, + "function_identities": 2654, + "source_declaration_addresses": 2654, + "type_ground_truth_addresses": 2655, + "linked_base": 4294967296 + }, + { + "platform": "macos", + "project": "snappy", + "mode": "O2-noinline", + "detected": { + "fmt": "macho", + "arch": "aarch64", + "bits": 64 + }, + "readable_dwarf": true, + "dwarf_cus": 4, + "source_function_records": 214, + "resolved": 214, + "function_identities": 355, + "source_declaration_addresses": 355, + "type_ground_truth_addresses": 355, + "linked_base": 0 + } + ] +} diff --git a/docs/validation/cpp-oracle-qualification.md b/docs/validation/cpp-oracle-qualification.md new file mode 100644 index 0000000..0ffa593 --- /dev/null +++ b/docs/validation/cpp-oracle-qualification.md @@ -0,0 +1,99 @@ +# C++ oracle coverage and recompiler target boundaries + +Status: local follow-up to PR #103 at `ea2b461`. Not an official C++ policy, +leaderboard update or decompiler quality improvement. No Fission code changed. + +## Frozen measurement + +Qualification-v2 revision `07dd95dbe6214b8a78fc1e4a55961ab515986489`, sample SHA256 +`f432c528e3071efa60fdaecc132aafa8c351e386bf7203d252e2875d6de02620`: +250 unchanged requests across 45 cells (Linux 108, Windows 97, macOS 45). +Frozen Fission and Kuna 1.808 outputs are re-ingested; no producer or corpus +program is executed. This is not a comparison against current/latest producers. + +| Diagnostic | Baseline | Candidate | +| --- | ---: | ---: | +| Common source CFG eligible | 48 | 54 | +| Fission paired / distance zero | 48 / 18 | 54 / 18 | +| Kuna paired / distance zero | 47 / 20 | 53 / 21 | + +All existing measured function results are identical, with no lost source row. +Two fresh source AST / source CFG / producer CFG passes per cell are identical +for 45/45 cells with metric caches disabled. Each cell shares an immutable native +index between its two passes. Native facts are hash-bound, not a metric cache. +Producer raw output is reused, not regenerated. + +Two previously filtered third-party source definitions are now connected. +Retaining requested headers also prevents four project definitions from being +misparsed with incomplete surrounding source scopes. Five new identities match +qualified names and lines in independent original source bundles; the sixth +matches the raw parser's leaf name and line, with its namespace declarations +inspected directly. The remaining two macro-decorated source declarations have +the same raw-parser limitation as before. This bounded audit is not independent +build/link qualification or execution parity. + +The 42 previous `missing_source_origin` entries are now: 2 connected, 38 rejected +as unqualified template specializations, 1 without a containing definition and +1 without exactly one real parser definition. Missing TU/source-line evidence is +not repaired by name/path guessing. No request is removed or resampled. + +## Recompilation correctness + +A native probe on this Darwin/arm64 host reproduced selection of `/usr/bin/gcc` +for ELF/AArch64, followed by emission of a Mach-O/AArch64 object. Matching CPU +architecture did not imply matching output format. No published score was +recomputed or inferred from this probe. + +Bare `gcc` is now selected for native ELF architectures only on Linux; other +hosts require an explicit existing cross-ELF compiler. Compiled relocatable +objects must match the original format family, architecture and bitness before +byte extraction. Ordinary COFF objects belong to the PE target family. +Unknown/incompatible targets return explicit abstention and are excluded from +binary aggregates. Compatible objects retain the existing byte comparison. +Byte-match cache generation is 9; the new target identity is also in its key. + +Read-only inspection of all 250 requested native entries reports no available +legacy recompiler on this Mac: 108 Linux requests need absent +`aarch64-linux-gnu-gcc`; 142 Windows/Mach-O requests have no legacy recompiler. +This is a toolchain capability audit, not a C++ byte-match run. Format/ISA checks +do not prove matching compiler family/version, C++ ABI, standard library, +exception/unwind configuration, or complete original command flags. + +## Remaining gates + +* **Artifacts:** 27 Windows source-TU misses (Detours 16, WinSparkle 11) have no + retained `.ii` bundle; 19 other Windows requests have no usable native file/line + fact. Recollect evidence from the same build or introduce a separately frozen + corpus revision. Rebuilding a new binary is not proof for the old address plan. +* **Templates:** 84 requests now reach the specialization policy boundary. + A concrete instantiated AST/CFG and proven native template arguments are needed + before conditional compilation, `if constexpr` and overload resolution can be + interpreted. Generic template bodies are not substituted as instance oracles. +* **Lifetime/exceptions:** explicit definition CFG remains diagnostic only. + Implicit construction/destruction, cleanup on early exits, exceptions/unwinding + and full-TU callee context are unqualified. Missing syntax does not prove that + these effects are absent, and newly connected object-using functions earn no + full semantic qualification. +* **Types:** split/computed DWARF locations, unknown legacy PDB formal status, + incomplete objects and unknown vptr offsets still abstain. Native audit details + are retained; storage positions are not used to guess formal classification. + The prior 26/250 type-policy population has not been expanded or re-scored here. +* **C++ recompilation:** qualify GCC/MSVC/Clang commands, original compiler and + target ABI evidence, then validate reconstructed C/C++ under the chosen policy. + Passing scalar/COFF fixtures or the new object check is not full C++ support. + +## Validation and evidence + +* Focused workflow locally: 246 passed, 5 platform-specific skips; Black, Ruff and + existing C++ owner mypy checks pass. New fixtures cover a real system-header + definition and ELF/Mach-O/COFF target identification without guest execution. +* Full baseline: 907 passed / 19 failed / 28 skipped / 24 errors. +* Full candidate: 927 passed / 19 failed / 28 skipped / 24 errors. + All 43 existing failure/error identities are unchanged; none added or removed. +* New remote CI has not run for this unsubmitted follow-up. Existing head CI is + not evidence for this patch. No website/overlay/Union/Hugging Face change. + +Complete local artifacts, inputs, per-cell pairs, source identity audit, native +location inventory, known failures and proposed patch are retained under Fission +`benchmark/artifacts/cpp-oracle-qualification/2026-10-18/`. Public submission +requires complete human review under `docs/agents.md`. diff --git a/docs/validation/cpp-pdb-definition-summary.json b/docs/validation/cpp-pdb-definition-summary.json new file mode 100644 index 0000000..3d3a8e4 --- /dev/null +++ b/docs/validation/cpp-pdb-definition-summary.json @@ -0,0 +1,288 @@ +{ + "validated_code_commit": "bb5ccfb5a30335982abc4e7240613d8b0d736624", + "prerequisite_branch_head": "e216d9cb6475a8ea0c7d60a2334519ee1e3960b4", + "dataset_revision": "07dd95dbe6214b8a78fc1e4a55961ab515986489", + "definition_coverage": { + "fission": { + "native_outputs": 244, + "before_indexed": 244, + "after_indexed": 244, + "valid_c_unchanged": 244 + }, + "kuna": { + "native_outputs": 250, + "before_indexed": 247, + "after_indexed": 250, + "valid_c_unchanged": 154 + } + }, + "pdb": { + "image_pairs": 18, + "range_matches": 91418, + "range_records": 91418, + "packaged_name_differences": 15145, + "isolated_inputs_without_pdb": 18, + "rows": [ + { + "project": "OpenCC", + "optimization": "O0", + "functions": 10280, + "range_matches": 6237, + "packaged_function_records": 6237, + "packaged_name_differences": 843, + "source_line_provenance": 7749, + "measurable_procedures": 747 + }, + { + "project": "detours", + "optimization": "O0", + "functions": 3437, + "range_matches": 136, + "packaged_function_records": 136, + "packaged_name_differences": 0, + "source_line_provenance": 2084, + "measurable_procedures": 2150 + }, + { + "project": "ninja", + "optimization": "O0", + "functions": 7434, + "range_matches": 5424, + "packaged_function_records": 5424, + "packaged_name_differences": 112, + "source_line_provenance": 6048, + "measurable_procedures": 585 + }, + { + "project": "snappy", + "optimization": "O0", + "functions": 477, + "range_matches": 408, + "packaged_function_records": 408, + "packaged_name_differences": 21, + "source_line_provenance": 425, + "measurable_procedures": 57 + }, + { + "project": "tracy", + "optimization": "O0", + "functions": 45663, + "range_matches": 28431, + "packaged_function_records": 28431, + "packaged_name_differences": 5811, + "source_line_provenance": 40783, + "measurable_procedures": 619 + }, + { + "project": "winsparkle", + "optimization": "O0", + "functions": 38402, + "range_matches": 2267, + "packaged_function_records": 2267, + "packaged_name_differences": 229, + "source_line_provenance": 18945, + "measurable_procedures": 18679 + }, + { + "project": "OpenCC", + "optimization": "O2", + "functions": 4122, + "range_matches": 1363, + "packaged_function_records": 1363, + "packaged_name_differences": 212, + "source_line_provenance": 1853, + "measurable_procedures": 1563 + }, + { + "project": "detours", + "optimization": "O2", + "functions": 3437, + "range_matches": 136, + "packaged_function_records": 136, + "packaged_name_differences": 0, + "source_line_provenance": 2075, + "measurable_procedures": 2163 + }, + { + "project": "ninja", + "optimization": "O2", + "functions": 2700, + "range_matches": 800, + "packaged_function_records": 800, + "packaged_name_differences": 39, + "source_line_provenance": 1432, + "measurable_procedures": 1070 + }, + { + "project": "snappy", + "optimization": "O2", + "functions": 447, + "range_matches": 378, + "packaged_function_records": 378, + "packaged_name_differences": 9, + "source_line_provenance": 225, + "measurable_procedures": 312 + }, + { + "project": "tracy", + "optimization": "O2", + "functions": 14674, + "range_matches": 5132, + "packaged_function_records": 5132, + "packaged_name_differences": 1220, + "source_line_provenance": 8147, + "measurable_procedures": 5838 + }, + { + "project": "winsparkle", + "optimization": "O2", + "functions": 37752, + "range_matches": 1048, + "packaged_function_records": 1048, + "packaged_name_differences": 165, + "source_line_provenance": 18015, + "measurable_procedures": 19560 + }, + { + "project": "OpenCC", + "optimization": "O2-noinline", + "functions": 8516, + "range_matches": 5604, + "packaged_function_records": 5604, + "packaged_name_differences": 787, + "source_line_provenance": 4384, + "measurable_procedures": 5124 + }, + { + "project": "detours", + "optimization": "O2-noinline", + "functions": 3437, + "range_matches": 136, + "packaged_function_records": 136, + "packaged_name_differences": 0, + "source_line_provenance": 2076, + "measurable_procedures": 2162 + }, + { + "project": "ninja", + "optimization": "O2-noinline", + "functions": 6799, + "range_matches": 4896, + "packaged_function_records": 4896, + "packaged_name_differences": 107, + "source_line_provenance": 3737, + "measurable_procedures": 4164 + }, + { + "project": "snappy", + "optimization": "O2-noinline", + "functions": 476, + "range_matches": 407, + "packaged_function_records": 407, + "packaged_name_differences": 21, + "source_line_provenance": 274, + "measurable_procedures": 320 + }, + { + "project": "tracy", + "optimization": "O2-noinline", + "functions": 40574, + "range_matches": 26455, + "packaged_function_records": 26455, + "packaged_name_differences": 5341, + "source_line_provenance": 25278, + "measurable_procedures": 23929 + }, + { + "project": "winsparkle", + "optimization": "O2-noinline", + "functions": 38300, + "range_matches": 2160, + "packaged_function_records": 2160, + "packaged_name_differences": 228, + "source_line_provenance": 18414, + "measurable_procedures": 19974 + } + ] + }, + "windows_abi_diagnostic": { + "fission": { + "requested": 97, + "measured": 0, + "excluded": 97, + "perfect": 0, + "mean": null + }, + "kuna": { + "requested": 97, + "measured": 32, + "excluded": 65, + "perfect": 0, + "mean": 0.015625 + } + }, + "controls": [ + { + "control": "wrong-project-GUID", + "rejected": true, + "reason": "PE/PDB GUID or Age mismatch" + }, + { + "control": "wrong-age", + "rejected": true, + "reason": "PE/PDB GUID or Age mismatch" + }, + { + "control": "wrong-dbi-architecture", + "rejected": true, + "reason": "PE/PDB DBI Age or architecture mismatch" + }, + { + "control": "wrong-section-rva", + "rejected": true, + "reason": "PE/PDB section mapping mismatch" + }, + { + "control": "truncated", + "rejected": true, + "reason": "invalid MSF block geometry" + }, + { + "control": "missing", + "unavailable": true + }, + { + "control": "positive-consumers", + "identities": 447, + "source_owners": 94, + "native_instruction_entry": true, + "source_hint": true, + "evalkit_address_identity": true + } + ], + "existing_elf_macho_type_values": { + "compared": 66, + "all_equal": true, + "cells_per_tool": 6 + }, + "default_pdb_scoring_enabled": false, + "msvc_byte_match_abstains": true, + "formal_parameter_ordinals_inferred": false, + "full_test_suite_run": false, + "official_cpp_union_or_rank": false, + "new_python_dependency": "tree-sitter-cpp 0.23.4", + "lint_limitation": "Existing SIM105 in type_match.py is present in prerequisite code; all other scoped Ruff checks pass.", + "code_sha256": { + "decbench/evalkit/resolve.py": "6d9a8f3fe293d31f18decac53787de6ecf2d173302380688959028d1cd67a196", + "decbench/metrics/byte_match.py": "a0fdf6c41ea2dbcae54cd06d3e5d3a8cd3533abeebe7c71a8c05f1f52bb3d4ca", + "decbench/metrics/type_match.py": "3b04a69838a9b24810ec16f8981660b208c0b5e2cd1eaf951bd17d7586cb81fa", + "decbench/metrics/variable_features.py": "41f824db2871ab07ba80cb99c5b21cfd8620a1c03024c18da1952aaf2a01d6f0", + "decbench/utils/binfmt.py": "99a5ee30210818c335713778cff39c6b8326f421daddfe3f515baaa6755059ad", + "decbench/utils/function_identity.py": "e5e25f2659de88ff18c0c0b3ac10c08dd7a8abb3b9bf1e67ea4e974371b1a6f7", + "decbench/utils/native_code.py": "9f936705eefa9a1c178c836ce4da928dad98c436357d783fd499060581fcf97c", + "decbench/utils/pdb_info.py": "4af30e58a31f5871aaa103cab7bdd7250579ec220273c8b36ef33792314861fe", + "decbench/utils/source_extract.py": "c7b885abfab08060be48b58f96157bd09fdc7720bc45e7937bf450f2d67d14f4", + "pyproject.toml": "4b2ed19dcb63f2e6e9dd1f09a9d9bb316f8d86e2670a81c4415d9f678a7ba7f9", + "tests/test_type_match_correspondence.py": "0ebcd1d7ba5af4e6b385e088431144332bde5aee9b0e4860c19da6d2b0cada12" + } +} diff --git a/docs/validation/cpp-review-fixes.json b/docs/validation/cpp-review-fixes.json new file mode 100644 index 0000000..145d5a8 --- /dev/null +++ b/docs/validation/cpp-review-fixes.json @@ -0,0 +1,147 @@ +{ + "date": "2026-10-18", + "base": "cfcc485e964a9298d08714816cd4839809be3d6b", + "upstream_main_checked": "ae070b885b4e005ed37510a2c3445af6284dc2b9", + "submission": "local proposed follow-up to PR103; human review required before submission", + "new_tests_before": "10 failed", + "new_tests_after": "10 passed", + "focused_baseline": { + "passed": 156, + "skipped": 5 + }, + "focused_candidate": { + "passed": 166, + "skipped": 5 + }, + "full_repository": { + "baseline": "20 failed, 863 passed, 28 skipped, 24 errors", + "candidate": "20 failed, 873 passed, 28 skipped, 24 errors", + "new_failure_identities": [], + "same_failure_identities": [ + "tests/test_byte_match_fixup.py::TestCompileWithFixup::test_undeclared_symbol_stubbed", + "tests/test_caching_dataset_subset.py::test_byte_match_caches_with_real_binary", + "tests/test_content.py::test_about_view_keeps_intro_and_outro_around_the_cards", + "tests/test_content.py::test_goal_card_parse_yields_three_cards", + "tests/test_content.py::test_goal_card_perfect_lines_match_the_metric_registry", + "tests/test_content.py::test_inline_html_passes_through_unescaped", + "tests/test_content.py::test_metric_viz_blocks_open_by_default_and_carry_a_visualization", + "tests/test_decompilers.py::TestSmokeDecompile::test_decompile_tiny_binary[angr]", + "tests/test_e2e_pipeline.py::TestFullPipelineIntegration::test_ged_pipeline", + "tests/test_evalkit.py::test_cli_export_smoke", + "tests/test_evalkit.py::test_cli_ingest_smoke", + "tests/test_evalkit.py::test_export_anon_assignment_deterministic", + "tests/test_evalkit.py::test_export_functions_json_matches_dwarf_truth", + "tests/test_evalkit.py::test_export_layout_and_zip", + "tests/test_evalkit.py::test_export_strict_vs_allow_unresolved", + "tests/test_evalkit.py::test_export_strip_verification", + "tests/test_evalkit.py::test_force_reingest_purges_slices_the_new_submission_dropped", + "tests/test_evalkit.py::test_ingest_drops_extra_and_unparseable_addresses", + "tests/test_evalkit.py::test_ingest_force_semantics", + "tests/test_evalkit.py::test_ingest_packaged_zip_happy_path", + "tests/test_evalkit.py::test_ingest_partial_submission_failed_functions", + "tests/test_evalkit.py::test_ingest_rejects_bad_dec_id", + "tests/test_evalkit.py::test_ingest_relabel_collision_keeps_larger_body", + "tests/test_evalkit.py::test_ingest_requires_sample_manifest", + "tests/test_evalkit.py::test_ingest_warns_when_the_tree_manifest_drifted_from_the_kit", + "tests/test_evalkit.py::test_package_address_not_in_public_list", + "tests/test_evalkit.py::test_package_duplicate_addresses_in_one_file", + "tests/test_evalkit.py::test_package_duplicate_function_names", + "tests/test_evalkit.py::test_package_happy_path", + "tests/test_evalkit.py::test_package_listed_c_file_missing", + "tests/test_evalkit.py::test_package_missing_results_json", + "tests/test_evalkit.py::test_package_non_identifier_function_name", + "tests/test_evalkit.py::test_package_out_flag_writes_elsewhere", + "tests/test_evalkit.py::test_package_partial_submission_warns_but_packages", + "tests/test_evalkit.py::test_package_tolerates_thumb_odd_addresses", + "tests/test_evalkit.py::test_package_unknown_binary", + "tests/test_evalkit.py::test_package_unparseable_results_json", + "tests/test_evalkit.py::test_verify_strip_rejects_an_unstripped_binary", + "tests/test_glaurung_decompilers.py::TestDockerInstall::test_docker_decompile_is_static_address_scoped", + "tests/test_glaurung_decompilers.py::TestDockerInstall::test_nonzero_exit_with_json_fails_closed", + "tests/test_manifold_backend.py::test_decompile_binary_maps_fun_names_to_addresses", + "tests/test_manifold_backend.py::test_decompile_binary_narrows_to_requested_addresses", + "tests/test_manifold_backend.py::test_docker_run_mounts_the_binary_and_reads_back_the_unit", + "tests/test_source_native_arch_evidence.py::test_pe_source_variables_receive_native_addresses" + ] + }, + "pdb_reader_ast": { + "only_typed_error_class_and_explicit_raises_changed": true, + "explicit_parser_raise_sites": 55 + }, + "validation": { + "scoped_style": "scoped Black and Ruff passed", + "policy_mypy": "4 modules clean", + "shared_ruff": "one unchanged baseline SIM105 in type_match.py; shared files not globally lint-clean", + "real_codeview_fixture": "passed", + "diff_check": "passed", + "remote_ci": "not run for proposed follow-up; existing PR103 run 38410600522 action_required" + }, + "population": { + "dataset_revision": "07dd95dbe6214b8a78fc1e4a55961ab515986489", + "sample_sha256": "f432c528e3071efa60fdaecc132aafa8c351e386bf7203d252e2875d6de02620", + "scope": "fresh evaluator replay of frozen producer outputs; producers not rerun", + "cells": 45, + "requested": 250, + "all_nonduration_reports_equal": true, + "excluded_fields": [ + "tools.*.legacy_pdb_abi_diagnostic.computation_time_seconds" + ], + "totals": { + "fission": { + "source": { + "eligible": 26, + "perfect": 0, + "mean": 0.009615384615384616 + }, + "abi": { + "eligible": 26, + "perfect": 0, + "mean": 0.04807692307692308 + } + }, + "kuna": { + "source": { + "eligible": 26, + "perfect": 1, + "mean": 0.09615384615384616 + }, + "abi": { + "eligible": 26, + "perfect": 1, + "mean": 0.09615384615384616 + } + } + } + }, + "source_files": { + ".github/workflows/cpp-policy.yml": "e1115b6c373baec12e5eba4750281b4c9f49b5f4a45128e12cf84d083712219f", + "decbench/decompilers/raw/fission_raw.py": "c29097cf1b8c397d7c6ea328c1eeb510cf38ab3f018332ab4555438a5d08b93e", + "decbench/metrics/cpp_type_match.py": "e71b21c467ee2ae06b4864b276568436a2ec93f6422fcc8615911b504666f66b", + "decbench/metrics/cpp_types.py": "a70c866f4fd82bbe620e2de4961e21fa3a33bf1a592e02b8b048e6936d384fb6", + "decbench/metrics/type_match.py": "7b867508e2d839b2d792784b8c68a1de0df7bc2fdf6e7ee5eb12ca60b60efb9e", + "decbench/metrics/variable_match.py": "32e5839c3c16cf7af2d835200cf0aac3e301a8945cb354bde7c78518d8d1308b", + "decbench/models/decompilation.py": "2e01cb817a9c21b41e7890b4736c7f03521167cba8eef87686ad74a646479c83", + "decbench/utils/pdb_info.py": "e4df4798555320a343eb3d34085f347947e7208880fb234a902b3b4eb467c395", + "docs/cpp-type-policy-proposal.md": "7b0ee4b3a97b2c95d42027969e411d28e7603d98ca5a48e0103d30203186021b", + "tests/test_type_match_correspondence.py": "972ec91623b53425a791ea64835fecd87cd4dd904a04ae316af453400cd37ec8", + "tests/test_cpp_review_contracts.py": "5a5b3955e07c6d13f474896a06876b4ad17bbe86235b5dbbe16c63f99d798a38" + }, + "frozen_inputs": { + "dataset_revision": "07dd95dbe6214b8a78fc1e4a55961ab515986489", + "sample_sha256": "f432c528e3071efa60fdaecc132aafa8c351e386bf7203d252e2875d6de02620", + "files_hashed": 572, + "producer_records_found": { + "fission": 250, + "kuna": 250 + }, + "fission_duplicate_or_empty_name_records": 0 + }, + "versions": { + "type_match_cache": "24", + "cpp_type_match_cache": "3", + "cpp_policy": "cpp-types-v2-proposal" + }, + "official_score": false, + "producer_rerun": false, + "fission_performance_changes_included": false +} diff --git a/docs/validation/cpp-review-fixes.md b/docs/validation/cpp-review-fixes.md new file mode 100644 index 0000000..6e20f1d --- /dev/null +++ b/docs/validation/cpp-review-fixes.md @@ -0,0 +1,38 @@ +# C++ evaluator review follow-up + +Local proposal, 2026-10-18, against [PR #103](https://github.com/Noelo-Lab/decbench/pull/103) head `cfcc485e964a9298d08714816cd4839809be3d6b`. This is benchmark correctness work, not a Fission speed/quality patch or an official C++ policy. [Compact evidence](cpp-review-fixes.json). + +## Changes + +- Restrict function-level oracle abstention to explicit `UnsupportedType` and `PdbFormatError` during oracle preparation. Run recovery scoring outside that boundary. Unexpected evaluator/parser `ValueError` propagates instead of silently reducing the eligible population; source contexts still close on failures. +- Add native `PdbFormatError(ValueError)` for recognized unsupported/invalid PDB/CodeView checks. AST comparison proves the reader changes only the exception class/declaration and explicit raise names; parser algorithms/messages and existing ValueError-compatible callers remain unchanged. This avoids converting intentional native format exclusions into arbitrary evaluator-failure exclusions. +- Require scoped display names to be nonempty and unique among all function variables, at both the Fission adapter and shared matching owner. Duplicate names gain no scoped PCs. Keep direct variable PCs primary, independent stack/ABI evidence intact, and the legacy whole-line mapping contract unchanged. Stable binding-ID transport remains future work. +- Resolve configured/PATH Fission executable paths before changing the child working directory. Real fixture process tests cover relative paths, spaces, changed cwd and version/decompilation calls. +- State the actual normal `tree-sitter-cpp` runtime dependency. Do not invent cross-format virtual-base equivalence; native locator equality remains an experimental policy limit. +- Bump `type_match` cache 23 ->24 and experimental evaluator cache 2 ->3. Keep `cpp-types-v2-proposal`, the type-comparison rules, default metrics and Union unchanged. Include the new tests in the existing focused Ubuntu workflow. + +## Validation + +The ten new regression cases fail on the original implementation and pass after the change. Existing focused lane: baseline **156 passed /5 skipped**, candidate **166 passed /5 skipped**. Native Mach-O/DWARF/dSYM and real CodeView/PDB compiler fixtures pass. Off-host optimized ELF, Thumb and ELF ICF cases still skip honestly on this Mac. The new workflow includes all new cases; this is not a claim of fresh remote Ubuntu success. + +Full local repository: baseline **863 passed /20 failed /28 skipped /24 errors**; candidate **873 passed /20 failed /28 skipped /24 errors**. All 44 failure/error identities agree; no new identity. These include existing eval-kit/host-format, backend and content failures, listed in compact evidence. The full repository is not green. Initial collection also lacked the declared mistune and pinned rust-joern packages; install them in the isolated environment and rerun both sides. Keep the initial collection log. + +Scoped Black/Ruff and four C++ policy mypy owners pass; diff check passes. Shared Ruff still reports the identical pre-existing `SIM105` in `type_match.py:557`; do not claim whole-repository lint clean. Pinned Rust-Joern remains `9fd97f50885202596b125ebd2504e64910874bcb`. The PDB exception-only AST check is retained. + +## Frozen qualification replay + +Dataset [qualification-v2 revision](https://huggingface.co/datasets/sjkim1127/decbench-cpp-qualification/tree/07dd95dbe6214b8a78fc1e4a55961ab515986489); [dataset](https://huggingface.co/datasets/sjkim1127/decbench-cpp-qualification); [earlier raw corpus](https://huggingface.co/datasets/sjkim1127/decbench-cpp-raw-corpus). + +Re-ingest the same frozen Fission and Kuna outputs, **250 per tool across 45 cells**, then evaluate source and ABI views independently with score caches disabled. Producers are not rerun; these are the earlier frozen outputs, not current Fission/main or latest Kuna results. Source/debug artifacts stay exclusively on the evaluator side. All cell report fields, oracle exclusions, matching/type decisions and legacy PDB diagnostic outputs agree between baseline and candidate, excluding only `tools.*.legacy_pdb_abi_diagnostic.computation_time_seconds`. The first strict comparison exposed legacy timing differences. All 36 raw differences across 18 Windows cells are exactly those duration fields; raw reports, the initial failed comparator log and the correction are retained. No score or evidence field is excluded. Shared eligible population remains 26/250; Fission source/ABI perfect 0/26, fixed Kuna 1.808 source/ABI perfect 1/26. This is supported-subset evidence, not a complete C++ score or ranking. No denominator is silently reduced on an unexpected evaluator fault. + +Verified 572 input/debug/producer artifact hashes, including all 500 raw producer records. Frozen Fission output has no ambiguous or empty variable display names, so the new rejection path has zero affected records in this population. Source/test file hashes and every full per-cell output are retained locally. The initial sequential evaluator launcher was replaced with two spawn workers plus one disjoint macOS-tail worker; complete checkpoints were preserved. No performance claim follows from this validation orchestration. + +## Submission and remaining decisions + +PR #103 stays a dependent draft on open #91. Upstream main advanced to `ae070b885b4e005ed37510a2c3445af6284dc2b9` via an unrelated GED change; this follow-up is based on the exact current PR head, without bundling that upstream change. The existing upstream run 38410600522 is `action_required`. A fresh exact-head CI run is required after approval/push. No comment, push, PR update, website/snapshot/CHANGELOG or dataset publication is performed by this local preparation. + +DecBench `docs/agents.md` requires human ownership/review of the complete contribution before submission. The earlier approval covered the original #103 patch, not this new delta. Review the complete follow-up patch, report and proposed PR update before approving submission. Official C++ type policy, cross-format virtual-base semantics, expanded oracle eligibility and stable producer binding IDs remain open. Fission performance work stays stopped and preserved separately. + +## Reproduction + +Use Python 3.12 and the declared development dependencies, with pinned rust-joern as in `.github/workflows/cpp-policy.yml`. Run that focused pytest/Black/Ruff/mypy lane, `pytest --no-cov` on both exact source revisions, then compare failure identities. `tests/test_cpp_review_contracts.py` contains the new standalone regressions. Full replay drivers, immutable source/input hashes, per-cell reports, logs and the complete proposed patch are retained under Fission `benchmark/artifacts/cpp-type-policy/2026-10-18-review-fixes/`. No canonical results tree or published score is rewritten. diff --git a/docs/validation/cpp-source-parser-classification.md b/docs/validation/cpp-source-parser-classification.md new file mode 100644 index 0000000..c9c0066 --- /dev/null +++ b/docs/validation/cpp-source-parser-classification.md @@ -0,0 +1,104 @@ +# C++ source-parser classification and bounded native MSVC control + +This follow-up changes diagnostic exclusion labels, not eligibility, metric +arithmetic or defaults. The C++ profiles remain experimental and outside Union. + +## Six new-image Windows mismatches + +The recollected WinSparkle images have different native identities from the +frozen 250-function pilot. Their preprocessing cannot be attached to old images. +The separate recollection is frozen at Hugging Face revision +`ebe719316aa303e860f75a362bc3ab740b31ff2d`, under `recollection-v1/`: + +https://huggingface.co/datasets/sjkim1127/decbench-cpp-qualification/tree/ebe719316aa303e860f75a362bc3ab740b31ff2d/recollection-v1 + +All six apparent `source_name_mismatch` rows resolve inside an erroneous +Tree-sitter definition named `DefDlgProcW`. Recovery starts at a Windows SDK +prototype, treats a subsequent enum as its body, and swallows roughly 200,000 +physical lines including unrelated STL methods. Four affected rows are O0, one +O2 and one O2-noinline. This is evidence of source-front-end failure; the +recovered name cannot establish a native/source identity conflict. + +`SourceCfgIndex.resolve()` now checks an error-bearing definition before its +name/template classification. It still rejects the span and never invokes CFG +parsing for it. It does not traverse swallowed definitions, relax name guards, +filter Windows SDK paths, rewrite sources or qualify template instantiations. +Four synthetic regressions cover matching and different recovered/native names, +with and without a template prefix. +Real-image replay changes exactly six labels to `source_syntax_error`; all +previously connected source definitions remain connected. The underlying SDK TU +parse failure remains unresolved. A compiler-confirmed minimized reproduction +has not yet been obtained; an isolated SDK prototype alone parses successfully. + +## Fixed 250-function regression replay + +A fresh cache-disabled replay uses all 45 cells and the same saved producer +outputs as the baseline at `81efc86dc9a0af22cb8d90e766b3e5a5e77f869c`. +All four tools retain source eligibility 56/250 and type eligibility 26/250. +All individual GED/type results are unchanged. Seven already excluded functions +change labels (three name mismatches and four template exclusions become source +syntax errors); each appears in all four reports. No function is added or lost. +This is separate from the six new-image Windows cases above. + +## Native MSVC controlled family + +A separate, handwritten compiler control exercises `Pair` field reads, lvalue +reference updates and conditional reference selection. This is a pipeline probe, +not a corpus-quality result or an addition to the scored population. It excludes +implicit object lifetime, templates and exception behavior. + +The reproducible collector and saved producer plan are in the author's corpus +repository, commit `52f8f5277a614a6be770af6271c0fb2ba21e5e7f`: + +https://github.com/sjkim1127/Decbench_SampleSet/tree/52f8f5277a614a6be770af6271c0fb2ba21e5e7f/targets/qualification-controls + +Original native MSVC artifacts, run `39818175807`: + +https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/39818175807 + +Recompilation and independent original rebuild, run `39818822625`: + +https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/39818822625 + +Both runs succeed. MSVC version is `19.51.36260.0`, x64, with `/Od /Ob0`, +`/O2`, and `/O2 /Ob0` respectively. Native PE/PDB GUID, Age, sections and exact +procedure extents are checked by the existing readers. Removing the producer +input's debug directory and CodeView payload preserves executable-section bytes. +Producers receive isolated images and three entry PCs per image, never oracle +source/PDB data. No original or recovered program is executed. + +All four producers emit 9/9 nonempty functions and connect 9/9 source and output +CFGs. Source CFG topology is definition-only; it does not establish semantic +correctness. The O0 PDB lacks explicit formal classification, so its three +functions remain type exclusions. Both O2 modes admit 6/6 functions per tool in +both type views; source-view perfect is 0/6 for every tool, ABI-view perfect is +0/6 Fission, 2/6 Kuna, 0/6 angr, 2/6 Ghidra. + +| Producer version | Requested | Compiles and links | Exact native code | +| --- | ---: | ---: | ---: | +| Frozen Fission CLI 0.2.3 | 9 | 8 | 4 | +| Kuna 1.808 | 9 | 9 | 5 | +| angr 9.2.213 | 9 | 9 | 5 | +| Ghidra 12.0.4 | 9 | 9 | 5 | + +Each output is compiled separately by the same MSVC version/toolset with the +original code-generation flags. All receive the same five fixed pseudo-type +aliases and an `extern "C"` linkage wrapper; output expressions, declarations +and types are otherwise unchanged. No compiler-driven repair or oracle source +injection occurs. Fission O0 has one rejected `uint8_t*` to `int*` assignment. +Successful compilation does not establish defined behavior: angr O0's field-read +output contains uninitialized pointers, and its reference selector declares +`unsigned long`, which is only 32 bits under LLP64. + +Exact bytes are extracted only from matched native PDB procedure ranges. +Capstone must decode every byte before comparison. Existing branch/PC-relative +normalization and nop skipping give the same equality counts in this probe; +these diagnostic counts are not official `byte_match` scores. Original rebuilds +preserve all nine procedure byte sequences. Two producer runs preserve all 36 +function records after excluding only `computation_time`. + +Runtime parity, arbitrary MSVC C++ output repair, corpus-wide MSVC byte scoring, +full-TU parsing and general Windows qualification remain unproven. No default +metric/cache generation, Rust-Joern pin, result overlay, site or dataset flags +are changed. Full local evidence is retained under +`Fission/benchmark/artifacts/cpp-msvc-source-slice/2026-10-22/`. diff --git a/docs/validation/cpp-type-policy-population.json b/docs/validation/cpp-type-policy-population.json new file mode 100644 index 0000000..c7ab4a7 --- /dev/null +++ b/docs/validation/cpp-type-policy-population.json @@ -0,0 +1,7329 @@ +{ + "seed": "cpp-native-debug-pilot-250-v1", + "selection": "hash order, round robin across all 45 binary cells; unique project-owned entry addresses; no score/size selection", + "functions": [ + { + "address": 70576, + "name": "_M_check_len", + "cell_id": "00", + "platform": "linux", + "project": "OpenCC", + "optimization": "O0", + "binary": "linux/arm64/O0/OpenCC/opencc", + "input": "inputs/linux/arm64/O0/OpenCC/opencc", + "id": "000" + }, + { + "address": 356320, + "name": "_M_erase_at_end", + "cell_id": "01", + "platform": "linux", + "project": "leveldb", + "optimization": "O0", + "binary": "linux/arm64/O0/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O0/leveldb/libleveldb.so.1.23.0", + "id": "001" + }, + { + "address": 330576, + "name": "operator==*>", + "cell_id": "02", + "platform": "linux", + "project": "mergerfs", + "optimization": "O0", + "binary": "linux/arm64/O0/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O0/mergerfs/mergerfs", + "id": "002" + }, + { + "address": 102688, + "name": "deallocate", + "cell_id": "03", + "platform": "linux", + "project": "snappy", + "optimization": "O0", + "binary": "linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "id": "003" + }, + { + "address": 263848, + "name": "operator!= >", + "cell_id": "04", + "platform": "linux", + "project": "ssdb", + "optimization": "O0", + "binary": "linux/arm64/O0/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O0/ssdb/ssdb-server", + "id": "004" + }, + { + "address": 365308, + "name": "__uninit_copy<__gnu_cxx::__normal_iterator >, sign*>", + "cell_id": "05", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O0", + "binary": "linux/arm64/O0/the-powder-toy/powder", + "input": "inputs/linux/arm64/O0/the-powder-toy/powder", + "id": "005" + }, + { + "address": 103824, + "name": "_extractValue", + "cell_id": "06", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2", + "binary": "linux/arm64/O2/OpenCC/opencc", + "input": "inputs/linux/arm64/O2/OpenCC/opencc", + "id": "006" + }, + { + "address": 206752, + "name": "Prev", + "cell_id": "07", + "platform": "linux", + "project": "leveldb", + "optimization": "O2", + "binary": "linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "id": "007" + }, + { + "address": 753616, + "name": "_M_erase", + "cell_id": "08", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2", + "binary": "linux/arm64/O2/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2/mergerfs/mergerfs", + "id": "008" + }, + { + "address": 36992, + "name": "DecompressAllTags >", + "cell_id": "09", + "platform": "linux", + "project": "snappy", + "optimization": "O2", + "binary": "linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "id": "009" + }, + { + "address": 786288, + "name": "qfront", + "cell_id": "10", + "platform": "linux", + "project": "ssdb", + "optimization": "O2", + "binary": "linux/arm64/O2/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2/ssdb/ssdb-server", + "id": "010" + }, + { + "address": 1866144, + "name": "DrawRect", + "cell_id": "11", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2", + "binary": "linux/arm64/O2/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2/the-powder-toy/powder", + "id": "011" + }, + { + "address": 58564, + "name": "get", + "cell_id": "12", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/linux/arm64/O2-noinline/OpenCC/opencc", + "id": "012" + }, + { + "address": 217428, + "name": "ok", + "cell_id": "13", + "platform": "linux", + "project": "leveldb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "id": "013" + }, + { + "address": 397636, + "name": "passthrough_open", + "cell_id": "14", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2-noinline/mergerfs/mergerfs", + "id": "014" + }, + { + "address": 60848, + "name": "SnappySinkAllocator", + "cell_id": "15", + "platform": "linux", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "id": "015" + }, + { + "address": 356304, + "name": "~_Vector_base", + "cell_id": "16", + "platform": "linux", + "project": "ssdb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2-noinline/ssdb/ssdb-server", + "id": "016" + }, + { + "address": 337760, + "name": "vector", + "cell_id": "17", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2-noinline/the-powder-toy/powder", + "id": "017" + }, + { + "address": 5369530208, + "name": "std::vector >::operator[]", + "cell_id": "18", + "platform": "windows", + "project": "OpenCC", + "optimization": "O0", + "binary": "windows/x86_64/O0/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O0/OpenCC/opencc.exe", + "id": "018" + }, + { + "address": 5368746256, + "name": "detour_free_trampoline", + "cell_id": "19", + "platform": "windows", + "project": "detours", + "optimization": "O0", + "binary": "windows/x86_64/O0/detours/withdll.exe", + "input": "inputs/windows/x86_64/O0/detours/withdll.exe", + "id": "019" + }, + { + "address": 5368871632, + "name": "std::_Tree,std::allocator,0> >::end", + "cell_id": "20", + "platform": "windows", + "project": "ninja", + "optimization": "O0", + "binary": "windows/x86_64/O0/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O0/ninja/ninja.exe", + "id": "020" + }, + { + "address": 6442458512, + "name": "stdext::exception::exception", + "cell_id": "21", + "platform": "windows", + "project": "snappy", + "optimization": "O0", + "binary": "windows/x86_64/O0/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O0/snappy/snappy.dll", + "id": "021" + }, + { + "address": 5371849952, + "name": "tracy::pdqsort_detail::sort3,std::equal_to >::Iter<0> *,", + "cell_id": "22", + "platform": "windows", + "project": "tracy", + "optimization": "O0", + "binary": "windows/x86_64/O0/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O0/tracy/tracy-profiler.exe", + "id": "022" + }, + { + "address": 6442532172, + "name": "std::num_get > >::_Parse_fp_with_locale<0>", + "cell_id": "23", + "platform": "windows", + "project": "winsparkle", + "optimization": "O0", + "binary": "windows/x86_64/O0/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O0/winsparkle/WinSparkle.dll", + "id": "023" + }, + { + "address": 5368851280, + "name": "std::basic_string,std::allocator >::_Reallocate_grow_by<,unsigned __int64,char>", + "cell_id": "24", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2", + "binary": "windows/x86_64/O2/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2/OpenCC/opencc.exe", + "id": "024" + }, + { + "address": 5368722096, + "name": "DetourEnumerateImportsEx", + "cell_id": "25", + "platform": "windows", + "project": "detours", + "optimization": "O2", + "binary": "windows/x86_64/O2/detours/withdll.exe", + "input": "inputs/windows/x86_64/O2/detours/withdll.exe", + "id": "025" + }, + { + "address": 5368726864, + "name": "std::basic_string,std::allocator >::assign", + "cell_id": "26", + "platform": "windows", + "project": "ninja", + "optimization": "O2", + "binary": "windows/x86_64/O2/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2/ninja/ninja.exe", + "id": "026" + }, + { + "address": 6442456320, + "name": "std::basic_string,std::allocator >::_Append", + "cell_id": "27", + "platform": "windows", + "project": "snappy", + "optimization": "O2", + "binary": "windows/x86_64/O2/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2/snappy/snappy.dll", + "id": "027" + }, + { + "address": 5371116688, + "name": "tracy::detail::Table<1,80,short,tracy::Worker::SourceLocationZones,tracy::hash,std::equal_to >::insertKeyPrepareEmptySpot", + "cell_id": "28", + "platform": "windows", + "project": "tracy", + "optimization": "O2", + "binary": "windows/x86_64/O2/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2/tracy/tracy-profiler.exe", + "id": "028" + }, + { + "address": 6442623764, + "name": "winsparkle::WinSparkleDialog::UpdateLayout", + "cell_id": "29", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2", + "binary": "windows/x86_64/O2/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2/winsparkle/WinSparkle.dll", + "id": "029" + }, + { + "address": 5369102496, + "name": "std::move", + "cell_id": "30", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "id": "030" + }, + { + "address": 5368753200, + "name": "CDetourDis::Copy66", + "cell_id": "31", + "platform": "windows", + "project": "detours", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/detours/withdll.exe", + "input": "inputs/windows/x86_64/O2-noinline/detours/withdll.exe", + "id": "031" + }, + { + "address": 5369006896, + "name": "std::basic_string,std::allocator >::basic_string,std::allocator >", + "cell_id": "32", + "platform": "windows", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2-noinline/ninja/ninja.exe", + "id": "032" + }, + { + "address": 6442481776, + "name": "snappy::UNALIGNED_LOAD64", + "cell_id": "33", + "platform": "windows", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2-noinline/snappy/snappy.dll", + "id": "033" + }, + { + "address": 5368918560, + "name": "std::ranges::_Unwrap_sent const *,std::pair const *>", + "cell_id": "34", + "platform": "windows", + "project": "tracy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "id": "034" + }, + { + "address": 6442521088, + "name": "std::forward,std::allocator > >", + "cell_id": "35", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "id": "035" + }, + { + "address": 4295469832, + "name": "Convert", + "cell_id": "36", + "platform": "macos", + "project": "OpenCC", + "optimization": "O0", + "binary": "macos/arm64/O0/OpenCC/opencc", + "input": "inputs/macos/arm64/O0/OpenCC/opencc", + "id": "036" + }, + { + "address": 4295304968, + "name": "Phony", + "cell_id": "37", + "platform": "macos", + "project": "ninja", + "optimization": "O0", + "binary": "macos/arm64/O0/ninja/ninja", + "input": "inputs/macos/arm64/O0/ninja/ninja", + "id": "037" + }, + { + "address": 38396, + "name": "GetBase", + "cell_id": "38", + "platform": "macos", + "project": "snappy", + "optimization": "O0", + "binary": "macos/arm64/O0/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O0/snappy/libsnappy.dylib", + "id": "038" + }, + { + "address": 4295331624, + "name": "ConstructBuffer", + "cell_id": "39", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2", + "binary": "macos/arm64/O2/OpenCC/opencc", + "input": "inputs/macos/arm64/O2/OpenCC/opencc", + "id": "039" + }, + { + "address": 4295011732, + "name": "LogEntry", + "cell_id": "40", + "platform": "macos", + "project": "ninja", + "optimization": "O2", + "binary": "macos/arm64/O2/ninja/ninja", + "input": "inputs/macos/arm64/O2/ninja/ninja", + "id": "040" + }, + { + "address": 16172, + "name": "Compress", + "cell_id": "41", + "platform": "macos", + "project": "snappy", + "optimization": "O2", + "binary": "macos/arm64/O2/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2/snappy/libsnappy.dylib", + "id": "041" + }, + { + "address": 4295091500, + "name": "ConversionInspectionResult", + "cell_id": "42", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/macos/arm64/O2-noinline/OpenCC/opencc", + "id": "042" + }, + { + "address": 4295172112, + "name": "TimerToMicros", + "cell_id": "43", + "platform": "macos", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/ninja/ninja", + "input": "inputs/macos/arm64/O2-noinline/ninja/ninja", + "id": "043" + }, + { + "address": 22440, + "name": "GetBase", + "cell_id": "44", + "platform": "macos", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "id": "044" + }, + { + "address": 168196, + "name": "PrettyPrefix", + "cell_id": "00", + "platform": "linux", + "project": "OpenCC", + "optimization": "O0", + "binary": "linux/arm64/O0/OpenCC/opencc", + "input": "inputs/linux/arm64/O0/OpenCC/opencc", + "id": "045" + }, + { + "address": 476864, + "name": "Logv", + "cell_id": "01", + "platform": "linux", + "project": "leveldb", + "optimization": "O0", + "binary": "linux/arm64/O0/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O0/leveldb/libleveldb.so.1.23.0", + "id": "046" + }, + { + "address": 300692, + "name": "back", + "cell_id": "02", + "platform": "linux", + "project": "mergerfs", + "optimization": "O0", + "binary": "linux/arm64/O0/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O0/mergerfs/mergerfs", + "id": "047" + }, + { + "address": 60756, + "name": "CompressFragmentDoubleHash", + "cell_id": "03", + "platform": "linux", + "project": "snappy", + "optimization": "O0", + "binary": "linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "id": "048" + }, + { + "address": 356140, + "name": "move_iterator", + "cell_id": "04", + "platform": "linux", + "project": "ssdb", + "optimization": "O0", + "binary": "linux/arm64/O0/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O0/ssdb/ssdb-server", + "id": "049" + }, + { + "address": 3357028, + "name": "operator--", + "cell_id": "05", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O0", + "binary": "linux/arm64/O0/the-powder-toy/powder", + "input": "inputs/linux/arm64/O0/the-powder-toy/powder", + "id": "050" + }, + { + "address": 43024, + "name": "StartObject", + "cell_id": "06", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2", + "binary": "linux/arm64/O2/OpenCC/opencc", + "input": "inputs/linux/arm64/O2/OpenCC/opencc", + "id": "051" + }, + { + "address": 162464, + "name": "ForEachOverlapping", + "cell_id": "07", + "platform": "linux", + "project": "leveldb", + "optimization": "O2", + "binary": "linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "id": "052" + }, + { + "address": 521680, + "name": "_M_check_len", + "cell_id": "08", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2", + "binary": "linux/arm64/O2/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2/mergerfs/mergerfs", + "id": "053" + }, + { + "address": 29568, + "name": "~SnappyIOVecReader", + "cell_id": "09", + "platform": "linux", + "project": "snappy", + "optimization": "O2", + "binary": "linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "id": "054" + }, + { + "address": 223908, + "name": "zset_one", + "cell_id": "10", + "platform": "linux", + "project": "ssdb", + "optimization": "O2", + "binary": "linux/arm64/O2/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2/ssdb/ssdb-server", + "id": "055" + }, + { + "address": 935632, + "name": "NotifyMenuListChanged", + "cell_id": "11", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2", + "binary": "linux/arm64/O2/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2/the-powder-toy/powder", + "id": "056" + }, + { + "address": 56544, + "name": "__niter_base*>", + "cell_id": "12", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/linux/arm64/O2-noinline/OpenCC/opencc", + "id": "057" + }, + { + "address": 262464, + "name": "_Rb_tree", + "cell_id": "13", + "platform": "linux", + "project": "leveldb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "id": "058" + }, + { + "address": 439632, + "name": "__addressof", + "cell_id": "14", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2-noinline/mergerfs/mergerfs", + "id": "059" + }, + { + "address": 40112, + "name": "snappy_compress", + "cell_id": "15", + "platform": "linux", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "id": "060" + }, + { + "address": 276864, + "name": "_S_key", + "cell_id": "16", + "platform": "linux", + "project": "ssdb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2-noinline/ssdb/ssdb-server", + "id": "061" + }, + { + "address": 1481648, + "name": "operator=", + "cell_id": "17", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2-noinline/the-powder-toy/powder", + "id": "062" + }, + { + "address": 5369331680, + "name": "std::num_put > >::_Ffmt", + "cell_id": "18", + "platform": "windows", + "project": "OpenCC", + "optimization": "O0", + "binary": "windows/x86_64/O0/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O0/OpenCC/opencc.exe", + "id": "063" + }, + { + "address": 5368726192, + "name": "DetourGetEntryPoint", + "cell_id": "19", + "platform": "windows", + "project": "detours", + "optimization": "O0", + "binary": "windows/x86_64/O0/detours/withdll.exe", + "input": "inputs/windows/x86_64/O0/detours/withdll.exe", + "id": "064" + }, + { + "address": 5368738640, + "name": "std::vector >::_Xlength", + "cell_id": "20", + "platform": "windows", + "project": "ninja", + "optimization": "O0", + "binary": "windows/x86_64/O0/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O0/ninja/ninja.exe", + "id": "065" + }, + { + "address": 6442494016, + "name": "std::forward<__m128i &>", + "cell_id": "21", + "platform": "windows", + "project": "snappy", + "optimization": "O0", + "binary": "windows/x86_64/O0/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O0/snappy/snappy.dll", + "id": "066" + }, + { + "address": 5369490400, + "name": "std::addressof >", + "cell_id": "22", + "platform": "windows", + "project": "tracy", + "optimization": "O0", + "binary": "windows/x86_64/O0/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O0/tracy/tracy-profiler.exe", + "id": "067" + }, + { + "address": 6442657484, + "name": "wxTopLevelWindowBase::DoGetScreenPosition", + "cell_id": "23", + "platform": "windows", + "project": "winsparkle", + "optimization": "O0", + "binary": "windows/x86_64/O0/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O0/winsparkle/WinSparkle.dll", + "id": "068" + }, + { + "address": 5369069536, + "name": "opencc::Segments::AddSegment", + "cell_id": "24", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2", + "binary": "windows/x86_64/O2/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2/OpenCC/opencc.exe", + "id": "069" + }, + { + "address": 5368736032, + "name": "DetourCreateProcessWithDllA", + "cell_id": "25", + "platform": "windows", + "project": "detours", + "optimization": "O2", + "binary": "windows/x86_64/O2/detours/withdll.exe", + "input": "inputs/windows/x86_64/O2/detours/withdll.exe", + "id": "070" + }, + { + "address": 5368984880, + "name": "std::_Tree_val >::_Erase_tree > >", + "cell_id": "26", + "platform": "windows", + "project": "ninja", + "optimization": "O2", + "binary": "windows/x86_64/O2/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2/ninja/ninja.exe", + "id": "071" + }, + { + "address": 6442471952, + "name": "snappy::ByteArraySource::", + "cell_id": "27", + "platform": "windows", + "project": "snappy", + "optimization": "O2", + "binary": "windows/x86_64/O2/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2/snappy/snappy.dll", + "id": "072" + }, + { + "address": 5369328048, + "name": "nlohmann::json_abi_v3_12_0::basic_json,std::allocator >,bool,__int64,unsigned __int64,double,std::allocator,nlohmann::json_abi_v3_12_0::adl_serializer,std::vector >,void>::push_back", + "cell_id": "28", + "platform": "windows", + "project": "tracy", + "optimization": "O2", + "binary": "windows/x86_64/O2/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2/tracy/tracy-profiler.exe", + "id": "073" + }, + { + "address": 6442591412, + "name": "wxMBConvStrictUTF8::Clone", + "cell_id": "29", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2", + "binary": "windows/x86_64/O2/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2/winsparkle/WinSparkle.dll", + "id": "074" + }, + { + "address": 5369017984, + "name": "opencc::AmbiguityStream::~AmbiguityStream", + "cell_id": "30", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "id": "075" + }, + { + "address": 5368725568, + "name": "EnumerateModulesInProcess", + "cell_id": "31", + "platform": "windows", + "project": "detours", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/detours/withdll.exe", + "input": "inputs/windows/x86_64/O2-noinline/detours/withdll.exe", + "id": "076" + }, + { + "address": 5368884160, + "name": "std::tuple,std::allocator > const &>::tuple,std::allocator > const &>,std::allocator > const &,0>", + "cell_id": "32", + "platform": "windows", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2-noinline/ninja/ninja.exe", + "id": "077" + }, + { + "address": 6442470240, + "name": "std::vector >::operator[]", + "cell_id": "33", + "platform": "windows", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2-noinline/snappy/snappy.dll", + "id": "078" + }, + { + "address": 5370210544, + "name": "tracy::pdqsort_detail::sort2,std::equal_to >::Iter<1> > > >,", + "cell_id": "34", + "platform": "windows", + "project": "tracy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "id": "079" + }, + { + "address": 6442501184, + "name": "std::_Vector_const_iterator > >::operator*", + "cell_id": "35", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "id": "080" + }, + { + "address": 4295136360, + "name": "NewFromString", + "cell_id": "36", + "platform": "macos", + "project": "OpenCC", + "optimization": "O0", + "binary": "macos/arm64/O0/OpenCC/opencc", + "input": "inputs/macos/arm64/O0/OpenCC/opencc", + "id": "081" + }, + { + "address": 4295101904, + "name": "~BuildLog", + "cell_id": "37", + "platform": "macos", + "project": "ninja", + "optimization": "O0", + "binary": "macos/arm64/O0/ninja/ninja", + "input": "inputs/macos/arm64/O0/ninja/ninja", + "id": "082" + }, + { + "address": 17812, + "name": "~CompressionContext", + "cell_id": "38", + "platform": "macos", + "project": "snappy", + "optimization": "O0", + "binary": "macos/arm64/O0/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O0/snappy/libsnappy.dylib", + "id": "083" + }, + { + "address": 4295071540, + "name": "NewFromFile", + "cell_id": "39", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2", + "binary": "macos/arm64/O2/OpenCC/opencc", + "input": "inputs/macos/arm64/O2/OpenCC/opencc", + "id": "084" + }, + { + "address": 4295180224, + "name": "UnreadToken", + "cell_id": "40", + "platform": "macos", + "project": "ninja", + "optimization": "O2", + "binary": "macos/arm64/O2/ninja/ninja", + "input": "inputs/macos/arm64/O2/ninja/ninja", + "id": "085" + }, + { + "address": 18392, + "name": "Skip", + "cell_id": "41", + "platform": "macos", + "project": "snappy", + "optimization": "O2", + "binary": "macos/arm64/O2/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2/snappy/libsnappy.dylib", + "id": "086" + }, + { + "address": 4295320812, + "name": "SerializeToFile", + "cell_id": "42", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/macos/arm64/O2-noinline/OpenCC/opencc", + "id": "087" + }, + { + "address": 4294978148, + "name": "DeferGuessParallelism", + "cell_id": "43", + "platform": "macos", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/ninja/ninja", + "input": "inputs/macos/arm64/O2-noinline/ninja/ninja", + "id": "088" + }, + { + "address": 4732, + "name": "GetAppendBufferVariable", + "cell_id": "44", + "platform": "macos", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "id": "089" + }, + { + "address": 199344, + "name": "shortID", + "cell_id": "00", + "platform": "linux", + "project": "OpenCC", + "optimization": "O0", + "binary": "linux/arm64/O0/OpenCC/opencc", + "input": "inputs/linux/arm64/O0/OpenCC/opencc", + "id": "090" + }, + { + "address": 180020, + "name": "vector", + "cell_id": "01", + "platform": "linux", + "project": "leveldb", + "optimization": "O0", + "binary": "linux/arm64/O0/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O0/leveldb/libleveldb.so.1.23.0", + "id": "091" + }, + { + "address": 608396, + "name": "_M_swap_data", + "cell_id": "02", + "platform": "linux", + "project": "mergerfs", + "optimization": "O0", + "binary": "linux/arm64/O0/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O0/mergerfs/mergerfs", + "id": "092" + }, + { + "address": 106728, + "name": "base", + "cell_id": "03", + "platform": "linux", + "project": "snappy", + "optimization": "O0", + "binary": "linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "id": "093" + }, + { + "address": 169424, + "name": "proc", + "cell_id": "04", + "platform": "linux", + "project": "ssdb", + "optimization": "O0", + "binary": "linux/arm64/O0/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O0/ssdb/ssdb-server", + "id": "094" + }, + { + "address": 2856520, + "name": "AddOption", + "cell_id": "05", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O0", + "binary": "linux/arm64/O0/the-powder-toy/powder", + "input": "inputs/linux/arm64/O0/the-powder-toy/powder", + "id": "095" + }, + { + "address": 72944, + "name": "argId", + "cell_id": "06", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2", + "binary": "linux/arm64/O2/OpenCC/opencc", + "input": "inputs/linux/arm64/O2/OpenCC/opencc", + "id": "096" + }, + { + "address": 145296, + "name": "Finalize", + "cell_id": "07", + "platform": "linux", + "project": "leveldb", + "optimization": "O2", + "binary": "linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "id": "097" + }, + { + "address": 281936, + "name": "set", + "cell_id": "08", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2", + "binary": "linux/arm64/O2/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2/mergerfs/mergerfs", + "id": "098" + }, + { + "address": 15072, + "name": "Append32", + "cell_id": "09", + "platform": "linux", + "project": "snappy", + "optimization": "O2", + "binary": "linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "id": "099" + }, + { + "address": 806980, + "name": "qslice", + "cell_id": "10", + "platform": "linux", + "project": "ssdb", + "optimization": "O2", + "binary": "linux/arm64/O2/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2/ssdb/ssdb-server", + "id": "100" + }, + { + "address": 381936, + "name": "~ParseException", + "cell_id": "11", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2", + "binary": "linux/arm64/O2/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2/the-powder-toy/powder", + "id": "101" + }, + { + "address": 129184, + "name": "__addressof", + "cell_id": "12", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/linux/arm64/O2-noinline/OpenCC/opencc", + "id": "102" + }, + { + "address": 130180, + "name": "leveldb_get", + "cell_id": "13", + "platform": "linux", + "project": "leveldb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "id": "103" + }, + { + "address": 1216368, + "name": "resize", + "cell_id": "14", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2-noinline/mergerfs/mergerfs", + "id": "104" + }, + { + "address": 41056, + "name": "~UncheckedByteArraySink", + "cell_id": "15", + "platform": "linux", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "id": "105" + }, + { + "address": 787764, + "name": "_M_construct", + "cell_id": "16", + "platform": "linux", + "project": "ssdb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2-noinline/ssdb/ssdb-server", + "id": "106" + }, + { + "address": 2305584, + "name": "SimulationData", + "cell_id": "17", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2-noinline/the-powder-toy/powder", + "id": "107" + }, + { + "address": 5368779088, + "name": "std::_Unfancy", + "cell_id": "18", + "platform": "windows", + "project": "OpenCC", + "optimization": "O0", + "binary": "windows/x86_64/O0/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O0/OpenCC/opencc.exe", + "id": "108" + }, + { + "address": 5368720736, + "name": "DetourEnumerateImportsThunk", + "cell_id": "19", + "platform": "windows", + "project": "detours", + "optimization": "O0", + "binary": "windows/x86_64/O0/detours/withdll.exe", + "input": "inputs/windows/x86_64/O0/detours/withdll.exe", + "id": "109" + }, + { + "address": 5369346976, + "name": "std::_Pop_heap_unchecked,std::allocator > *,std::less >", + "cell_id": "20", + "platform": "windows", + "project": "ninja", + "optimization": "O0", + "binary": "windows/x86_64/O0/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O0/ninja/ninja.exe", + "id": "110" + }, + { + "address": 6442494448, + "name": "std::_String_const_iterator > >::_String_const_iterator > >", + "cell_id": "21", + "platform": "windows", + "project": "snappy", + "optimization": "O0", + "binary": "windows/x86_64/O0/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O0/snappy/snappy.dll", + "id": "111" + }, + { + "address": 5369056096, + "name": "std::vector,std::allocator > >,std::allocator,std::allocator > > > >::_Emplace_one_at_back,std::allocator > > >", + "cell_id": "22", + "platform": "windows", + "project": "tracy", + "optimization": "O0", + "binary": "windows/x86_64/O0/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O0/tracy/tracy-profiler.exe", + "id": "112" + }, + { + "address": 6442582792, + "name": "std::basic_ios >::fill", + "cell_id": "23", + "platform": "windows", + "project": "winsparkle", + "optimization": "O0", + "binary": "windows/x86_64/O0/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O0/winsparkle/WinSparkle.dll", + "id": "113" + }, + { + "address": 5368756304, + "name": "TCLAP::ValueArg,std::allocator > >::~ValueArg,std::allocator > >", + "cell_id": "24", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2", + "binary": "windows/x86_64/O2/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2/OpenCC/opencc.exe", + "id": "114" + }, + { + "address": 5368749952, + "name": "DetourDetach", + "cell_id": "25", + "platform": "windows", + "project": "detours", + "optimization": "O2", + "binary": "windows/x86_64/O2/detours/withdll.exe", + "input": "inputs/windows/x86_64/O2/detours/withdll.exe", + "id": "115" + }, + { + "address": 5368752336, + "name": "Builder::FinishCommand", + "cell_id": "26", + "platform": "windows", + "project": "ninja", + "optimization": "O2", + "binary": "windows/x86_64/O2/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2/ninja/ninja.exe", + "id": "116" + }, + { + "address": 6442478192, + "name": "snappy::CompressFromIOVec", + "cell_id": "27", + "platform": "windows", + "project": "snappy", + "optimization": "O2", + "binary": "windows/x86_64/O2/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2/snappy/snappy.dll", + "id": "117" + }, + { + "address": 5371686704, + "name": "tracy::detail::Table<1,80,unsigned int,char const *,tracy::hash,std::equal_to >::try_increase_info", + "cell_id": "28", + "platform": "windows", + "project": "tracy", + "optimization": "O2", + "binary": "windows/x86_64/O2/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2/tracy/tracy-profiler.exe", + "id": "118" + }, + { + "address": 6442534180, + "name": "std::num_put > >::_Ifmt", + "cell_id": "29", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2", + "binary": "windows/x86_64/O2/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2/winsparkle/WinSparkle.dll", + "id": "119" + }, + { + "address": 5368786336, + "name": "std::_Ptr_base::_Decref", + "cell_id": "30", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "id": "120" + }, + { + "address": 5368748912, + "name": "DetourSetIgnoreTooSmall", + "cell_id": "31", + "platform": "windows", + "project": "detours", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/detours/withdll.exe", + "input": "inputs/windows/x86_64/O2-noinline/detours/withdll.exe", + "id": "121" + }, + { + "address": 5369006064, + "name": "std::_Tree,0> >::max_size", + "cell_id": "32", + "platform": "windows", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2-noinline/ninja/ninja.exe", + "id": "122" + }, + { + "address": 6442480800, + "name": "snappy::SnappyIOVecReader::Skip", + "cell_id": "33", + "platform": "windows", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2-noinline/snappy/snappy.dll", + "id": "123" + }, + { + "address": 5370194528, + "name": "std::make_pair,std::equal_to >::InsertionState>", + "cell_id": "34", + "platform": "windows", + "project": "tracy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "id": "124" + }, + { + "address": 6442590884, + "name": "wxWindowBase::CanBeOutsideClientArea", + "cell_id": "35", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "id": "125" + }, + { + "address": 4295165176, + "name": "GetDefault", + "cell_id": "36", + "platform": "macos", + "project": "OpenCC", + "optimization": "O0", + "binary": "macos/arm64/O0/OpenCC/opencc", + "input": "inputs/macos/arm64/O0/OpenCC/opencc", + "id": "126" + }, + { + "address": 4295304616, + "name": "GetBinding", + "cell_id": "37", + "platform": "macos", + "project": "ninja", + "optimization": "O0", + "binary": "macos/arm64/O0/ninja/ninja", + "input": "inputs/macos/arm64/O0/ninja/ninja", + "id": "127" + }, + { + "address": 49728, + "name": "SlowAppend", + "cell_id": "38", + "platform": "macos", + "project": "snappy", + "optimization": "O0", + "binary": "macos/arm64/O0/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O0/snappy/libsnappy.dylib", + "id": "128" + }, + { + "address": 4294972844, + "name": "CopyFileContents", + "cell_id": "39", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2", + "binary": "macos/arm64/O2/OpenCC/opencc", + "input": "inputs/macos/arm64/O2/OpenCC/opencc", + "id": "129" + }, + { + "address": 4295116668, + "name": "~PosixJobserverPool", + "cell_id": "40", + "platform": "macos", + "project": "ninja", + "optimization": "O2", + "binary": "macos/arm64/O2/ninja/ninja", + "input": "inputs/macos/arm64/O2/ninja/ninja", + "id": "130" + }, + { + "address": 4736, + "name": "Peek", + "cell_id": "41", + "platform": "macos", + "project": "snappy", + "optimization": "O2", + "binary": "macos/arm64/O2/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2/snappy/libsnappy.dylib", + "id": "131" + }, + { + "address": 4294974572, + "name": "Null", + "cell_id": "42", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/macos/arm64/O2-noinline/OpenCC/opencc", + "id": "132" + }, + { + "address": 4295196936, + "name": "PrintStatus", + "cell_id": "43", + "platform": "macos", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/ninja/ninja", + "input": "inputs/macos/arm64/O2-noinline/ninja/ninja", + "id": "133" + }, + { + "address": 19576, + "name": "TryFastAppend", + "cell_id": "44", + "platform": "macos", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "id": "134" + }, + { + "address": 150932, + "name": "_List_const_iterator", + "cell_id": "00", + "platform": "linux", + "project": "OpenCC", + "optimization": "O0", + "binary": "linux/arm64/O0/OpenCC/opencc", + "input": "inputs/linux/arm64/O0/OpenCC/opencc", + "id": "135" + }, + { + "address": 322480, + "name": "Match", + "cell_id": "01", + "platform": "linux", + "project": "leveldb", + "optimization": "O0", + "binary": "linux/arm64/O0/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O0/leveldb/libleveldb.so.1.23.0", + "id": "136" + }, + { + "address": 714420, + "name": "imbue", + "cell_id": "02", + "platform": "linux", + "project": "mergerfs", + "optimization": "O0", + "binary": "linux/arm64/O0/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O0/mergerfs/mergerfs", + "id": "137" + }, + { + "address": 90180, + "name": "DecompressAllTags", + "cell_id": "03", + "platform": "linux", + "project": "snappy", + "optimization": "O0", + "binary": "linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "id": "138" + }, + { + "address": 414736, + "name": "~_Vector_impl", + "cell_id": "04", + "platform": "linux", + "project": "ssdb", + "optimization": "O0", + "binary": "linux/arm64/O0/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O0/ssdb/ssdb-server", + "id": "139" + }, + { + "address": 1541412, + "name": "_M_erase_at_end", + "cell_id": "05", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O0", + "binary": "linux/arm64/O0/the-powder-toy/powder", + "input": "inputs/linux/arm64/O0/the-powder-toy/powder", + "id": "140" + }, + { + "address": 27856, + "name": "basic_string<>", + "cell_id": "06", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2", + "binary": "linux/arm64/O2/OpenCC/opencc", + "input": "inputs/linux/arm64/O2/OpenCC/opencc", + "id": "141" + }, + { + "address": 140768, + "name": "_M_erase", + "cell_id": "07", + "platform": "linux", + "project": "leveldb", + "optimization": "O2", + "binary": "linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "id": "142" + }, + { + "address": 224384, + "name": "populate_c_argv", + "cell_id": "08", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2", + "binary": "linux/arm64/O2/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2/mergerfs/mergerfs", + "id": "143" + }, + { + "address": 14480, + "name": "Skip", + "cell_id": "09", + "platform": "linux", + "project": "snappy", + "optimization": "O2", + "binary": "linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "id": "144" + }, + { + "address": 275520, + "name": "output_name", + "cell_id": "10", + "platform": "linux", + "project": "ssdb", + "optimization": "O2", + "binary": "linux/arm64/O2/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2/ssdb/ssdb-server", + "id": "145" + }, + { + "address": 1976576, + "name": "~ScrollPanel", + "cell_id": "11", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2", + "binary": "linux/arm64/O2/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2/the-powder-toy/powder", + "id": "146" + }, + { + "address": 113296, + "name": "operator!= >", + "cell_id": "12", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/linux/arm64/O2-noinline/OpenCC/opencc", + "id": "147" + }, + { + "address": 170864, + "name": "move", + "cell_id": "13", + "platform": "linux", + "project": "leveldb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "id": "148" + }, + { + "address": 272176, + "name": "get<0, subprocess::preexec_func::HolderBase*, std::default_delete >", + "cell_id": "14", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2-noinline/mergerfs/mergerfs", + "id": "149" + }, + { + "address": 66048, + "name": "DecompressBranchless", + "cell_id": "15", + "platform": "linux", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "id": "150" + }, + { + "address": 275312, + "name": "operator!=", + "cell_id": "16", + "platform": "linux", + "project": "ssdb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2-noinline/ssdb/ssdb-server", + "id": "151" + }, + { + "address": 1715348, + "name": "max_size", + "cell_id": "17", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2-noinline/the-powder-toy/powder", + "id": "152" + }, + { + "address": 5368934176, + "name": "std::allocator,std::allocator > const ,opencc::", + "cell_id": "18", + "platform": "windows", + "project": "OpenCC", + "optimization": "O0", + "binary": "windows/x86_64/O0/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O0/OpenCC/opencc.exe", + "id": "153" + }, + { + "address": 5368743200, + "name": "_vsnwprintf_l", + "cell_id": "19", + "platform": "windows", + "project": "detours", + "optimization": "O0", + "binary": "windows/x86_64/O0/detours/withdll.exe", + "input": "inputs/windows/x86_64/O0/detours/withdll.exe", + "id": "154" + }, + { + "address": 5369277184, + "name": "std::deque >::_Getal", + "cell_id": "20", + "platform": "windows", + "project": "ninja", + "optimization": "O0", + "binary": "windows/x86_64/O0/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O0/ninja/ninja.exe", + "id": "155" + }, + { + "address": 6442460656, + "name": "std::basic_string,std::allocator >::max_size", + "cell_id": "21", + "platform": "windows", + "project": "snappy", + "optimization": "O0", + "binary": "windows/x86_64/O0/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O0/snappy/snappy.dll", + "id": "156" + }, + { + "address": 5370617184, + "name": "std::make_heap > >,", + "cell_id": "22", + "platform": "windows", + "project": "tracy", + "optimization": "O0", + "binary": "windows/x86_64/O0/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O0/tracy/tracy-profiler.exe", + "id": "157" + }, + { + "address": 6442571460, + "name": "std::error_condition::category", + "cell_id": "23", + "platform": "windows", + "project": "winsparkle", + "optimization": "O0", + "binary": "windows/x86_64/O0/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O0/winsparkle/WinSparkle.dll", + "id": "158" + }, + { + "address": 5368803552, + "name": "std::locale::facet::_Decref", + "cell_id": "24", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2", + "binary": "windows/x86_64/O2/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2/OpenCC/opencc.exe", + "id": "159" + }, + { + "address": 5368723152, + "name": "DetourFindFunction", + "cell_id": "25", + "platform": "windows", + "project": "detours", + "optimization": "O2", + "binary": "windows/x86_64/O2/detours/withdll.exe", + "input": "inputs/windows/x86_64/O2/detours/withdll.exe", + "id": "160" + }, + { + "address": 5368907216, + "name": "ManifestParser::ParseRule", + "cell_id": "26", + "platform": "windows", + "project": "ninja", + "optimization": "O2", + "binary": "windows/x86_64/O2/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2/ninja/ninja.exe", + "id": "161" + }, + { + "address": 6442483808, + "name": "snappy::SnappyDecompressor::RefillTag", + "cell_id": "27", + "platform": "windows", + "project": "snappy", + "optimization": "O2", + "binary": "windows/x86_64/O2/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2/snappy/snappy.dll", + "id": "162" + }, + { + "address": 5368911392, + "name": "std::vector >::_Buy_nonzero", + "cell_id": "28", + "platform": "windows", + "project": "tracy", + "optimization": "O2", + "binary": "windows/x86_64/O2/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2/tracy/tracy-profiler.exe", + "id": "163" + }, + { + "address": 6442598780, + "name": "wxGDIObject::IsFree", + "cell_id": "29", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2", + "binary": "windows/x86_64/O2/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2/winsparkle/WinSparkle.dll", + "id": "164" + }, + { + "address": 5369017024, + "name": "std::vector,std::allocator >,enum EvalString::TokenType>,std::allocator,std::allocator >,enum EvalString::TokenType> > >::_Emplace_back_with_unused_capacity,std::allocator >,enum EvalString::TokenType> >", + "cell_id": "32", + "platform": "windows", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2-noinline/ninja/ninja.exe", + "id": "167" + }, + { + "address": 6442484096, + "name": "std::basic_string,std::allocator >::empty", + "cell_id": "33", + "platform": "windows", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2-noinline/snappy/snappy.dll", + "id": "168" + }, + { + "address": 5369723472, + "name": "tracy::Vector,std::equal_to >::Iter<0> >::~Vector,std::equal_to >::Iter<0> >", + "cell_id": "34", + "platform": "windows", + "project": "tracy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "id": "169" + }, + { + "address": 6442619508, + "name": "std::_Default_allocator_traits,std::allocator > > >::construct,std::allocator >,std::basic_string,std::allocator > >", + "cell_id": "35", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "id": "170" + }, + { + "address": 4295560260, + "name": "TakeErrorMessage", + "cell_id": "36", + "platform": "macos", + "project": "OpenCC", + "optimization": "O0", + "binary": "macos/arm64/O0/OpenCC/opencc", + "input": "inputs/macos/arm64/O0/OpenCC/opencc", + "id": "171" + }, + { + "address": 4295129500, + "name": "EdgeWanted", + "cell_id": "37", + "platform": "macos", + "project": "ninja", + "optimization": "O0", + "binary": "macos/arm64/O0/ninja/ninja", + "input": "inputs/macos/arm64/O0/ninja/ninja", + "id": "172" + }, + { + "address": 23648, + "name": "SnappyArrayWriter", + "cell_id": "38", + "platform": "macos", + "project": "snappy", + "optimization": "O0", + "binary": "macos/arm64/O0/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O0/snappy/libsnappy.dylib", + "id": "173" + }, + { + "address": 4295247992, + "name": "AppendConvertedSegment", + "cell_id": "39", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2", + "binary": "macos/arm64/O2/OpenCC/opencc", + "input": "inputs/macos/arm64/O2/OpenCC/opencc", + "id": "174" + }, + { + "address": 4295103556, + "name": "~EdgeEnv", + "cell_id": "40", + "platform": "macos", + "project": "ninja", + "optimization": "O2", + "binary": "macos/arm64/O2/ninja/ninja", + "input": "inputs/macos/arm64/O2/ninja/ninja", + "id": "175" + }, + { + "address": 25764, + "name": "AppendFromSelf", + "cell_id": "41", + "platform": "macos", + "project": "snappy", + "optimization": "O2", + "binary": "macos/arm64/O2/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2/snappy/libsnappy.dylib", + "id": "176" + }, + { + "address": 4295068292, + "name": "StrSingleValueDictEntry", + "cell_id": "42", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/macos/arm64/O2-noinline/OpenCC/opencc", + "id": "177" + }, + { + "address": 4295086932, + "name": "ParseEdge", + "cell_id": "43", + "platform": "macos", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/ninja/ninja", + "input": "inputs/macos/arm64/O2-noinline/ninja/ninja", + "id": "178" + }, + { + "address": 21628, + "name": "InternalUncompressAllTags", + "cell_id": "44", + "platform": "macos", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "id": "179" + }, + { + "address": 68704, + "name": "_S_relocate", + "cell_id": "00", + "platform": "linux", + "project": "OpenCC", + "optimization": "O0", + "binary": "linux/arm64/O0/OpenCC/opencc", + "input": "inputs/linux/arm64/O0/OpenCC/opencc", + "id": "180" + }, + { + "address": 361924, + "name": "_M_get_insert_unique_pos", + "cell_id": "01", + "platform": "linux", + "project": "leveldb", + "optimization": "O0", + "binary": "linux/arm64/O0/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O0/leveldb/libleveldb.so.1.23.0", + "id": "181" + }, + { + "address": 1187244, + "name": "_Iter_pred", + "cell_id": "02", + "platform": "linux", + "project": "mergerfs", + "optimization": "O0", + "binary": "linux/arm64/O0/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O0/mergerfs/mergerfs", + "id": "182" + }, + { + "address": 80028, + "name": "SetOutputPtr", + "cell_id": "03", + "platform": "linux", + "project": "snappy", + "optimization": "O0", + "binary": "linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "id": "183" + }, + { + "address": 243992, + "name": "base", + "cell_id": "04", + "platform": "linux", + "project": "ssdb", + "optimization": "O0", + "binary": "linux/arm64/O0/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O0/ssdb/ssdb-server", + "id": "184" + }, + { + "address": 2078336, + "name": "_M_create&>", + "cell_id": "05", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O0", + "binary": "linux/arm64/O0/the-powder-toy/powder", + "input": "inputs/linux/arm64/O0/the-powder-toy/powder", + "id": "185" + }, + { + "address": 82080, + "name": "longID", + "cell_id": "06", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2", + "binary": "linux/arm64/O2/OpenCC/opencc", + "input": "inputs/linux/arm64/O2/OpenCC/opencc", + "id": "186" + }, + { + "address": 171952, + "name": "Clear", + "cell_id": "07", + "platform": "linux", + "project": "leveldb", + "optimization": "O2", + "binary": "linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "id": "187" + }, + { + "address": 146208, + "name": "_M_erase", + "cell_id": "08", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2", + "binary": "linux/arm64/O2/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2/mergerfs/mergerfs", + "id": "188" + }, + { + "address": 15028, + "name": "~Sink", + "cell_id": "09", + "platform": "linux", + "project": "snappy", + "optimization": "O2", + "binary": "linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "id": "189" + }, + { + "address": 205488, + "name": "scan", + "cell_id": "10", + "platform": "linux", + "project": "ssdb", + "optimization": "O2", + "binary": "linux/arm64/O2/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2/ssdb/ssdb-server", + "id": "190" + }, + { + "address": 1920768, + "name": "TextPosition", + "cell_id": "11", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2", + "binary": "linux/arm64/O2/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2/the-powder-toy/powder", + "id": "191" + }, + { + "address": 98016, + "name": "SpecificationException", + "cell_id": "12", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/linux/arm64/O2-noinline/OpenCC/opencc", + "id": "192" + }, + { + "address": 332032, + "name": "BackgroundThreadMain", + "cell_id": "13", + "platform": "linux", + "project": "leveldb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "id": "193" + }, + { + "address": 568112, + "name": "__adjust_heap<__gnu_cxx::__normal_iterator > >, long int, char, __gnu_cxx::__ops::_Iter_less_iter>", + "cell_id": "14", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2-noinline/mergerfs/mergerfs", + "id": "194" + }, + { + "address": 47300, + "name": "Reset", + "cell_id": "15", + "platform": "linux", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "id": "195" + }, + { + "address": 798180, + "name": "hincr", + "cell_id": "16", + "platform": "linux", + "project": "ssdb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2-noinline/ssdb/ssdb-server", + "id": "196" + }, + { + "address": 1354788, + "name": "__invoke_impl&, bool>", + "cell_id": "17", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2-noinline/the-powder-toy/powder", + "id": "197" + }, + { + "address": 5369420688, + "name": "std::_Allocate_at_least_helper >", + "cell_id": "18", + "platform": "windows", + "project": "OpenCC", + "optimization": "O0", + "binary": "windows/x86_64/O0/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O0/OpenCC/opencc.exe", + "id": "198" + }, + { + "address": 5368758768, + "name": "CDetourDis::SanityCheckSystem", + "cell_id": "19", + "platform": "windows", + "project": "detours", + "optimization": "O0", + "binary": "windows/x86_64/O0/detours/withdll.exe", + "input": "inputs/windows/x86_64/O0/detours/withdll.exe", + "id": "199" + }, + { + "address": 5369171312, + "name": "std::_Uninitialized_backout,std::equal_to,std::allocator > > > > > > *>::~_Uninitialized_backout,std::equal_to,std::allocator > > > > > > *>", + "cell_id": "20", + "platform": "windows", + "project": "ninja", + "optimization": "O0", + "binary": "windows/x86_64/O0/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O0/ninja/ninja.exe", + "id": "200" + }, + { + "address": 6442500448, + "name": "snappy::SnappyDecompressionValidator::CheckLength", + "cell_id": "21", + "platform": "windows", + "project": "snappy", + "optimization": "O0", + "binary": "windows/x86_64/O0/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O0/snappy/snappy.dll", + "id": "201" + }, + { + "address": 5369167696, + "name": "std::_Uninitialized_backout_al,std::allocator >,bool,__int64,unsigned __int64,double,std::allocator,nlohmann::json_abi_v3_12_0::adl_serializer,std::vector >,void> > >::_Release", + "cell_id": "22", + "platform": "windows", + "project": "tracy", + "optimization": "O0", + "binary": "windows/x86_64/O0/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O0/tracy/tracy-profiler.exe", + "id": "202" + }, + { + "address": 6442584892, + "name": "std::_Iostream_error_category2::name", + "cell_id": "23", + "platform": "windows", + "project": "winsparkle", + "optimization": "O0", + "binary": "windows/x86_64/O0/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O0/winsparkle/WinSparkle.dll", + "id": "203" + }, + { + "address": 5369048736, + "name": "opencc::UTF8Util::IdeographicDescriptionOperatorArity", + "cell_id": "24", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2", + "binary": "windows/x86_64/O2/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2/OpenCC/opencc.exe", + "id": "204" + }, + { + "address": 5368757712, + "name": "CDetourDis::SetCodeModule", + "cell_id": "25", + "platform": "windows", + "project": "detours", + "optimization": "O2", + "binary": "windows/x86_64/O2/detours/withdll.exe", + "input": "inputs/windows/x86_64/O2/detours/withdll.exe", + "id": "205" + }, + { + "address": 5368934368, + "name": "Pool::DelayEdge", + "cell_id": "26", + "platform": "windows", + "project": "ninja", + "optimization": "O2", + "binary": "windows/x86_64/O2/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2/ninja/ninja.exe", + "id": "206" + }, + { + "address": 6442490224, + "name": "std::vector >::max_size", + "cell_id": "27", + "platform": "windows", + "project": "snappy", + "optimization": "O2", + "binary": "windows/x86_64/O2/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2/snappy/snappy.dll", + "id": "207" + }, + { + "address": 5370045408, + "name": "tracy::detail::Table<1,80,unsigned int,tracy::View::MemPathData,tracy::hash,std::equal_to >::initData", + "cell_id": "28", + "platform": "windows", + "project": "tracy", + "optimization": "O2", + "binary": "windows/x86_64/O2/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2/tracy/tracy-profiler.exe", + "id": "208" + }, + { + "address": 6442592052, + "name": "wxGDIObject::CreateRefData", + "cell_id": "29", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2", + "binary": "windows/x86_64/O2/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2/winsparkle/WinSparkle.dll", + "id": "209" + }, + { + "address": 5368722512, + "name": "TCLAP::ExtractValue,std::allocator > >", + "cell_id": "30", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "id": "210" + }, + { + "address": 5368724080, + "name": "DetourRestoreAfterWith", + "cell_id": "31", + "platform": "windows", + "project": "detours", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/detours/withdll.exe", + "input": "inputs/windows/x86_64/O2-noinline/detours/withdll.exe", + "id": "211" + }, + { + "address": 5368735072, + "name": "std::_Compressed_pair,std::_Compressed_pair >,std::_Tree_val >,1>,1>::_Compressed_pair,std::_Compressed_pair >,std::_Tree_val >,1>,1> const &,std::_Zero_then_variadic_args_t>", + "cell_id": "32", + "platform": "windows", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2-noinline/ninja/ninja.exe", + "id": "212" + }, + { + "address": 6442479472, + "name": "snappy::RawCompressFromIOVec", + "cell_id": "33", + "platform": "windows", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2-noinline/snappy/snappy.dll", + "id": "213" + }, + { + "address": 5371373120, + "name": "std::addressof<", + "cell_id": "34", + "platform": "windows", + "project": "tracy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "id": "214" + }, + { + "address": 6442624904, + "name": "std::_Vector_iterator > >::_Unwrapped", + "cell_id": "35", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "id": "215" + }, + { + "address": 4295144012, + "name": "ConfigInternal", + "cell_id": "36", + "platform": "macos", + "project": "OpenCC", + "optimization": "O0", + "binary": "macos/arm64/O0/OpenCC/opencc", + "input": "inputs/macos/arm64/O0/OpenCC/opencc", + "id": "216" + }, + { + "address": 4295414460, + "name": "operator[]", + "cell_id": "37", + "platform": "macos", + "project": "ninja", + "optimization": "O0", + "binary": "macos/arm64/O0/ninja/ninja", + "input": "inputs/macos/arm64/O0/ninja/ninja", + "id": "217" + }, + { + "address": 20972, + "name": "CompressFromIOVec", + "cell_id": "38", + "platform": "macos", + "project": "snappy", + "optimization": "O0", + "binary": "macos/arm64/O0/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O0/snappy/libsnappy.dylib", + "id": "218" + }, + { + "address": 4295335864, + "name": "TextDict", + "cell_id": "39", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2", + "binary": "macos/arm64/O2/OpenCC/opencc", + "input": "inputs/macos/arm64/O2/OpenCC/opencc", + "id": "219" + }, + { + "address": 4295045872, + "name": "PrintHeader", + "cell_id": "40", + "platform": "macos", + "project": "ninja", + "optimization": "O2", + "binary": "macos/arm64/O2/ninja/ninja", + "input": "inputs/macos/arm64/O2/ninja/ninja", + "id": "220" + }, + { + "address": 25496, + "name": "TryFastAppend", + "cell_id": "41", + "platform": "macos", + "project": "snappy", + "optimization": "O2", + "binary": "macos/arm64/O2/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2/snappy/libsnappy.dylib", + "id": "221" + }, + { + "address": 4295088464, + "name": "~ConversionChain", + "cell_id": "42", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/macos/arm64/O2-noinline/OpenCC/opencc", + "id": "222" + }, + { + "address": 4295188472, + "name": "Node", + "cell_id": "43", + "platform": "macos", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/ninja/ninja", + "input": "inputs/macos/arm64/O2-noinline/ninja/ninja", + "id": "223" + }, + { + "address": 21516, + "name": "UnalignedCopy64", + "cell_id": "44", + "platform": "macos", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "id": "224" + }, + { + "address": 137184, + "name": "parse", + "cell_id": "00", + "platform": "linux", + "project": "OpenCC", + "optimization": "O0", + "binary": "linux/arm64/O0/OpenCC/opencc", + "input": "inputs/linux/arm64/O0/OpenCC/opencc", + "id": "225" + }, + { + "address": 251792, + "name": "__niter_base", + "cell_id": "01", + "platform": "linux", + "project": "leveldb", + "optimization": "O0", + "binary": "linux/arm64/O0/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O0/leveldb/libleveldb.so.1.23.0", + "id": "226" + }, + { + "address": 1241028, + "name": "is_same_file", + "cell_id": "02", + "platform": "linux", + "project": "mergerfs", + "optimization": "O0", + "binary": "linux/arm64/O0/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O0/mergerfs/mergerfs", + "id": "227" + }, + { + "address": 57948, + "name": "RequiredSize", + "cell_id": "03", + "platform": "linux", + "project": "snappy", + "optimization": "O0", + "binary": "linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "id": "228" + }, + { + "address": 246436, + "name": "~SSDBImpl", + "cell_id": "04", + "platform": "linux", + "project": "ssdb", + "optimization": "O0", + "binary": "linux/arm64/O0/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O0/ssdb/ssdb-server", + "id": "229" + }, + { + "address": 1848584, + "name": "reset", + "cell_id": "05", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O0", + "binary": "linux/arm64/O0/the-powder-toy/powder", + "input": "inputs/linux/arm64/O0/the-powder-toy/powder", + "id": "230" + }, + { + "address": 99248, + "name": "WriteInspectionResultJson > > >", + "cell_id": "06", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2", + "binary": "linux/arm64/O2/OpenCC/opencc", + "input": "inputs/linux/arm64/O2/OpenCC/opencc", + "id": "231" + }, + { + "address": 116064, + "name": "NewIterator", + "cell_id": "07", + "platform": "linux", + "project": "leveldb", + "optimization": "O2", + "binary": "linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "id": "232" + }, + { + "address": 144064, + "name": "from_string", + "cell_id": "08", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2", + "binary": "linux/arm64/O2/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2/mergerfs/mergerfs", + "id": "233" + }, + { + "address": 25072, + "name": "operator=", + "cell_id": "09", + "platform": "linux", + "project": "snappy", + "optimization": "O2", + "binary": "linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "id": "234" + }, + { + "address": 163600, + "name": "_run_thread", + "cell_id": "10", + "platform": "linux", + "project": "ssdb", + "optimization": "O2", + "binary": "linux/arm64/O2/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2/ssdb/ssdb-server", + "id": "235" + }, + { + "address": 970464, + "name": "~NGravityOption", + "cell_id": "11", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2", + "binary": "linux/arm64/O2/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2/the-powder-toy/powder", + "id": "236" + }, + { + "address": 82944, + "name": "~Exception", + "cell_id": "12", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/linux/arm64/O2-noinline/OpenCC/opencc", + "id": "237" + }, + { + "address": 347348, + "name": "Size", + "cell_id": "13", + "platform": "linux", + "project": "leveldb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "id": "238" + }, + { + "address": 1105408, + "name": "signbit", + "cell_id": "14", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2-noinline/mergerfs/mergerfs", + "id": "239" + }, + { + "address": 56128, + "name": "~SnappyIOVecReader", + "cell_id": "15", + "platform": "linux", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "id": "240" + }, + { + "address": 259856, + "name": "_M_valptr", + "cell_id": "16", + "platform": "linux", + "project": "ssdb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2-noinline/ssdb/ssdb-server", + "id": "241" + }, + { + "address": 2772724, + "name": "assign", + "cell_id": "17", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2-noinline/the-powder-toy/powder", + "id": "242" + }, + { + "address": 5369175024, + "name": "rapidjson::GenericUri,rapidjson::MemoryPoolAllocator >,rapidjson::CrtAllocator>::GetStringLength", + "cell_id": "18", + "platform": "windows", + "project": "OpenCC", + "optimization": "O0", + "binary": "windows/x86_64/O0/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O0/OpenCC/opencc.exe", + "id": "243" + }, + { + "address": 5368728496, + "name": "EnumerateModulesInProcess", + "cell_id": "19", + "platform": "windows", + "project": "detours", + "optimization": "O0", + "binary": "windows/x86_64/O0/detours/withdll.exe", + "input": "inputs/windows/x86_64/O0/detours/withdll.exe", + "id": "244" + }, + { + "address": 5369228496, + "name": "emhash8::HashMap,std::equal_to,std::allocator >,emhash8::DefaultPolicy>::rebuild", + "cell_id": "20", + "platform": "windows", + "project": "ninja", + "optimization": "O0", + "binary": "windows/x86_64/O0/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O0/ninja/ninja.exe", + "id": "245" + }, + { + "address": 6442500704, + "name": "snappy::ExtractOffset", + "cell_id": "21", + "platform": "windows", + "project": "snappy", + "optimization": "O0", + "binary": "windows/x86_64/O0/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O0/snappy/snappy.dll", + "id": "246" + }, + { + "address": 5371386032, + "name": "tracy::detail::Table<1,80,unsigned int,tracy::View::MemPathData,tracy::hash,std::equal_to >::emplace", + "cell_id": "22", + "platform": "windows", + "project": "tracy", + "optimization": "O0", + "binary": "windows/x86_64/O0/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O0/tracy/tracy-profiler.exe", + "id": "247" + }, + { + "address": 6442616972, + "name": "wxUnusedVar", + "cell_id": "23", + "platform": "windows", + "project": "winsparkle", + "optimization": "O0", + "binary": "windows/x86_64/O0/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O0/winsparkle/WinSparkle.dll", + "id": "248" + }, + { + "address": 5369106320, + "name": "std::_Temporary_owner", + "cell_id": "01", + "platform": "linux", + "project": "leveldb", + "optimization": "O0", + "binary": "linux/arm64/O0/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O0/leveldb/libleveldb.so.1.23.0", + "id": "226" + } + ] + }, + { + "id": "02", + "schema": "decbench-cpp-qualification-cell-v2", + "state": "imported-payload-verified", + "platform": "linux", + "architecture": "aarch64", + "project": "mergerfs", + "optimization": "O0", + "source_repository": "trapexit/mergerfs", + "source_revision": "515b0549ef615418df41ed3ffd993d036bc52e1f", + "binary": "linux/arm64/O0/mergerfs/mergerfs", + "binary_sha256": "1df5d6e93b778da35dd50980f87243534cb9b0d0c2ec0ee65263ee2d2f9c7c2e", + "stripped": "linux/arm64/O0/mergerfs/mergerfs.stripped", + "stripped_sha256": "4e2a3d9e45cf60ac57685fa4f6571dc52b123a4a6edaa1f837057d472c381669", + "code_sections_equal": true, + "debug_removed": true, + "symtab_removed": true, + "source_preprocessed_sha256": "35e276076678c183ec81120ca73b29bc645862abf818a7a7fc5b6b09ec3c9f2d", + "raw_corpus_revision": "f1b7e849641baf1d35e293f400bc36aa7a1a4e2c", + "scoring_ready": false, + "remaining_gates": [ + "original source archive", + "effective per-TU compile/link command provenance" + ], + "source_owned_function_records": 32022, + "source_owned_cus": 168, + "debug_producers": [ + "GNU C++20 13.3.0 -mlittle-endian -mabi=lp64 -g -O0 -std=c++20 -fno-builtin -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection", + "GNU C++20 13.3.0 -mlittle-endian -mabi=lp64 -g -O0 -std=c++20 -std=c++20 -fno-builtin -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection" + ], + "input": "inputs/linux/arm64/O0/mergerfs/mergerfs", + "original_source_sha256": "50d31cd379b545084494377a0859890eb9a79fdb1be4bf2483d7d99a2d5a7c01", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 12087, + "rejected_unmapped_range_records": 61, + "available_unique_addresses": 12087, + "rows": [ + { + "address": 330576, + "name": "operator==*>", + "cell_id": "02", + "platform": "linux", + "project": "mergerfs", + "optimization": "O0", + "binary": "linux/arm64/O0/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O0/mergerfs/mergerfs", + "id": "002" + }, + { + "address": 300692, + "name": "back", + "cell_id": "02", + "platform": "linux", + "project": "mergerfs", + "optimization": "O0", + "binary": "linux/arm64/O0/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O0/mergerfs/mergerfs", + "id": "047" + }, + { + "address": 608396, + "name": "_M_swap_data", + "cell_id": "02", + "platform": "linux", + "project": "mergerfs", + "optimization": "O0", + "binary": "linux/arm64/O0/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O0/mergerfs/mergerfs", + "id": "092" + }, + { + "address": 714420, + "name": "imbue", + "cell_id": "02", + "platform": "linux", + "project": "mergerfs", + "optimization": "O0", + "binary": "linux/arm64/O0/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O0/mergerfs/mergerfs", + "id": "137" + }, + { + "address": 1187244, + "name": "_Iter_pred", + "cell_id": "02", + "platform": "linux", + "project": "mergerfs", + "optimization": "O0", + "binary": "linux/arm64/O0/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O0/mergerfs/mergerfs", + "id": "182" + }, + { + "address": 1241028, + "name": "is_same_file", + "cell_id": "02", + "platform": "linux", + "project": "mergerfs", + "optimization": "O0", + "binary": "linux/arm64/O0/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O0/mergerfs/mergerfs", + "id": "227" + } + ] + }, + { + "id": "03", + "schema": "decbench-cpp-qualification-cell-v2", + "state": "imported-payload-verified", + "platform": "linux", + "architecture": "aarch64", + "project": "snappy", + "optimization": "O0", + "source_repository": "google/snappy", + "source_revision": "2a6c78c781cb76ad191795449cb2267f33138150", + "binary": "linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "binary_sha256": "d58a0fbe8d916cd64df6da9d6d1bbf17996a04af277148a6a44da2352f928592", + "stripped": "linux/arm64/O0/snappy/libsnappy.so.1.2.2.stripped", + "stripped_sha256": "f71312d8db4ca40bb626132b3c157c18f49222b8b6619edb949664b2b98bf89f", + "code_sections_equal": true, + "debug_removed": true, + "symtab_removed": true, + "source_preprocessed_sha256": "90749ef2905e02f475945930070dd5968fdeabac6325d7db72f96d90880f8841", + "raw_corpus_revision": "f1b7e849641baf1d35e293f400bc36aa7a1a4e2c", + "scoring_ready": false, + "remaining_gates": [ + "original source archive", + "effective per-TU compile/link command provenance" + ], + "source_owned_function_records": 358, + "source_owned_cus": 4, + "debug_producers": [ + "GNU C++11 13.3.0 -mlittle-endian -mabi=lp64 -g -O0 -std=c++11 -fno-builtin -fno-exceptions -fno-rtti -fPIC -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection" + ], + "input": "inputs/linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "original_source_sha256": "c3bddf6e337e8ca86620ac6ae668b4288f8352f2453dc3f7fa1500a7d56bcfb0", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 357, + "rejected_unmapped_range_records": 0, + "available_unique_addresses": 357, + "rows": [ + { + "address": 102688, + "name": "deallocate", + "cell_id": "03", + "platform": "linux", + "project": "snappy", + "optimization": "O0", + "binary": "linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "id": "003" + }, + { + "address": 60756, + "name": "CompressFragmentDoubleHash", + "cell_id": "03", + "platform": "linux", + "project": "snappy", + "optimization": "O0", + "binary": "linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "id": "048" + }, + { + "address": 106728, + "name": "base", + "cell_id": "03", + "platform": "linux", + "project": "snappy", + "optimization": "O0", + "binary": "linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "id": "093" + }, + { + "address": 90180, + "name": "DecompressAllTags", + "cell_id": "03", + "platform": "linux", + "project": "snappy", + "optimization": "O0", + "binary": "linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "id": "138" + }, + { + "address": 80028, + "name": "SetOutputPtr", + "cell_id": "03", + "platform": "linux", + "project": "snappy", + "optimization": "O0", + "binary": "linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "id": "183" + }, + { + "address": 57948, + "name": "RequiredSize", + "cell_id": "03", + "platform": "linux", + "project": "snappy", + "optimization": "O0", + "binary": "linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O0/snappy/libsnappy.so.1.2.2", + "id": "228" + } + ] + }, + { + "id": "04", + "state": "artifact-built", + "project": "ssdb", + "platform": "linux", + "architecture": "aarch64", + "optimization": "O0", + "source_repository": "ideawu/ssdb", + "source_revision": "f229ba277c7f7d0ca5a441c0c6fb3d1209af68e4", + "binary": "linux/arm64/O0/ssdb/ssdb-server", + "binary_sha256": "2658b5c309107d19721904e305b63c3ae1d0a614e02ae36880ccbf9a882a36cb", + "effective_flags": "-O0 -g -fno-builtin -save-temps=obj -std=c++11 -DNDEBUG -D__STDC_FORMAT_MACROS -DNEW_MAC -Wall -Wno-sign-compare -I/work/builds/linux/ssdb/O0/source/deps/leveldb-1.20/include -I/work/builds/linux/ssdb/O0/source/deps/jemalloc-4.1.0/include", + "compiler": "g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0", + "scoring_ready": false, + "remaining_gates": [ + "full C++ type scoring policy" + ], + "stripped": "linux/arm64/O0/ssdb/ssdb-server.stripped", + "stripped_sha256": "387640c8d0fed448f69852eae43bb4d4d430269935cc3834d9172085f8eb0bbb", + "code_sections_equal": true, + "debug_removed": true, + "symtab_removed": true, + "source_owned_function_records": 3087, + "source_owned_cus": 32, + "debug_producers": [ + "GNU C++11 13.3.0 -mlittle-endian -mabi=lp64 -g -O0 -std=c++11 -fno-builtin -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection", + "GNU C++11 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -std=c++11 -std=c++11 -fno-builtin -fno-builtin-memcmp -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection", + "GNU C++17 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection", + "GNU C99 13.3.0 -mlittle-endian -mabi=lp64 -g3 -O3 -std=gnu99 -fvisibility=hidden -funroll-loops -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection" + ], + "input": "inputs/linux/arm64/O0/ssdb/ssdb-server", + "original_source_sha256": "66c4c1572b2c932bd68fbe33643d124cc2df984f3a0c5d0d9b7cada9abfb711e", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 2024, + "rejected_unmapped_range_records": 1, + "available_unique_addresses": 2024, + "rows": [ + { + "address": 263848, + "name": "operator!= >", + "cell_id": "04", + "platform": "linux", + "project": "ssdb", + "optimization": "O0", + "binary": "linux/arm64/O0/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O0/ssdb/ssdb-server", + "id": "004" + }, + { + "address": 356140, + "name": "move_iterator", + "cell_id": "04", + "platform": "linux", + "project": "ssdb", + "optimization": "O0", + "binary": "linux/arm64/O0/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O0/ssdb/ssdb-server", + "id": "049" + }, + { + "address": 169424, + "name": "proc", + "cell_id": "04", + "platform": "linux", + "project": "ssdb", + "optimization": "O0", + "binary": "linux/arm64/O0/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O0/ssdb/ssdb-server", + "id": "094" + }, + { + "address": 414736, + "name": "~_Vector_impl", + "cell_id": "04", + "platform": "linux", + "project": "ssdb", + "optimization": "O0", + "binary": "linux/arm64/O0/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O0/ssdb/ssdb-server", + "id": "139" + }, + { + "address": 243992, + "name": "base", + "cell_id": "04", + "platform": "linux", + "project": "ssdb", + "optimization": "O0", + "binary": "linux/arm64/O0/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O0/ssdb/ssdb-server", + "id": "184" + }, + { + "address": 246436, + "name": "~SSDBImpl", + "cell_id": "04", + "platform": "linux", + "project": "ssdb", + "optimization": "O0", + "binary": "linux/arm64/O0/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O0/ssdb/ssdb-server", + "id": "229" + } + ] + }, + { + "id": "05", + "state": "artifact-built", + "project": "the-powder-toy", + "platform": "linux", + "architecture": "aarch64", + "optimization": "O0", + "source_repository": "The-Powder-Toy/The-Powder-Toy", + "source_revision": "c205becb1cf7ef1915293e2c99b2c7901d2887cd", + "binary": "linux/arm64/O0/the-powder-toy/powder", + "binary_sha256": "292acbd28b6f5865c3e2a597af3c1c1364c704f7a05432b87a7e644ab972563c", + "effective_flags": "-O0 -g -fno-builtin -save-temps=obj", + "compiler": "g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0", + "upstream_source_modified": false, + "scoring_ready": false, + "remaining_gates": [ + "full C++ type scoring policy" + ], + "stripped": "linux/arm64/O0/the-powder-toy/powder.stripped", + "stripped_sha256": "2fb8edde86aff3195ea8eadee3b56569ab46e15604c29c09cf6dabf68236fbbc", + "code_sections_equal": true, + "debug_removed": true, + "symtab_removed": true, + "source_owned_function_records": 91263, + "source_owned_cus": 401, + "debug_producers": [ + "GNU C++20 13.3.0 -mlittle-endian -mabi=lp64 -g -O0 -std=c++20 -fno-rtti -fno-builtin -ftree-vectorize -funsafe-math-optimizations -ffast-math -fomit-frame-pointer -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection" + ], + "input": "inputs/linux/arm64/O0/the-powder-toy/powder", + "original_source_sha256": "2fb378b939b18428b80d6e7d25b68c7ecdf827ece517c955b51044b93febe00e", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 27245, + "rejected_unmapped_range_records": 20, + "available_unique_addresses": 27245, + "rows": [ + { + "address": 365308, + "name": "__uninit_copy<__gnu_cxx::__normal_iterator >, sign*>", + "cell_id": "05", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O0", + "binary": "linux/arm64/O0/the-powder-toy/powder", + "input": "inputs/linux/arm64/O0/the-powder-toy/powder", + "id": "005" + }, + { + "address": 3357028, + "name": "operator--", + "cell_id": "05", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O0", + "binary": "linux/arm64/O0/the-powder-toy/powder", + "input": "inputs/linux/arm64/O0/the-powder-toy/powder", + "id": "050" + }, + { + "address": 2856520, + "name": "AddOption", + "cell_id": "05", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O0", + "binary": "linux/arm64/O0/the-powder-toy/powder", + "input": "inputs/linux/arm64/O0/the-powder-toy/powder", + "id": "095" + }, + { + "address": 1541412, + "name": "_M_erase_at_end", + "cell_id": "05", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O0", + "binary": "linux/arm64/O0/the-powder-toy/powder", + "input": "inputs/linux/arm64/O0/the-powder-toy/powder", + "id": "140" + }, + { + "address": 2078336, + "name": "_M_create&>", + "cell_id": "05", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O0", + "binary": "linux/arm64/O0/the-powder-toy/powder", + "input": "inputs/linux/arm64/O0/the-powder-toy/powder", + "id": "185" + }, + { + "address": 1848584, + "name": "reset", + "cell_id": "05", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O0", + "binary": "linux/arm64/O0/the-powder-toy/powder", + "input": "inputs/linux/arm64/O0/the-powder-toy/powder", + "id": "230" + } + ] + }, + { + "id": "06", + "schema": "decbench-cpp-qualification-cell-v2", + "state": "imported-payload-verified", + "platform": "linux", + "architecture": "aarch64", + "project": "OpenCC", + "optimization": "O2", + "source_repository": "BYVoid/OpenCC", + "source_revision": "c363a7ba51d487950982bd8a589211ffbfd95ba1", + "binary": "linux/arm64/O2/OpenCC/opencc", + "binary_sha256": "ecc3983dbd6f27432fce924ce9b6bf78e3d94b13087253b32fe74125ee85eed1", + "stripped": "linux/arm64/O2/OpenCC/opencc.stripped", + "stripped_sha256": "b479bb9d7fe117fa1bf498091f57b4596f0b236a0e7ee9048ee47471774346fc", + "code_sections_equal": true, + "debug_removed": true, + "symtab_removed": true, + "source_preprocessed_sha256": "a3de37200428ffddb27129f3b2252c2dffe4b4a7570074b25bbd839ce6df3a1b", + "raw_corpus_revision": "f1b7e849641baf1d35e293f400bc36aa7a1a4e2c", + "scoring_ready": false, + "remaining_gates": [ + "original source archive", + "effective per-TU compile/link command provenance" + ], + "source_owned_function_records": 217, + "source_owned_cus": 3, + "debug_producers": [ + "GNU C++17 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -std=gnu++17 -fno-builtin -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection" + ], + "input": "inputs/linux/arm64/O2/OpenCC/opencc", + "original_source_sha256": "3864f7b1c414f36a9d0aa547470806a4e07da8645697a3f6e1eeae3a83cad1a4", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 216, + "rejected_unmapped_range_records": 0, + "available_unique_addresses": 216, + "rows": [ + { + "address": 103824, + "name": "_extractValue", + "cell_id": "06", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2", + "binary": "linux/arm64/O2/OpenCC/opencc", + "input": "inputs/linux/arm64/O2/OpenCC/opencc", + "id": "006" + }, + { + "address": 43024, + "name": "StartObject", + "cell_id": "06", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2", + "binary": "linux/arm64/O2/OpenCC/opencc", + "input": "inputs/linux/arm64/O2/OpenCC/opencc", + "id": "051" + }, + { + "address": 72944, + "name": "argId", + "cell_id": "06", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2", + "binary": "linux/arm64/O2/OpenCC/opencc", + "input": "inputs/linux/arm64/O2/OpenCC/opencc", + "id": "096" + }, + { + "address": 27856, + "name": "basic_string<>", + "cell_id": "06", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2", + "binary": "linux/arm64/O2/OpenCC/opencc", + "input": "inputs/linux/arm64/O2/OpenCC/opencc", + "id": "141" + }, + { + "address": 82080, + "name": "longID", + "cell_id": "06", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2", + "binary": "linux/arm64/O2/OpenCC/opencc", + "input": "inputs/linux/arm64/O2/OpenCC/opencc", + "id": "186" + }, + { + "address": 99248, + "name": "WriteInspectionResultJson > > >", + "cell_id": "06", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2", + "binary": "linux/arm64/O2/OpenCC/opencc", + "input": "inputs/linux/arm64/O2/OpenCC/opencc", + "id": "231" + } + ] + }, + { + "id": "07", + "schema": "decbench-cpp-qualification-cell-v2", + "state": "imported-payload-verified", + "platform": "linux", + "architecture": "aarch64", + "project": "leveldb", + "optimization": "O2", + "source_repository": "google/leveldb", + "source_revision": "7ee830d02b623e8ffe0b95d59a74db1e58da04c5", + "binary": "linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "binary_sha256": "266c1bd53cd5a19666169883d18f530eb8d2a0f01e8416299cee7005923b9b61", + "stripped": "linux/arm64/O2/leveldb/libleveldb.so.1.23.0.stripped", + "stripped_sha256": "85211404edaf421a7e3a4fda0e0828bd159f12773aa066a90943e7f99818ae05", + "code_sections_equal": true, + "debug_removed": true, + "symtab_removed": true, + "source_preprocessed_sha256": "ecfe76028afbea4283b2de2f582ef903cd05538c45614de5dfd8f29d4f26447c", + "raw_corpus_revision": "f1b7e849641baf1d35e293f400bc36aa7a1a4e2c", + "scoring_ready": false, + "remaining_gates": [ + "original source archive", + "effective per-TU compile/link command provenance" + ], + "source_owned_function_records": 757, + "source_owned_cus": 38, + "debug_producers": [ + "GNU C++17 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -std=c++17 -fno-builtin -fno-exceptions -fno-rtti -fPIC -fvisibility=hidden -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection" + ], + "input": "inputs/linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "original_source_sha256": "3cf3b99b84bf708dfb84a7c30aee1e04d25d4ad984f0f720d78b88a6d07e0f4f", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 727, + "rejected_unmapped_range_records": 0, + "available_unique_addresses": 727, + "rows": [ + { + "address": 206752, + "name": "Prev", + "cell_id": "07", + "platform": "linux", + "project": "leveldb", + "optimization": "O2", + "binary": "linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "id": "007" + }, + { + "address": 162464, + "name": "ForEachOverlapping", + "cell_id": "07", + "platform": "linux", + "project": "leveldb", + "optimization": "O2", + "binary": "linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "id": "052" + }, + { + "address": 145296, + "name": "Finalize", + "cell_id": "07", + "platform": "linux", + "project": "leveldb", + "optimization": "O2", + "binary": "linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "id": "097" + }, + { + "address": 140768, + "name": "_M_erase", + "cell_id": "07", + "platform": "linux", + "project": "leveldb", + "optimization": "O2", + "binary": "linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "id": "142" + }, + { + "address": 171952, + "name": "Clear", + "cell_id": "07", + "platform": "linux", + "project": "leveldb", + "optimization": "O2", + "binary": "linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "id": "187" + }, + { + "address": 116064, + "name": "NewIterator", + "cell_id": "07", + "platform": "linux", + "project": "leveldb", + "optimization": "O2", + "binary": "linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2/leveldb/libleveldb.so.1.23.0", + "id": "232" + } + ] + }, + { + "id": "08", + "schema": "decbench-cpp-qualification-cell-v2", + "state": "imported-payload-verified", + "platform": "linux", + "architecture": "aarch64", + "project": "mergerfs", + "optimization": "O2", + "source_repository": "trapexit/mergerfs", + "source_revision": "515b0549ef615418df41ed3ffd993d036bc52e1f", + "binary": "linux/arm64/O2/mergerfs/mergerfs", + "binary_sha256": "5b289de46cade809e9360994ab8844f44d4fbef16ed52911b9442af2cff5af13", + "stripped": "linux/arm64/O2/mergerfs/mergerfs.stripped", + "stripped_sha256": "3be17911d1cbf52a516b016dd5b2df1234c8f5f5ca68af933209f4620c5ecc5e", + "code_sections_equal": true, + "debug_removed": true, + "symtab_removed": true, + "source_preprocessed_sha256": "35e276076678c183ec81120ca73b29bc645862abf818a7a7fc5b6b09ec3c9f2d", + "raw_corpus_revision": "f1b7e849641baf1d35e293f400bc36aa7a1a4e2c", + "scoring_ready": false, + "remaining_gates": [ + "original source archive", + "effective per-TU compile/link command provenance" + ], + "source_owned_function_records": 2652, + "source_owned_cus": 168, + "debug_producers": [ + "GNU C++20 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -std=c++20 -fno-builtin -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection", + "GNU C++20 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -std=c++20 -std=c++20 -fno-builtin -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection" + ], + "input": "inputs/linux/arm64/O2/mergerfs/mergerfs", + "original_source_sha256": "50d31cd379b545084494377a0859890eb9a79fdb1be4bf2483d7d99a2d5a7c01", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 1942, + "rejected_unmapped_range_records": 126, + "available_unique_addresses": 1942, + "rows": [ + { + "address": 753616, + "name": "_M_erase", + "cell_id": "08", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2", + "binary": "linux/arm64/O2/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2/mergerfs/mergerfs", + "id": "008" + }, + { + "address": 521680, + "name": "_M_check_len", + "cell_id": "08", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2", + "binary": "linux/arm64/O2/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2/mergerfs/mergerfs", + "id": "053" + }, + { + "address": 281936, + "name": "set", + "cell_id": "08", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2", + "binary": "linux/arm64/O2/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2/mergerfs/mergerfs", + "id": "098" + }, + { + "address": 224384, + "name": "populate_c_argv", + "cell_id": "08", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2", + "binary": "linux/arm64/O2/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2/mergerfs/mergerfs", + "id": "143" + }, + { + "address": 146208, + "name": "_M_erase", + "cell_id": "08", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2", + "binary": "linux/arm64/O2/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2/mergerfs/mergerfs", + "id": "188" + }, + { + "address": 144064, + "name": "from_string", + "cell_id": "08", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2", + "binary": "linux/arm64/O2/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2/mergerfs/mergerfs", + "id": "233" + } + ] + }, + { + "id": "09", + "schema": "decbench-cpp-qualification-cell-v2", + "state": "imported-payload-verified", + "platform": "linux", + "architecture": "aarch64", + "project": "snappy", + "optimization": "O2", + "source_repository": "google/snappy", + "source_revision": "2a6c78c781cb76ad191795449cb2267f33138150", + "binary": "linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "binary_sha256": "a7d910ba9b2f894b26793c9844722bf705cf52850d80fbfb132fc82eb048a227", + "stripped": "linux/arm64/O2/snappy/libsnappy.so.1.2.2.stripped", + "stripped_sha256": "610d1dd5c0b42016acee9d487c74d4598c9a5a702946004c94ea84ab2cb94a7e", + "code_sections_equal": true, + "debug_removed": true, + "symtab_removed": true, + "source_preprocessed_sha256": "90749ef2905e02f475945930070dd5968fdeabac6325d7db72f96d90880f8841", + "raw_corpus_revision": "f1b7e849641baf1d35e293f400bc36aa7a1a4e2c", + "scoring_ready": false, + "remaining_gates": [ + "original source archive", + "effective per-TU compile/link command provenance" + ], + "source_owned_function_records": 97, + "source_owned_cus": 4, + "debug_producers": [ + "GNU C++11 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -std=c++11 -fno-builtin -fno-exceptions -fno-rtti -fPIC -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection" + ], + "input": "inputs/linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "original_source_sha256": "c3bddf6e337e8ca86620ac6ae668b4288f8352f2453dc3f7fa1500a7d56bcfb0", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 97, + "rejected_unmapped_range_records": 0, + "available_unique_addresses": 97, + "rows": [ + { + "address": 36992, + "name": "DecompressAllTags >", + "cell_id": "09", + "platform": "linux", + "project": "snappy", + "optimization": "O2", + "binary": "linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "id": "009" + }, + { + "address": 29568, + "name": "~SnappyIOVecReader", + "cell_id": "09", + "platform": "linux", + "project": "snappy", + "optimization": "O2", + "binary": "linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "id": "054" + }, + { + "address": 15072, + "name": "Append32", + "cell_id": "09", + "platform": "linux", + "project": "snappy", + "optimization": "O2", + "binary": "linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "id": "099" + }, + { + "address": 14480, + "name": "Skip", + "cell_id": "09", + "platform": "linux", + "project": "snappy", + "optimization": "O2", + "binary": "linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "id": "144" + }, + { + "address": 15028, + "name": "~Sink", + "cell_id": "09", + "platform": "linux", + "project": "snappy", + "optimization": "O2", + "binary": "linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "id": "189" + }, + { + "address": 25072, + "name": "operator=", + "cell_id": "09", + "platform": "linux", + "project": "snappy", + "optimization": "O2", + "binary": "linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2/snappy/libsnappy.so.1.2.2", + "id": "234" + } + ] + }, + { + "id": "10", + "state": "artifact-built", + "project": "ssdb", + "platform": "linux", + "architecture": "aarch64", + "optimization": "O2", + "source_repository": "ideawu/ssdb", + "source_revision": "f229ba277c7f7d0ca5a441c0c6fb3d1209af68e4", + "binary": "linux/arm64/O2/ssdb/ssdb-server", + "binary_sha256": "5547f80555c5da739dc91932474b7cc9337bba11391c8b08337360fe363a4ac3", + "effective_flags": "-O2 -g -fno-builtin -save-temps=obj -std=c++11 -DNDEBUG -D__STDC_FORMAT_MACROS -DNEW_MAC -Wall -Wno-sign-compare -I/work/builds/linux/ssdb/O2/source/deps/leveldb-1.20/include -I/work/builds/linux/ssdb/O2/source/deps/jemalloc-4.1.0/include", + "compiler": "g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0", + "scoring_ready": false, + "remaining_gates": [ + "full C++ type scoring policy" + ], + "stripped": "linux/arm64/O2/ssdb/ssdb-server.stripped", + "stripped_sha256": "c72e6ca74deb4905b2c0615ea8be79f538d825f603279ea0c338f63fd0ae7ed5", + "code_sections_equal": true, + "debug_removed": true, + "symtab_removed": true, + "source_owned_function_records": 647, + "source_owned_cus": 32, + "debug_producers": [ + "GNU C++11 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -O2 -std=c++11 -std=c++11 -fno-builtin -fno-builtin-memcmp -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection", + "GNU C++11 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -std=c++11 -fno-builtin -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection", + "GNU C++17 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection", + "GNU C99 13.3.0 -mlittle-endian -mabi=lp64 -g3 -O3 -std=gnu99 -fvisibility=hidden -funroll-loops -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection" + ], + "input": "inputs/linux/arm64/O2/ssdb/ssdb-server", + "original_source_sha256": "66c4c1572b2c932bd68fbe33643d124cc2df984f3a0c5d0d9b7cada9abfb711e", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 626, + "rejected_unmapped_range_records": 0, + "available_unique_addresses": 626, + "rows": [ + { + "address": 786288, + "name": "qfront", + "cell_id": "10", + "platform": "linux", + "project": "ssdb", + "optimization": "O2", + "binary": "linux/arm64/O2/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2/ssdb/ssdb-server", + "id": "010" + }, + { + "address": 223908, + "name": "zset_one", + "cell_id": "10", + "platform": "linux", + "project": "ssdb", + "optimization": "O2", + "binary": "linux/arm64/O2/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2/ssdb/ssdb-server", + "id": "055" + }, + { + "address": 806980, + "name": "qslice", + "cell_id": "10", + "platform": "linux", + "project": "ssdb", + "optimization": "O2", + "binary": "linux/arm64/O2/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2/ssdb/ssdb-server", + "id": "100" + }, + { + "address": 275520, + "name": "output_name", + "cell_id": "10", + "platform": "linux", + "project": "ssdb", + "optimization": "O2", + "binary": "linux/arm64/O2/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2/ssdb/ssdb-server", + "id": "145" + }, + { + "address": 205488, + "name": "scan", + "cell_id": "10", + "platform": "linux", + "project": "ssdb", + "optimization": "O2", + "binary": "linux/arm64/O2/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2/ssdb/ssdb-server", + "id": "190" + }, + { + "address": 163600, + "name": "_run_thread", + "cell_id": "10", + "platform": "linux", + "project": "ssdb", + "optimization": "O2", + "binary": "linux/arm64/O2/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2/ssdb/ssdb-server", + "id": "235" + } + ] + }, + { + "id": "11", + "state": "artifact-built", + "project": "the-powder-toy", + "platform": "linux", + "architecture": "aarch64", + "optimization": "O2", + "source_repository": "The-Powder-Toy/The-Powder-Toy", + "source_revision": "c205becb1cf7ef1915293e2c99b2c7901d2887cd", + "binary": "linux/arm64/O2/the-powder-toy/powder", + "binary_sha256": "eca2a3d2f3e3272523d8c2e6c51e9f166adfcd6992126a8e1be6296074c1f1f8", + "effective_flags": "-O2 -g -fno-builtin -save-temps=obj", + "compiler": "g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0", + "upstream_source_modified": false, + "scoring_ready": false, + "remaining_gates": [ + "full C++ type scoring policy" + ], + "stripped": "linux/arm64/O2/the-powder-toy/powder.stripped", + "stripped_sha256": "a0127641604507b93359d089d0a12ba9b557d65500349fd45e721bbd8c97af96", + "code_sections_equal": true, + "debug_removed": true, + "symtab_removed": true, + "source_owned_function_records": 6472, + "source_owned_cus": 401, + "debug_producers": [ + "GNU C++20 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -std=c++20 -fno-rtti -fno-builtin -ftree-vectorize -funsafe-math-optimizations -ffast-math -fomit-frame-pointer -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection" + ], + "input": "inputs/linux/arm64/O2/the-powder-toy/powder", + "original_source_sha256": "2fb378b939b18428b80d6e7d25b68c7ecdf827ece517c955b51044b93febe00e", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 4564, + "rejected_unmapped_range_records": 133, + "available_unique_addresses": 4564, + "rows": [ + { + "address": 1866144, + "name": "DrawRect", + "cell_id": "11", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2", + "binary": "linux/arm64/O2/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2/the-powder-toy/powder", + "id": "011" + }, + { + "address": 935632, + "name": "NotifyMenuListChanged", + "cell_id": "11", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2", + "binary": "linux/arm64/O2/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2/the-powder-toy/powder", + "id": "056" + }, + { + "address": 381936, + "name": "~ParseException", + "cell_id": "11", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2", + "binary": "linux/arm64/O2/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2/the-powder-toy/powder", + "id": "101" + }, + { + "address": 1976576, + "name": "~ScrollPanel", + "cell_id": "11", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2", + "binary": "linux/arm64/O2/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2/the-powder-toy/powder", + "id": "146" + }, + { + "address": 1920768, + "name": "TextPosition", + "cell_id": "11", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2", + "binary": "linux/arm64/O2/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2/the-powder-toy/powder", + "id": "191" + }, + { + "address": 970464, + "name": "~NGravityOption", + "cell_id": "11", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2", + "binary": "linux/arm64/O2/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2/the-powder-toy/powder", + "id": "236" + } + ] + }, + { + "id": "12", + "schema": "decbench-cpp-qualification-cell-v2", + "state": "imported-payload-verified", + "platform": "linux", + "architecture": "aarch64", + "project": "OpenCC", + "optimization": "O2-noinline", + "source_repository": "BYVoid/OpenCC", + "source_revision": "c363a7ba51d487950982bd8a589211ffbfd95ba1", + "binary": "linux/arm64/O2-noinline/OpenCC/opencc", + "binary_sha256": "2bb7f300e3a0c980485e26e54f18cb5ca7cb707bebe3c6bd0b30586d12229843", + "stripped": "linux/arm64/O2-noinline/OpenCC/opencc.stripped", + "stripped_sha256": "a396371fb685ddbe812113ab92109204a20ef616fb494752ca364eca88071463", + "code_sections_equal": true, + "debug_removed": true, + "symtab_removed": true, + "source_preprocessed_sha256": "a3de37200428ffddb27129f3b2252c2dffe4b4a7570074b25bbd839ce6df3a1b", + "raw_corpus_revision": "f1b7e849641baf1d35e293f400bc36aa7a1a4e2c", + "scoring_ready": false, + "remaining_gates": [ + "original source archive", + "effective per-TU compile/link command provenance" + ], + "source_owned_function_records": 950, + "source_owned_cus": 3, + "debug_producers": [ + "GNU C++17 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -std=gnu++17 -fno-inline -fno-builtin -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection" + ], + "input": "inputs/linux/arm64/O2-noinline/OpenCC/opencc", + "original_source_sha256": "3864f7b1c414f36a9d0aa547470806a4e07da8645697a3f6e1eeae3a83cad1a4", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 897, + "rejected_unmapped_range_records": 0, + "available_unique_addresses": 897, + "rows": [ + { + "address": 58564, + "name": "get", + "cell_id": "12", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/linux/arm64/O2-noinline/OpenCC/opencc", + "id": "012" + }, + { + "address": 56544, + "name": "__niter_base*>", + "cell_id": "12", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/linux/arm64/O2-noinline/OpenCC/opencc", + "id": "057" + }, + { + "address": 129184, + "name": "__addressof", + "cell_id": "12", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/linux/arm64/O2-noinline/OpenCC/opencc", + "id": "102" + }, + { + "address": 113296, + "name": "operator!= >", + "cell_id": "12", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/linux/arm64/O2-noinline/OpenCC/opencc", + "id": "147" + }, + { + "address": 98016, + "name": "SpecificationException", + "cell_id": "12", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/linux/arm64/O2-noinline/OpenCC/opencc", + "id": "192" + }, + { + "address": 82944, + "name": "~Exception", + "cell_id": "12", + "platform": "linux", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/linux/arm64/O2-noinline/OpenCC/opencc", + "id": "237" + } + ] + }, + { + "id": "13", + "schema": "decbench-cpp-qualification-cell-v2", + "state": "imported-payload-verified", + "platform": "linux", + "architecture": "aarch64", + "project": "leveldb", + "optimization": "O2-noinline", + "source_repository": "google/leveldb", + "source_revision": "7ee830d02b623e8ffe0b95d59a74db1e58da04c5", + "binary": "linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "binary_sha256": "42b3e298f816ef92e7f2253147028cafbd0274071f0b9030f2bfc2d32e374309", + "stripped": "linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0.stripped", + "stripped_sha256": "e8ef8316bf5ae7554a977e56168a18f172385a6c1700980592259fd2082eeb83", + "code_sections_equal": true, + "debug_removed": true, + "symtab_removed": true, + "source_preprocessed_sha256": "ecfe76028afbea4283b2de2f582ef903cd05538c45614de5dfd8f29d4f26447c", + "raw_corpus_revision": "f1b7e849641baf1d35e293f400bc36aa7a1a4e2c", + "scoring_ready": false, + "remaining_gates": [ + "original source archive", + "effective per-TU compile/link command provenance" + ], + "source_owned_function_records": 3682, + "source_owned_cus": 38, + "debug_producers": [ + "GNU C++17 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -std=c++17 -fno-inline -fno-builtin -fno-exceptions -fno-rtti -fPIC -fvisibility=hidden -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection" + ], + "input": "inputs/linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "original_source_sha256": "3cf3b99b84bf708dfb84a7c30aee1e04d25d4ad984f0f720d78b88a6d07e0f4f", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 2594, + "rejected_unmapped_range_records": 28, + "available_unique_addresses": 2594, + "rows": [ + { + "address": 217428, + "name": "ok", + "cell_id": "13", + "platform": "linux", + "project": "leveldb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "id": "013" + }, + { + "address": 262464, + "name": "_Rb_tree", + "cell_id": "13", + "platform": "linux", + "project": "leveldb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "id": "058" + }, + { + "address": 130180, + "name": "leveldb_get", + "cell_id": "13", + "platform": "linux", + "project": "leveldb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "id": "103" + }, + { + "address": 170864, + "name": "move", + "cell_id": "13", + "platform": "linux", + "project": "leveldb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "id": "148" + }, + { + "address": 332032, + "name": "BackgroundThreadMain", + "cell_id": "13", + "platform": "linux", + "project": "leveldb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "id": "193" + }, + { + "address": 347348, + "name": "Size", + "cell_id": "13", + "platform": "linux", + "project": "leveldb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "input": "inputs/linux/arm64/O2-noinline/leveldb/libleveldb.so.1.23.0", + "id": "238" + } + ] + }, + { + "id": "14", + "schema": "decbench-cpp-qualification-cell-v2", + "state": "imported-payload-verified", + "platform": "linux", + "architecture": "aarch64", + "project": "mergerfs", + "optimization": "O2-noinline", + "source_repository": "trapexit/mergerfs", + "source_revision": "515b0549ef615418df41ed3ffd993d036bc52e1f", + "binary": "linux/arm64/O2-noinline/mergerfs/mergerfs", + "binary_sha256": "d4d20f6913a3706000918f1a48613a27737c0343d37d410aeb0bd650623e82d6", + "stripped": "linux/arm64/O2-noinline/mergerfs/mergerfs.stripped", + "stripped_sha256": "0633c718ea2d9f161b8453cdf2cb4c7784dadfe02822cb1da2550d851cefbabc", + "code_sections_equal": true, + "debug_removed": true, + "symtab_removed": true, + "source_preprocessed_sha256": "35e276076678c183ec81120ca73b29bc645862abf818a7a7fc5b6b09ec3c9f2d", + "raw_corpus_revision": "f1b7e849641baf1d35e293f400bc36aa7a1a4e2c", + "scoring_ready": false, + "remaining_gates": [ + "original source archive", + "effective per-TU compile/link command provenance" + ], + "source_owned_function_records": 30245, + "source_owned_cus": 168, + "debug_producers": [ + "GNU C++20 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -std=c++20 -fno-inline -fno-builtin -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection", + "GNU C++20 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -std=c++20 -std=c++20 -fno-inline -fno-builtin -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection" + ], + "input": "inputs/linux/arm64/O2-noinline/mergerfs/mergerfs", + "original_source_sha256": "50d31cd379b545084494377a0859890eb9a79fdb1be4bf2483d7d99a2d5a7c01", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 14332, + "rejected_unmapped_range_records": 539, + "available_unique_addresses": 14332, + "rows": [ + { + "address": 397636, + "name": "passthrough_open", + "cell_id": "14", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2-noinline/mergerfs/mergerfs", + "id": "014" + }, + { + "address": 439632, + "name": "__addressof", + "cell_id": "14", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2-noinline/mergerfs/mergerfs", + "id": "059" + }, + { + "address": 1216368, + "name": "resize", + "cell_id": "14", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2-noinline/mergerfs/mergerfs", + "id": "104" + }, + { + "address": 272176, + "name": "get<0, subprocess::preexec_func::HolderBase*, std::default_delete >", + "cell_id": "14", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2-noinline/mergerfs/mergerfs", + "id": "149" + }, + { + "address": 568112, + "name": "__adjust_heap<__gnu_cxx::__normal_iterator > >, long int, char, __gnu_cxx::__ops::_Iter_less_iter>", + "cell_id": "14", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2-noinline/mergerfs/mergerfs", + "id": "194" + }, + { + "address": 1105408, + "name": "signbit", + "cell_id": "14", + "platform": "linux", + "project": "mergerfs", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/mergerfs/mergerfs", + "input": "inputs/linux/arm64/O2-noinline/mergerfs/mergerfs", + "id": "239" + } + ] + }, + { + "id": "15", + "schema": "decbench-cpp-qualification-cell-v2", + "state": "imported-payload-verified", + "platform": "linux", + "architecture": "aarch64", + "project": "snappy", + "optimization": "O2-noinline", + "source_repository": "google/snappy", + "source_revision": "2a6c78c781cb76ad191795449cb2267f33138150", + "binary": "linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "binary_sha256": "ddc3f4ef64715b7cb7175274e90d2b3ee1478f91854561b933d77f0aabe1a7e8", + "stripped": "linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2.stripped", + "stripped_sha256": "27b3960b13b8ce4155e30de7d44906388d6c571476b56e4162bc9e4ad90daaae", + "code_sections_equal": true, + "debug_removed": true, + "symtab_removed": true, + "source_preprocessed_sha256": "90749ef2905e02f475945930070dd5968fdeabac6325d7db72f96d90880f8841", + "raw_corpus_revision": "f1b7e849641baf1d35e293f400bc36aa7a1a4e2c", + "scoring_ready": false, + "remaining_gates": [ + "original source archive", + "effective per-TU compile/link command provenance" + ], + "source_owned_function_records": 349, + "source_owned_cus": 4, + "debug_producers": [ + "GNU C++11 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -std=c++11 -fno-inline -fno-builtin -fno-exceptions -fno-rtti -fPIC -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection" + ], + "input": "inputs/linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "original_source_sha256": "c3bddf6e337e8ca86620ac6ae668b4288f8352f2453dc3f7fa1500a7d56bcfb0", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 348, + "rejected_unmapped_range_records": 0, + "available_unique_addresses": 348, + "rows": [ + { + "address": 60848, + "name": "SnappySinkAllocator", + "cell_id": "15", + "platform": "linux", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "id": "015" + }, + { + "address": 40112, + "name": "snappy_compress", + "cell_id": "15", + "platform": "linux", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "id": "060" + }, + { + "address": 41056, + "name": "~UncheckedByteArraySink", + "cell_id": "15", + "platform": "linux", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "id": "105" + }, + { + "address": 66048, + "name": "DecompressBranchless", + "cell_id": "15", + "platform": "linux", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "id": "150" + }, + { + "address": 47300, + "name": "Reset", + "cell_id": "15", + "platform": "linux", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "id": "195" + }, + { + "address": 56128, + "name": "~SnappyIOVecReader", + "cell_id": "15", + "platform": "linux", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "input": "inputs/linux/arm64/O2-noinline/snappy/libsnappy.so.1.2.2", + "id": "240" + } + ] + }, + { + "id": "16", + "state": "artifact-built", + "project": "ssdb", + "platform": "linux", + "architecture": "aarch64", + "optimization": "O2-noinline", + "source_repository": "ideawu/ssdb", + "source_revision": "f229ba277c7f7d0ca5a441c0c6fb3d1209af68e4", + "binary": "linux/arm64/O2-noinline/ssdb/ssdb-server", + "binary_sha256": "0d1aa8bf183bd7f36396c0b8f02cdb396cd71a0b25a782698c48f1177a16f695", + "effective_flags": "-O2 -fno-inline -g -fno-builtin -save-temps=obj -std=c++11 -DNDEBUG -D__STDC_FORMAT_MACROS -DNEW_MAC -Wall -Wno-sign-compare -I/work/builds/linux/ssdb/O2-noinline/source/deps/leveldb-1.20/include -I/work/builds/linux/ssdb/O2-noinline/source/deps/jemalloc-4.1.0/include", + "compiler": "g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0", + "scoring_ready": false, + "remaining_gates": [ + "full C++ type scoring policy" + ], + "stripped": "linux/arm64/O2-noinline/ssdb/ssdb-server.stripped", + "stripped_sha256": "66134e36f007c0f4785ba5eeb996d18cd514d15df5f8be673ea2d16041e38f78", + "code_sections_equal": true, + "debug_removed": true, + "symtab_removed": true, + "source_owned_function_records": 3150, + "source_owned_cus": 32, + "debug_producers": [ + "GNU C++11 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -O2 -std=c++11 -std=c++11 -fno-inline -fno-builtin -fno-builtin-memcmp -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection", + "GNU C++11 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -std=c++11 -fno-inline -fno-builtin -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection", + "GNU C++17 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection", + "GNU C99 13.3.0 -mlittle-endian -mabi=lp64 -g3 -O3 -std=gnu99 -fvisibility=hidden -funroll-loops -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection" + ], + "input": "inputs/linux/arm64/O2-noinline/ssdb/ssdb-server", + "original_source_sha256": "66c4c1572b2c932bd68fbe33643d124cc2df984f3a0c5d0d9b7cada9abfb711e", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 2340, + "rejected_unmapped_range_records": 34, + "available_unique_addresses": 2340, + "rows": [ + { + "address": 356304, + "name": "~_Vector_base", + "cell_id": "16", + "platform": "linux", + "project": "ssdb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2-noinline/ssdb/ssdb-server", + "id": "016" + }, + { + "address": 276864, + "name": "_S_key", + "cell_id": "16", + "platform": "linux", + "project": "ssdb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2-noinline/ssdb/ssdb-server", + "id": "061" + }, + { + "address": 787764, + "name": "_M_construct", + "cell_id": "16", + "platform": "linux", + "project": "ssdb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2-noinline/ssdb/ssdb-server", + "id": "106" + }, + { + "address": 275312, + "name": "operator!=", + "cell_id": "16", + "platform": "linux", + "project": "ssdb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2-noinline/ssdb/ssdb-server", + "id": "151" + }, + { + "address": 798180, + "name": "hincr", + "cell_id": "16", + "platform": "linux", + "project": "ssdb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2-noinline/ssdb/ssdb-server", + "id": "196" + }, + { + "address": 259856, + "name": "_M_valptr", + "cell_id": "16", + "platform": "linux", + "project": "ssdb", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/ssdb/ssdb-server", + "input": "inputs/linux/arm64/O2-noinline/ssdb/ssdb-server", + "id": "241" + } + ] + }, + { + "id": "17", + "state": "artifact-built", + "project": "the-powder-toy", + "platform": "linux", + "architecture": "aarch64", + "optimization": "O2-noinline", + "source_repository": "The-Powder-Toy/The-Powder-Toy", + "source_revision": "c205becb1cf7ef1915293e2c99b2c7901d2887cd", + "binary": "linux/arm64/O2-noinline/the-powder-toy/powder", + "binary_sha256": "5741913e2b4a419d2c0a0e6d7584630fb5ea1df9bf1210c5f1f4b2e30ac7e728", + "effective_flags": "-O2 -fno-inline -g -fno-builtin -save-temps=obj", + "compiler": "g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0", + "upstream_source_modified": false, + "scoring_ready": false, + "remaining_gates": [ + "full C++ type scoring policy" + ], + "stripped": "linux/arm64/O2-noinline/the-powder-toy/powder.stripped", + "stripped_sha256": "316fb82a965c9d0538fd8e52256a970a6598541e8b5ce7235f655eca04da438d", + "code_sections_equal": true, + "debug_removed": true, + "symtab_removed": true, + "source_owned_function_records": 81517, + "source_owned_cus": 401, + "debug_producers": [ + "GNU C++20 13.3.0 -mlittle-endian -mabi=lp64 -g -O2 -std=c++20 -fno-rtti -fno-inline -fno-builtin -ftree-vectorize -funsafe-math-optimizations -ffast-math -fomit-frame-pointer -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection" + ], + "input": "inputs/linux/arm64/O2-noinline/the-powder-toy/powder", + "original_source_sha256": "2fb378b939b18428b80d6e7d25b68c7ecdf827ece517c955b51044b93febe00e", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 32011, + "rejected_unmapped_range_records": 4080, + "available_unique_addresses": 32011, + "rows": [ + { + "address": 337760, + "name": "vector", + "cell_id": "17", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2-noinline/the-powder-toy/powder", + "id": "017" + }, + { + "address": 1481648, + "name": "operator=", + "cell_id": "17", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2-noinline/the-powder-toy/powder", + "id": "062" + }, + { + "address": 2305584, + "name": "SimulationData", + "cell_id": "17", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2-noinline/the-powder-toy/powder", + "id": "107" + }, + { + "address": 1715348, + "name": "max_size", + "cell_id": "17", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2-noinline/the-powder-toy/powder", + "id": "152" + }, + { + "address": 1354788, + "name": "__invoke_impl&, bool>", + "cell_id": "17", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2-noinline/the-powder-toy/powder", + "id": "197" + }, + { + "address": 2772724, + "name": "assign", + "cell_id": "17", + "platform": "linux", + "project": "the-powder-toy", + "optimization": "O2-noinline", + "binary": "linux/arm64/O2-noinline/the-powder-toy/powder", + "input": "inputs/linux/arm64/O2-noinline/the-powder-toy/powder", + "id": "242" + } + ] + }, + { + "id": "18", + "state": "imported-pe-pdb-verified", + "project": "OpenCC", + "platform": "windows", + "architecture": "x86_64", + "optimization": "O0", + "source_repository": "BYVoid/OpenCC", + "source_revision": "c363a7ba51d487950982bd8a589211ffbfd95ba1", + "binary": "windows/x86_64/O0/OpenCC/opencc.exe", + "binary_sha256": "fdc60086bf692eef6ea15bf4c27b4806215a12e312a865f5f64d37dae7871127", + "stripped": "windows/x86_64/O0/OpenCC/opencc.exe.stripped", + "stripped_sha256": "1dc4891d2cf7eeeebf96ef8bb15dcd60f13220f5e97a0adc38f6bf787952dd70", + "pdb_sha256": "a634aa9895ea4b9847b1a42487f3ef328d97b66fc5fe72cd2d00b82b278ed8c8", + "pdb_guid": "0C9DED6C-BAE6-4B07-B994-51DE2A19515C", + "pdb_age": 1, + "code_sections_equal": true, + "debug_directory_removed": true, + "project_owned_function_count": 6237, + "project_ownership_method": "pdb-module-file-provenance", + "ownership_scope": "procedures in source-matched compilands; header-origin and dependency ownership need per-procedure verification", + "evidence_github_run": 38326743980, + "preprocessed_tu_count": 42, + "scoring_ready": false, + "remaining_gates": [ + "full source-range identity correspondence", + "MSVC source/ABI type evaluation adapter" + ], + "effective_optimization_profile_verified": true, + "build_environment": { + "runner": "win25-vs2026", + "image_version": "20260925.250.1" + }, + "preprocessed_archive": "windows/x86_64/O0/OpenCC/preprocessed.tar.gz", + "build_provenance": "windows/x86_64/O0/OpenCC/build-provenance.json", + "evidence_job_id": 115067056261, + "evidence_recipe_commit": "0bc8b97f2846f01b2f976722d28bf28cfa13c20d", + "evidence_run_url": "https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/38326743980", + "input": "inputs/windows/x86_64/O0/OpenCC/opencc.exe", + "original_source_sha256": "3864f7b1c414f36a9d0aa547470806a4e07da8645697a3f6e1eeae3a83cad1a4", + "packaging_checks_passed": true, + "qualified": false, + "available_unique_addresses": 6237, + "rows": [ + { + "address": 5369530208, + "name": "std::vector >::operator[]", + "cell_id": "18", + "platform": "windows", + "project": "OpenCC", + "optimization": "O0", + "binary": "windows/x86_64/O0/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O0/OpenCC/opencc.exe", + "id": "018" + }, + { + "address": 5369331680, + "name": "std::num_put > >::_Ffmt", + "cell_id": "18", + "platform": "windows", + "project": "OpenCC", + "optimization": "O0", + "binary": "windows/x86_64/O0/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O0/OpenCC/opencc.exe", + "id": "063" + }, + { + "address": 5368779088, + "name": "std::_Unfancy", + "cell_id": "18", + "platform": "windows", + "project": "OpenCC", + "optimization": "O0", + "binary": "windows/x86_64/O0/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O0/OpenCC/opencc.exe", + "id": "108" + }, + { + "address": 5368934176, + "name": "std::allocator,std::allocator > const ,opencc::", + "cell_id": "18", + "platform": "windows", + "project": "OpenCC", + "optimization": "O0", + "binary": "windows/x86_64/O0/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O0/OpenCC/opencc.exe", + "id": "153" + }, + { + "address": 5369420688, + "name": "std::_Allocate_at_least_helper >", + "cell_id": "18", + "platform": "windows", + "project": "OpenCC", + "optimization": "O0", + "binary": "windows/x86_64/O0/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O0/OpenCC/opencc.exe", + "id": "198" + }, + { + "address": 5369175024, + "name": "rapidjson::GenericUri,rapidjson::MemoryPoolAllocator >,rapidjson::CrtAllocator>::GetStringLength", + "cell_id": "18", + "platform": "windows", + "project": "OpenCC", + "optimization": "O0", + "binary": "windows/x86_64/O0/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O0/OpenCC/opencc.exe", + "id": "243" + } + ] + }, + { + "id": "19", + "state": "imported-pe-pdb-verified", + "project": "detours", + "platform": "windows", + "architecture": "x86_64", + "optimization": "O0", + "source_repository": "microsoft/Detours", + "source_revision": "e4bfd6b03e50de46b47abfbd1e46b384f0c5f833", + "binary": "windows/x86_64/O0/detours/withdll.exe", + "binary_sha256": "ae650464c7e49aeee665d052cf0ce4bbbceb0a3425f011bd4909d602c46c68e3", + "stripped": "windows/x86_64/O0/detours/withdll.exe.stripped", + "stripped_sha256": "967a9b6af65106daf535a914de82719346ac0b6df651f72a705a6aa6c315f546", + "pdb_sha256": "eb7b83aceccb11bb11c86e8964b479d4bd1694b7a4dfd3eb2ba8966657948cda", + "pdb_guid": "AFA37531-D7FE-45A1-90E6-7BED98367B50", + "pdb_age": 1, + "code_sections_equal": true, + "debug_directory_removed": true, + "project_owned_function_count": 136, + "evidence_github_run": 38208011147, + "scoring_ready": false, + "remaining_gates": [ + "original source archive and preprocessed TU replay", + "full source-range identity correspondence", + "MSVC source/ABI type evaluation adapter" + ], + "input": "inputs/windows/x86_64/O0/detours/withdll.exe", + "original_source_sha256": "d7dc7da01ec5c2c7909036e24ac30ed4678c815f7e7ee97d6ad28df8f812cc97", + "packaging_checks_passed": true, + "qualified": false, + "evidence_recipe_commit": "38039cbf1aee3c6c9e516f09463493b4b67bdaad", + "evidence_run_url": "https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/38208011147", + "available_unique_addresses": 136, + "rows": [ + { + "address": 5368746256, + "name": "detour_free_trampoline", + "cell_id": "19", + "platform": "windows", + "project": "detours", + "optimization": "O0", + "binary": "windows/x86_64/O0/detours/withdll.exe", + "input": "inputs/windows/x86_64/O0/detours/withdll.exe", + "id": "019" + }, + { + "address": 5368726192, + "name": "DetourGetEntryPoint", + "cell_id": "19", + "platform": "windows", + "project": "detours", + "optimization": "O0", + "binary": "windows/x86_64/O0/detours/withdll.exe", + "input": "inputs/windows/x86_64/O0/detours/withdll.exe", + "id": "064" + }, + { + "address": 5368720736, + "name": "DetourEnumerateImportsThunk", + "cell_id": "19", + "platform": "windows", + "project": "detours", + "optimization": "O0", + "binary": "windows/x86_64/O0/detours/withdll.exe", + "input": "inputs/windows/x86_64/O0/detours/withdll.exe", + "id": "109" + }, + { + "address": 5368743200, + "name": "_vsnwprintf_l", + "cell_id": "19", + "platform": "windows", + "project": "detours", + "optimization": "O0", + "binary": "windows/x86_64/O0/detours/withdll.exe", + "input": "inputs/windows/x86_64/O0/detours/withdll.exe", + "id": "154" + }, + { + "address": 5368758768, + "name": "CDetourDis::SanityCheckSystem", + "cell_id": "19", + "platform": "windows", + "project": "detours", + "optimization": "O0", + "binary": "windows/x86_64/O0/detours/withdll.exe", + "input": "inputs/windows/x86_64/O0/detours/withdll.exe", + "id": "199" + }, + { + "address": 5368728496, + "name": "EnumerateModulesInProcess", + "cell_id": "19", + "platform": "windows", + "project": "detours", + "optimization": "O0", + "binary": "windows/x86_64/O0/detours/withdll.exe", + "input": "inputs/windows/x86_64/O0/detours/withdll.exe", + "id": "244" + } + ] + }, + { + "id": "20", + "state": "imported-pe-pdb-verified", + "project": "ninja", + "platform": "windows", + "architecture": "x86_64", + "optimization": "O0", + "source_repository": "ninja-build/ninja", + "source_revision": "e4a128055f7698522da6de2821ffe0761623f298", + "binary": "windows/x86_64/O0/ninja/ninja.exe", + "binary_sha256": "da86ce75947a39d852ba1e56d6b2e25048fac64964198047231904654fd3f9bd", + "stripped": "windows/x86_64/O0/ninja/ninja.exe.stripped", + "stripped_sha256": "01b31cf9bc601cd06236dec0bfeba1b93068c1663ab66b4789d8507ae25f4589", + "pdb_sha256": "945678453515fa7d2756478bc4062ea1fca25a254fad2f9a95ca758d9f794dff", + "pdb_guid": "8BF4B647-539C-4218-8263-761D554FCFCF", + "pdb_age": 1, + "code_sections_equal": true, + "debug_directory_removed": true, + "project_owned_function_count": 5424, + "project_ownership_method": "pdb-module-file-provenance", + "ownership_scope": "procedures in source-matched compilands; header-origin and dependency ownership need per-procedure verification", + "evidence_github_run": 38326743980, + "preprocessed_tu_count": 38, + "scoring_ready": false, + "remaining_gates": [ + "full source-range identity correspondence", + "MSVC source/ABI type evaluation adapter" + ], + "effective_optimization_profile_verified": true, + "build_environment": { + "runner": "win25-vs2026", + "image_version": "20260925.250.1" + }, + "preprocessed_archive": "windows/x86_64/O0/ninja/preprocessed.tar.gz", + "build_provenance": "windows/x86_64/O0/ninja/build-provenance.json", + "evidence_job_id": 115067056264, + "evidence_recipe_commit": "0bc8b97f2846f01b2f976722d28bf28cfa13c20d", + "evidence_run_url": "https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/38326743980", + "input": "inputs/windows/x86_64/O0/ninja/ninja.exe", + "original_source_sha256": "fcd9a184d69f2bd795ffcd8b76dbbea7f7f4d306230c27842289b237cb378023", + "packaging_checks_passed": true, + "qualified": false, + "available_unique_addresses": 5424, + "rows": [ + { + "address": 5368871632, + "name": "std::_Tree,std::allocator,0> >::end", + "cell_id": "20", + "platform": "windows", + "project": "ninja", + "optimization": "O0", + "binary": "windows/x86_64/O0/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O0/ninja/ninja.exe", + "id": "020" + }, + { + "address": 5368738640, + "name": "std::vector >::_Xlength", + "cell_id": "20", + "platform": "windows", + "project": "ninja", + "optimization": "O0", + "binary": "windows/x86_64/O0/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O0/ninja/ninja.exe", + "id": "065" + }, + { + "address": 5369346976, + "name": "std::_Pop_heap_unchecked,std::allocator > *,std::less >", + "cell_id": "20", + "platform": "windows", + "project": "ninja", + "optimization": "O0", + "binary": "windows/x86_64/O0/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O0/ninja/ninja.exe", + "id": "110" + }, + { + "address": 5369277184, + "name": "std::deque >::_Getal", + "cell_id": "20", + "platform": "windows", + "project": "ninja", + "optimization": "O0", + "binary": "windows/x86_64/O0/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O0/ninja/ninja.exe", + "id": "155" + }, + { + "address": 5369171312, + "name": "std::_Uninitialized_backout,std::equal_to,std::allocator > > > > > > *>::~_Uninitialized_backout,std::equal_to,std::allocator > > > > > > *>", + "cell_id": "20", + "platform": "windows", + "project": "ninja", + "optimization": "O0", + "binary": "windows/x86_64/O0/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O0/ninja/ninja.exe", + "id": "200" + }, + { + "address": 5369228496, + "name": "emhash8::HashMap,std::equal_to,std::allocator >,emhash8::DefaultPolicy>::rebuild", + "cell_id": "20", + "platform": "windows", + "project": "ninja", + "optimization": "O0", + "binary": "windows/x86_64/O0/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O0/ninja/ninja.exe", + "id": "245" + } + ] + }, + { + "id": "21", + "state": "imported-pe-pdb-verified", + "project": "snappy", + "platform": "windows", + "architecture": "x86_64", + "optimization": "O0", + "source_repository": "google/snappy", + "source_revision": "2a6c78c781cb76ad191795449cb2267f33138150", + "binary": "windows/x86_64/O0/snappy/snappy.dll", + "binary_sha256": "e740c1a35bff33dbc2789f5b2420b48a50f608ba63bdbd1338988b6961c344e2", + "stripped": "windows/x86_64/O0/snappy/snappy.dll.stripped", + "stripped_sha256": "696f0c504463a557c6ccf56624596745f29bc7e62a7717befc93defe0d613491", + "pdb_sha256": "7d8a38b3f684df51a4155ab09bd4343cc24c79ebb7bd94f24df43ac935c18c9b", + "pdb_guid": "E830C3EC-4C97-45E9-B1E2-16EBB48AAB7B", + "pdb_age": 1, + "code_sections_equal": true, + "debug_directory_removed": true, + "project_owned_function_count": 408, + "project_ownership_method": "pdb-module-file-provenance", + "ownership_scope": "procedures in source-matched compilands; header-origin and dependency ownership need per-procedure verification", + "evidence_github_run": 38326743980, + "preprocessed_tu_count": 4, + "scoring_ready": false, + "remaining_gates": [ + "full source-range identity correspondence", + "MSVC source/ABI type evaluation adapter" + ], + "effective_optimization_profile_verified": true, + "build_environment": { + "image_version": "20260925.250.1", + "runner": "win25-vs2026" + }, + "preprocessed_archive": "windows/x86_64/O0/snappy/preprocessed.tar.gz", + "build_provenance": "windows/x86_64/O0/snappy/build-provenance.json", + "evidence_job_id": 115067056210, + "evidence_recipe_commit": "0bc8b97f2846f01b2f976722d28bf28cfa13c20d", + "evidence_run_url": "https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/38326743980", + "input": "inputs/windows/x86_64/O0/snappy/snappy.dll", + "original_source_sha256": "c3bddf6e337e8ca86620ac6ae668b4288f8352f2453dc3f7fa1500a7d56bcfb0", + "packaging_checks_passed": true, + "qualified": false, + "available_unique_addresses": 408, + "rows": [ + { + "address": 6442458512, + "name": "stdext::exception::exception", + "cell_id": "21", + "platform": "windows", + "project": "snappy", + "optimization": "O0", + "binary": "windows/x86_64/O0/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O0/snappy/snappy.dll", + "id": "021" + }, + { + "address": 6442494016, + "name": "std::forward<__m128i &>", + "cell_id": "21", + "platform": "windows", + "project": "snappy", + "optimization": "O0", + "binary": "windows/x86_64/O0/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O0/snappy/snappy.dll", + "id": "066" + }, + { + "address": 6442494448, + "name": "std::_String_const_iterator > >::_String_const_iterator > >", + "cell_id": "21", + "platform": "windows", + "project": "snappy", + "optimization": "O0", + "binary": "windows/x86_64/O0/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O0/snappy/snappy.dll", + "id": "111" + }, + { + "address": 6442460656, + "name": "std::basic_string,std::allocator >::max_size", + "cell_id": "21", + "platform": "windows", + "project": "snappy", + "optimization": "O0", + "binary": "windows/x86_64/O0/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O0/snappy/snappy.dll", + "id": "156" + }, + { + "address": 6442500448, + "name": "snappy::SnappyDecompressionValidator::CheckLength", + "cell_id": "21", + "platform": "windows", + "project": "snappy", + "optimization": "O0", + "binary": "windows/x86_64/O0/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O0/snappy/snappy.dll", + "id": "201" + }, + { + "address": 6442500704, + "name": "snappy::ExtractOffset", + "cell_id": "21", + "platform": "windows", + "project": "snappy", + "optimization": "O0", + "binary": "windows/x86_64/O0/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O0/snappy/snappy.dll", + "id": "246" + } + ] + }, + { + "id": "22", + "state": "imported-pe-pdb-verified", + "project": "tracy", + "platform": "windows", + "architecture": "x86_64", + "optimization": "O0", + "source_repository": "wolfpld/tracy", + "source_revision": "dd29819f3d3e668983d974507038c08dba06479c", + "binary": "windows/x86_64/O0/tracy/tracy-profiler.exe", + "binary_sha256": "e596e99646f950895968f5574903123c1c489be78af2e1f881354841aa63123c", + "stripped": "windows/x86_64/O0/tracy/tracy-profiler.exe.stripped", + "stripped_sha256": "8c089578f78011abef068dd5954aac2af6daf74231382e3dec72bf7ec796a94e", + "pdb_sha256": "4675e59ee4b053b9a9ee75c6210c383aec0b72b60dbe047efcceb638c75f7da6", + "pdb_guid": "27E05C15-A2BD-4C49-93FA-4DBDF2A45231", + "pdb_age": 1, + "code_sections_equal": true, + "debug_directory_removed": true, + "project_owned_function_count": 28431, + "project_ownership_method": "pdb-module-file-provenance", + "ownership_scope": "procedures in source-matched compilands; header-origin and dependency ownership need per-procedure verification", + "evidence_github_run": 38326925658, + "preprocessed_tu_count": 131, + "scoring_ready": false, + "remaining_gates": [ + "full source-range identity correspondence", + "MSVC source/ABI type evaluation adapter", + "fetched dependency source revisions, licenses and original archives" + ], + "effective_optimization_profile_verified": true, + "external_dependency_preprocessed_tus": 10, + "build_environment": { + "image_version": "20260925.250.1", + "runner": "win25-vs2026" + }, + "preprocessed_archive": "windows/x86_64/O0/tracy/preprocessed.tar.gz", + "build_provenance": "windows/x86_64/O0/tracy/build-provenance.json", + "evidence_job_id": 115067644305, + "evidence_recipe_commit": "be3a21392523d756ca3a5cba3ffd4eecbe1951d4", + "evidence_run_url": "https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/38326925658", + "input": "inputs/windows/x86_64/O0/tracy/tracy-profiler.exe", + "original_source_sha256": "ea59088fbeb24edc4503085417a607dbea43cf20ff2c07e2ff78106590d84bd6", + "packaging_checks_passed": true, + "qualified": false, + "available_unique_addresses": 28431, + "rows": [ + { + "address": 5371849952, + "name": "tracy::pdqsort_detail::sort3,std::equal_to >::Iter<0> *,", + "cell_id": "22", + "platform": "windows", + "project": "tracy", + "optimization": "O0", + "binary": "windows/x86_64/O0/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O0/tracy/tracy-profiler.exe", + "id": "022" + }, + { + "address": 5369490400, + "name": "std::addressof >", + "cell_id": "22", + "platform": "windows", + "project": "tracy", + "optimization": "O0", + "binary": "windows/x86_64/O0/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O0/tracy/tracy-profiler.exe", + "id": "067" + }, + { + "address": 5369056096, + "name": "std::vector,std::allocator > >,std::allocator,std::allocator > > > >::_Emplace_one_at_back,std::allocator > > >", + "cell_id": "22", + "platform": "windows", + "project": "tracy", + "optimization": "O0", + "binary": "windows/x86_64/O0/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O0/tracy/tracy-profiler.exe", + "id": "112" + }, + { + "address": 5370617184, + "name": "std::make_heap > >,", + "cell_id": "22", + "platform": "windows", + "project": "tracy", + "optimization": "O0", + "binary": "windows/x86_64/O0/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O0/tracy/tracy-profiler.exe", + "id": "157" + }, + { + "address": 5369167696, + "name": "std::_Uninitialized_backout_al,std::allocator >,bool,__int64,unsigned __int64,double,std::allocator,nlohmann::json_abi_v3_12_0::adl_serializer,std::vector >,void> > >::_Release", + "cell_id": "22", + "platform": "windows", + "project": "tracy", + "optimization": "O0", + "binary": "windows/x86_64/O0/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O0/tracy/tracy-profiler.exe", + "id": "202" + }, + { + "address": 5371386032, + "name": "tracy::detail::Table<1,80,unsigned int,tracy::View::MemPathData,tracy::hash,std::equal_to >::emplace", + "cell_id": "22", + "platform": "windows", + "project": "tracy", + "optimization": "O0", + "binary": "windows/x86_64/O0/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O0/tracy/tracy-profiler.exe", + "id": "247" + } + ] + }, + { + "id": "23", + "state": "imported-pe-pdb-verified", + "project": "winsparkle", + "platform": "windows", + "architecture": "x86_64", + "optimization": "O0", + "source_repository": "vslavik/winsparkle", + "source_revision": "a8986caf620262f7d4581b241436ceaa0cc9370f", + "binary": "windows/x86_64/O0/winsparkle/WinSparkle.dll", + "binary_sha256": "f58f76e881653da262428f7e776676ac04bd478b30ce8f553aadfec000d6dbca", + "stripped": "windows/x86_64/O0/winsparkle/WinSparkle.dll.stripped", + "stripped_sha256": "3102f0c03e4bbddbdf4c5e799870e6253078370f39eecde4432988067311d06c", + "pdb_sha256": "b8a6707a41a2f0ea72220dc3337a3c68bdce37477fad19e9bd1cd71ce798d1c3", + "pdb_guid": "788CD753-ED0D-4B19-AE26-01D34E79C99E", + "pdb_age": 1, + "code_sections_equal": true, + "debug_directory_removed": true, + "project_owned_function_count": 2267, + "evidence_github_run": 38208011147, + "scoring_ready": false, + "remaining_gates": [ + "original source archive and preprocessed TU replay", + "full source-range identity correspondence", + "MSVC source/ABI type evaluation adapter" + ], + "input": "inputs/windows/x86_64/O0/winsparkle/WinSparkle.dll", + "original_source_sha256": "e349da43394275abf8ba0c58323022354009e8d4fc9e70e8da3d00df965db122", + "packaging_checks_passed": true, + "qualified": false, + "evidence_recipe_commit": "38039cbf1aee3c6c9e516f09463493b4b67bdaad", + "evidence_run_url": "https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/38208011147", + "available_unique_addresses": 2267, + "rows": [ + { + "address": 6442532172, + "name": "std::num_get > >::_Parse_fp_with_locale<0>", + "cell_id": "23", + "platform": "windows", + "project": "winsparkle", + "optimization": "O0", + "binary": "windows/x86_64/O0/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O0/winsparkle/WinSparkle.dll", + "id": "023" + }, + { + "address": 6442657484, + "name": "wxTopLevelWindowBase::DoGetScreenPosition", + "cell_id": "23", + "platform": "windows", + "project": "winsparkle", + "optimization": "O0", + "binary": "windows/x86_64/O0/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O0/winsparkle/WinSparkle.dll", + "id": "068" + }, + { + "address": 6442582792, + "name": "std::basic_ios >::fill", + "cell_id": "23", + "platform": "windows", + "project": "winsparkle", + "optimization": "O0", + "binary": "windows/x86_64/O0/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O0/winsparkle/WinSparkle.dll", + "id": "113" + }, + { + "address": 6442571460, + "name": "std::error_condition::category", + "cell_id": "23", + "platform": "windows", + "project": "winsparkle", + "optimization": "O0", + "binary": "windows/x86_64/O0/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O0/winsparkle/WinSparkle.dll", + "id": "158" + }, + { + "address": 6442584892, + "name": "std::_Iostream_error_category2::name", + "cell_id": "23", + "platform": "windows", + "project": "winsparkle", + "optimization": "O0", + "binary": "windows/x86_64/O0/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O0/winsparkle/WinSparkle.dll", + "id": "203" + }, + { + "address": 6442616972, + "name": "wxUnusedVar", + "cell_id": "23", + "platform": "windows", + "project": "winsparkle", + "optimization": "O0", + "binary": "windows/x86_64/O0/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O0/winsparkle/WinSparkle.dll", + "id": "248" + } + ] + }, + { + "id": "24", + "state": "imported-pe-pdb-verified", + "project": "OpenCC", + "platform": "windows", + "architecture": "x86_64", + "optimization": "O2", + "source_repository": "BYVoid/OpenCC", + "source_revision": "c363a7ba51d487950982bd8a589211ffbfd95ba1", + "binary": "windows/x86_64/O2/OpenCC/opencc.exe", + "binary_sha256": "eeae1e36e841778c79f171096ba4bcb0ca90d857e7d4ebd75fea68b64801eb85", + "stripped": "windows/x86_64/O2/OpenCC/opencc.exe.stripped", + "stripped_sha256": "45a8cdd9f34f4ef3bbbcb05ec4e6165d7cefd9d1393918cf2bcc6ee24d8f9fcf", + "pdb_sha256": "9bab7db705d1a0a7a831ce73933dfebaf403a4a45af7a6970139d22134e24922", + "pdb_guid": "91FADFE0-2CC2-444A-A287-958EACF319E6", + "pdb_age": 1, + "code_sections_equal": true, + "debug_directory_removed": true, + "project_owned_function_count": 1363, + "project_ownership_method": "pdb-module-file-provenance", + "ownership_scope": "procedures in source-matched compilands; header-origin and dependency ownership need per-procedure verification", + "evidence_github_run": 38326743980, + "preprocessed_tu_count": 42, + "scoring_ready": false, + "remaining_gates": [ + "full source-range identity correspondence", + "MSVC source/ABI type evaluation adapter" + ], + "effective_optimization_profile_verified": true, + "build_environment": { + "image_version": "20260925.250.1", + "runner": "win25-vs2026" + }, + "preprocessed_archive": "windows/x86_64/O2/OpenCC/preprocessed.tar.gz", + "build_provenance": "windows/x86_64/O2/OpenCC/build-provenance.json", + "evidence_job_id": 115067056379, + "evidence_recipe_commit": "0bc8b97f2846f01b2f976722d28bf28cfa13c20d", + "evidence_run_url": "https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/38326743980", + "input": "inputs/windows/x86_64/O2/OpenCC/opencc.exe", + "original_source_sha256": "3864f7b1c414f36a9d0aa547470806a4e07da8645697a3f6e1eeae3a83cad1a4", + "packaging_checks_passed": true, + "qualified": false, + "available_unique_addresses": 1363, + "rows": [ + { + "address": 5368851280, + "name": "std::basic_string,std::allocator >::_Reallocate_grow_by<,unsigned __int64,char>", + "cell_id": "24", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2", + "binary": "windows/x86_64/O2/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2/OpenCC/opencc.exe", + "id": "024" + }, + { + "address": 5369069536, + "name": "opencc::Segments::AddSegment", + "cell_id": "24", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2", + "binary": "windows/x86_64/O2/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2/OpenCC/opencc.exe", + "id": "069" + }, + { + "address": 5368756304, + "name": "TCLAP::ValueArg,std::allocator > >::~ValueArg,std::allocator > >", + "cell_id": "24", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2", + "binary": "windows/x86_64/O2/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2/OpenCC/opencc.exe", + "id": "114" + }, + { + "address": 5368803552, + "name": "std::locale::facet::_Decref", + "cell_id": "24", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2", + "binary": "windows/x86_64/O2/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2/OpenCC/opencc.exe", + "id": "159" + }, + { + "address": 5369048736, + "name": "opencc::UTF8Util::IdeographicDescriptionOperatorArity", + "cell_id": "24", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2", + "binary": "windows/x86_64/O2/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2/OpenCC/opencc.exe", + "id": "204" + }, + { + "address": 5369106320, + "name": "std::_Temporary_owner,std::allocator >::assign", + "cell_id": "26", + "platform": "windows", + "project": "ninja", + "optimization": "O2", + "binary": "windows/x86_64/O2/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2/ninja/ninja.exe", + "id": "026" + }, + { + "address": 5368984880, + "name": "std::_Tree_val >::_Erase_tree > >", + "cell_id": "26", + "platform": "windows", + "project": "ninja", + "optimization": "O2", + "binary": "windows/x86_64/O2/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2/ninja/ninja.exe", + "id": "071" + }, + { + "address": 5368752336, + "name": "Builder::FinishCommand", + "cell_id": "26", + "platform": "windows", + "project": "ninja", + "optimization": "O2", + "binary": "windows/x86_64/O2/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2/ninja/ninja.exe", + "id": "116" + }, + { + "address": 5368907216, + "name": "ManifestParser::ParseRule", + "cell_id": "26", + "platform": "windows", + "project": "ninja", + "optimization": "O2", + "binary": "windows/x86_64/O2/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2/ninja/ninja.exe", + "id": "161" + }, + { + "address": 5368934368, + "name": "Pool::DelayEdge", + "cell_id": "26", + "platform": "windows", + "project": "ninja", + "optimization": "O2", + "binary": "windows/x86_64/O2/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2/ninja/ninja.exe", + "id": "206" + } + ] + }, + { + "id": "27", + "state": "imported-pe-pdb-verified", + "project": "snappy", + "platform": "windows", + "architecture": "x86_64", + "optimization": "O2", + "source_repository": "google/snappy", + "source_revision": "2a6c78c781cb76ad191795449cb2267f33138150", + "binary": "windows/x86_64/O2/snappy/snappy.dll", + "binary_sha256": "3b6369f2ba619f0fb402c97e1581dfc49173c59365b3e279e84df0d6973aae69", + "stripped": "windows/x86_64/O2/snappy/snappy.dll.stripped", + "stripped_sha256": "32d32be7699c41d660b34d3ca6260e36c47a382ef9880fd911332ebac3318ba5", + "pdb_sha256": "9adcce4ad43cb98c64f0f922e77c270c83b306819d958f3740fad952f4ca5939", + "pdb_guid": "4CC4D5AC-ED29-4F7C-B470-9140A772E30D", + "pdb_age": 1, + "code_sections_equal": true, + "debug_directory_removed": true, + "project_owned_function_count": 378, + "project_ownership_method": "pdb-module-file-provenance", + "ownership_scope": "procedures in source-matched compilands; header-origin and dependency ownership need per-procedure verification", + "evidence_github_run": 38326743980, + "preprocessed_tu_count": 4, + "scoring_ready": false, + "remaining_gates": [ + "full source-range identity correspondence", + "MSVC source/ABI type evaluation adapter" + ], + "effective_optimization_profile_verified": true, + "build_environment": { + "runner": "win25-vs2026", + "image_version": "20260925.250.1" + }, + "preprocessed_archive": "windows/x86_64/O2/snappy/preprocessed.tar.gz", + "build_provenance": "windows/x86_64/O2/snappy/build-provenance.json", + "evidence_job_id": 115067056340, + "evidence_recipe_commit": "0bc8b97f2846f01b2f976722d28bf28cfa13c20d", + "evidence_run_url": "https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/38326743980", + "input": "inputs/windows/x86_64/O2/snappy/snappy.dll", + "original_source_sha256": "c3bddf6e337e8ca86620ac6ae668b4288f8352f2453dc3f7fa1500a7d56bcfb0", + "packaging_checks_passed": true, + "qualified": false, + "available_unique_addresses": 378, + "rows": [ + { + "address": 6442456320, + "name": "std::basic_string,std::allocator >::_Append", + "cell_id": "27", + "platform": "windows", + "project": "snappy", + "optimization": "O2", + "binary": "windows/x86_64/O2/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2/snappy/snappy.dll", + "id": "027" + }, + { + "address": 6442471952, + "name": "snappy::ByteArraySource::", + "cell_id": "27", + "platform": "windows", + "project": "snappy", + "optimization": "O2", + "binary": "windows/x86_64/O2/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2/snappy/snappy.dll", + "id": "072" + }, + { + "address": 6442478192, + "name": "snappy::CompressFromIOVec", + "cell_id": "27", + "platform": "windows", + "project": "snappy", + "optimization": "O2", + "binary": "windows/x86_64/O2/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2/snappy/snappy.dll", + "id": "117" + }, + { + "address": 6442483808, + "name": "snappy::SnappyDecompressor::RefillTag", + "cell_id": "27", + "platform": "windows", + "project": "snappy", + "optimization": "O2", + "binary": "windows/x86_64/O2/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2/snappy/snappy.dll", + "id": "162" + }, + { + "address": 6442490224, + "name": "std::vector >::max_size", + "cell_id": "27", + "platform": "windows", + "project": "snappy", + "optimization": "O2", + "binary": "windows/x86_64/O2/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2/snappy/snappy.dll", + "id": "207" + } + ] + }, + { + "id": "28", + "state": "imported-pe-pdb-verified", + "project": "tracy", + "platform": "windows", + "architecture": "x86_64", + "optimization": "O2", + "source_repository": "wolfpld/tracy", + "source_revision": "dd29819f3d3e668983d974507038c08dba06479c", + "binary": "windows/x86_64/O2/tracy/tracy-profiler.exe", + "binary_sha256": "2efdabe472b09da9917649c9fd5b119b2a108a049eae38193d925bcca0273529", + "stripped": "windows/x86_64/O2/tracy/tracy-profiler.exe.stripped", + "stripped_sha256": "51d411d396c203cd9328b90034b32912252ce00e0b4b633052e7e21df00d0ec4", + "pdb_sha256": "b62da9087493057d739b516921a214578e7b73e17ca05683496d7a0a16164fb9", + "pdb_guid": "4E01ED48-D2B9-4E09-9563-45C8523F14EA", + "pdb_age": 1, + "code_sections_equal": true, + "debug_directory_removed": true, + "project_owned_function_count": 5132, + "project_ownership_method": "pdb-module-file-provenance", + "ownership_scope": "procedures in source-matched compilands; header-origin and dependency ownership need per-procedure verification", + "evidence_github_run": 38326925658, + "preprocessed_tu_count": 131, + "scoring_ready": false, + "remaining_gates": [ + "full source-range identity correspondence", + "MSVC source/ABI type evaluation adapter", + "fetched dependency source revisions, licenses and original archives" + ], + "effective_optimization_profile_verified": true, + "external_dependency_preprocessed_tus": 10, + "build_environment": { + "runner": "win25-vs2026", + "image_version": "20260925.250.1" + }, + "preprocessed_archive": "windows/x86_64/O2/tracy/preprocessed.tar.gz", + "build_provenance": "windows/x86_64/O2/tracy/build-provenance.json", + "evidence_job_id": 115067644520, + "evidence_recipe_commit": "be3a21392523d756ca3a5cba3ffd4eecbe1951d4", + "evidence_run_url": "https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/38326925658", + "input": "inputs/windows/x86_64/O2/tracy/tracy-profiler.exe", + "original_source_sha256": "ea59088fbeb24edc4503085417a607dbea43cf20ff2c07e2ff78106590d84bd6", + "packaging_checks_passed": true, + "qualified": false, + "available_unique_addresses": 5132, + "rows": [ + { + "address": 5371116688, + "name": "tracy::detail::Table<1,80,short,tracy::Worker::SourceLocationZones,tracy::hash,std::equal_to >::insertKeyPrepareEmptySpot", + "cell_id": "28", + "platform": "windows", + "project": "tracy", + "optimization": "O2", + "binary": "windows/x86_64/O2/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2/tracy/tracy-profiler.exe", + "id": "028" + }, + { + "address": 5369328048, + "name": "nlohmann::json_abi_v3_12_0::basic_json,std::allocator >,bool,__int64,unsigned __int64,double,std::allocator,nlohmann::json_abi_v3_12_0::adl_serializer,std::vector >,void>::push_back", + "cell_id": "28", + "platform": "windows", + "project": "tracy", + "optimization": "O2", + "binary": "windows/x86_64/O2/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2/tracy/tracy-profiler.exe", + "id": "073" + }, + { + "address": 5371686704, + "name": "tracy::detail::Table<1,80,unsigned int,char const *,tracy::hash,std::equal_to >::try_increase_info", + "cell_id": "28", + "platform": "windows", + "project": "tracy", + "optimization": "O2", + "binary": "windows/x86_64/O2/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2/tracy/tracy-profiler.exe", + "id": "118" + }, + { + "address": 5368911392, + "name": "std::vector >::_Buy_nonzero", + "cell_id": "28", + "platform": "windows", + "project": "tracy", + "optimization": "O2", + "binary": "windows/x86_64/O2/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2/tracy/tracy-profiler.exe", + "id": "163" + }, + { + "address": 5370045408, + "name": "tracy::detail::Table<1,80,unsigned int,tracy::View::MemPathData,tracy::hash,std::equal_to >::initData", + "cell_id": "28", + "platform": "windows", + "project": "tracy", + "optimization": "O2", + "binary": "windows/x86_64/O2/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2/tracy/tracy-profiler.exe", + "id": "208" + } + ] + }, + { + "id": "29", + "state": "imported-pe-pdb-verified", + "project": "winsparkle", + "platform": "windows", + "architecture": "x86_64", + "optimization": "O2", + "source_repository": "vslavik/winsparkle", + "source_revision": "a8986caf620262f7d4581b241436ceaa0cc9370f", + "binary": "windows/x86_64/O2/winsparkle/WinSparkle.dll", + "binary_sha256": "fa7cc5abf55619e119df660256bfcb42d224426b55182bbac7016bd0a9b6470c", + "stripped": "windows/x86_64/O2/winsparkle/WinSparkle.dll.stripped", + "stripped_sha256": "469e4682860c42d2da2d0ddc19df68e77bef1041260b51c2f388448b309a11ed", + "pdb_sha256": "be3b3ea575749890134219db3bd83b973b555d2ddc1bcfbb24f447f3bf49b34e", + "pdb_guid": "912D2D07-555A-46AB-A5B7-A08F8629FB8C", + "pdb_age": 1, + "code_sections_equal": true, + "debug_directory_removed": true, + "project_owned_function_count": 1048, + "evidence_github_run": 38208011147, + "scoring_ready": false, + "remaining_gates": [ + "original source archive and preprocessed TU replay", + "full source-range identity correspondence", + "MSVC source/ABI type evaluation adapter" + ], + "input": "inputs/windows/x86_64/O2/winsparkle/WinSparkle.dll", + "original_source_sha256": "e349da43394275abf8ba0c58323022354009e8d4fc9e70e8da3d00df965db122", + "packaging_checks_passed": true, + "qualified": false, + "evidence_recipe_commit": "38039cbf1aee3c6c9e516f09463493b4b67bdaad", + "evidence_run_url": "https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/38208011147", + "available_unique_addresses": 1048, + "rows": [ + { + "address": 6442623764, + "name": "winsparkle::WinSparkleDialog::UpdateLayout", + "cell_id": "29", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2", + "binary": "windows/x86_64/O2/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2/winsparkle/WinSparkle.dll", + "id": "029" + }, + { + "address": 6442591412, + "name": "wxMBConvStrictUTF8::Clone", + "cell_id": "29", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2", + "binary": "windows/x86_64/O2/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2/winsparkle/WinSparkle.dll", + "id": "074" + }, + { + "address": 6442534180, + "name": "std::num_put > >::_Ifmt", + "cell_id": "29", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2", + "binary": "windows/x86_64/O2/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2/winsparkle/WinSparkle.dll", + "id": "119" + }, + { + "address": 6442598780, + "name": "wxGDIObject::IsFree", + "cell_id": "29", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2", + "binary": "windows/x86_64/O2/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2/winsparkle/WinSparkle.dll", + "id": "164" + }, + { + "address": 6442592052, + "name": "wxGDIObject::CreateRefData", + "cell_id": "29", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2", + "binary": "windows/x86_64/O2/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2/winsparkle/WinSparkle.dll", + "id": "209" + } + ] + }, + { + "id": "30", + "state": "imported-pe-pdb-verified", + "project": "OpenCC", + "platform": "windows", + "architecture": "x86_64", + "optimization": "O2-noinline", + "source_repository": "BYVoid/OpenCC", + "source_revision": "c363a7ba51d487950982bd8a589211ffbfd95ba1", + "binary": "windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "binary_sha256": "01cfab8ccee0ea36e42fcc525c5dd1b71dbaaf92dd15fa8bfefec12d8e23b04c", + "stripped": "windows/x86_64/O2-noinline/OpenCC/opencc.exe.stripped", + "stripped_sha256": "d1c3dc4ec79ec94b0058b3120afefb070a2be0825833c4d0793f4c8a7b51ef59", + "pdb_sha256": "7e45a4fa85bd99a502c0fe79131e24eccc8b33e5631939c7ca286a4fca700709", + "pdb_guid": "31A1C385-3727-470A-8E0C-1CE856FEFBAF", + "pdb_age": 1, + "code_sections_equal": true, + "debug_directory_removed": true, + "project_owned_function_count": 5604, + "project_ownership_method": "pdb-module-file-provenance", + "ownership_scope": "procedures in source-matched compilands; header-origin and dependency ownership need per-procedure verification", + "evidence_github_run": 38326743980, + "preprocessed_tu_count": 42, + "scoring_ready": false, + "remaining_gates": [ + "full source-range identity correspondence", + "MSVC source/ABI type evaluation adapter" + ], + "effective_optimization_profile_verified": true, + "build_environment": { + "runner": "win25-vs2026", + "image_version": "20260925.250.1" + }, + "preprocessed_archive": "windows/x86_64/O2-noinline/OpenCC/preprocessed.tar.gz", + "build_provenance": "windows/x86_64/O2-noinline/OpenCC/build-provenance.json", + "evidence_job_id": 115067056349, + "evidence_recipe_commit": "0bc8b97f2846f01b2f976722d28bf28cfa13c20d", + "evidence_run_url": "https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/38326743980", + "input": "inputs/windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "original_source_sha256": "3864f7b1c414f36a9d0aa547470806a4e07da8645697a3f6e1eeae3a83cad1a4", + "packaging_checks_passed": true, + "qualified": false, + "available_unique_addresses": 5604, + "rows": [ + { + "address": 5369102496, + "name": "std::move", + "cell_id": "30", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "id": "030" + }, + { + "address": 5369017984, + "name": "opencc::AmbiguityStream::~AmbiguityStream", + "cell_id": "30", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "id": "075" + }, + { + "address": 5368786336, + "name": "std::_Ptr_base::_Decref", + "cell_id": "30", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "id": "120" + }, + { + "address": 5369017024, + "name": "std::vector,std::allocator > >", + "cell_id": "30", + "platform": "windows", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "input": "inputs/windows/x86_64/O2-noinline/OpenCC/opencc.exe", + "id": "210" + } + ] + }, + { + "id": "31", + "state": "imported-pe-pdb-verified", + "project": "detours", + "platform": "windows", + "architecture": "x86_64", + "optimization": "O2-noinline", + "source_repository": "microsoft/Detours", + "source_revision": "e4bfd6b03e50de46b47abfbd1e46b384f0c5f833", + "binary": "windows/x86_64/O2-noinline/detours/withdll.exe", + "binary_sha256": "63e23327bfc006906060c33f562992a74b7d063716e1950a1859c1c464243fc6", + "stripped": "windows/x86_64/O2-noinline/detours/withdll.exe.stripped", + "stripped_sha256": "882b7cbf0890ef5e13fd05c9659c652650232bdefbef9d82c5ada4c4fb16064b", + "pdb_sha256": "eaf8465f9cd3ff33b5994f94c5c1de22bdb6f0bfebefb18664a64d04034e1221", + "pdb_guid": "35848DC8-235B-4EE5-B1E2-48E588999499", + "pdb_age": 1, + "code_sections_equal": true, + "debug_directory_removed": true, + "project_owned_function_count": 136, + "evidence_github_run": 38208011147, + "scoring_ready": false, + "remaining_gates": [ + "original source archive and preprocessed TU replay", + "full source-range identity correspondence", + "MSVC source/ABI type evaluation adapter" + ], + "input": "inputs/windows/x86_64/O2-noinline/detours/withdll.exe", + "original_source_sha256": "d7dc7da01ec5c2c7909036e24ac30ed4678c815f7e7ee97d6ad28df8f812cc97", + "packaging_checks_passed": true, + "qualified": false, + "evidence_recipe_commit": "38039cbf1aee3c6c9e516f09463493b4b67bdaad", + "evidence_run_url": "https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/38208011147", + "available_unique_addresses": 136, + "rows": [ + { + "address": 5368753200, + "name": "CDetourDis::Copy66", + "cell_id": "31", + "platform": "windows", + "project": "detours", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/detours/withdll.exe", + "input": "inputs/windows/x86_64/O2-noinline/detours/withdll.exe", + "id": "031" + }, + { + "address": 5368725568, + "name": "EnumerateModulesInProcess", + "cell_id": "31", + "platform": "windows", + "project": "detours", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/detours/withdll.exe", + "input": "inputs/windows/x86_64/O2-noinline/detours/withdll.exe", + "id": "076" + }, + { + "address": 5368748912, + "name": "DetourSetIgnoreTooSmall", + "cell_id": "31", + "platform": "windows", + "project": "detours", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/detours/withdll.exe", + "input": "inputs/windows/x86_64/O2-noinline/detours/withdll.exe", + "id": "121" + }, + { + "address": 5368754080, + "name": "CDetourDis::CopyVex2", + "cell_id": "31", + "platform": "windows", + "project": "detours", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/detours/withdll.exe", + "input": "inputs/windows/x86_64/O2-noinline/detours/withdll.exe", + "id": "166" + }, + { + "address": 5368724080, + "name": "DetourRestoreAfterWith", + "cell_id": "31", + "platform": "windows", + "project": "detours", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/detours/withdll.exe", + "input": "inputs/windows/x86_64/O2-noinline/detours/withdll.exe", + "id": "211" + } + ] + }, + { + "id": "32", + "state": "imported-pe-pdb-verified", + "project": "ninja", + "platform": "windows", + "architecture": "x86_64", + "optimization": "O2-noinline", + "source_repository": "ninja-build/ninja", + "source_revision": "e4a128055f7698522da6de2821ffe0761623f298", + "binary": "windows/x86_64/O2-noinline/ninja/ninja.exe", + "binary_sha256": "f0f44bfc1a6644b120aff2093bb2490e78662da220218ede5c9f0542db1eb33c", + "stripped": "windows/x86_64/O2-noinline/ninja/ninja.exe.stripped", + "stripped_sha256": "dba990b021df2c0189ccb75ee345513ec90d987c73f9f6a169a41705e98df246", + "pdb_sha256": "2d7d9c84f38df731b38ba57d9e20b4a58582b7d8f8e2373825e956f0c0bedada", + "pdb_guid": "4EAA2065-0C7E-4FF6-8277-9C7DBF70F66C", + "pdb_age": 1, + "code_sections_equal": true, + "debug_directory_removed": true, + "project_owned_function_count": 4896, + "project_ownership_method": "pdb-module-file-provenance", + "ownership_scope": "procedures in source-matched compilands; header-origin and dependency ownership need per-procedure verification", + "evidence_github_run": 38326743980, + "preprocessed_tu_count": 38, + "scoring_ready": false, + "remaining_gates": [ + "full source-range identity correspondence", + "MSVC source/ABI type evaluation adapter" + ], + "effective_optimization_profile_verified": true, + "build_environment": { + "runner": "win25-vs2026", + "image_version": "20260925.250.1" + }, + "preprocessed_archive": "windows/x86_64/O2-noinline/ninja/preprocessed.tar.gz", + "build_provenance": "windows/x86_64/O2-noinline/ninja/build-provenance.json", + "evidence_job_id": 115067056338, + "evidence_recipe_commit": "0bc8b97f2846f01b2f976722d28bf28cfa13c20d", + "evidence_run_url": "https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/38326743980", + "input": "inputs/windows/x86_64/O2-noinline/ninja/ninja.exe", + "original_source_sha256": "fcd9a184d69f2bd795ffcd8b76dbbea7f7f4d306230c27842289b237cb378023", + "packaging_checks_passed": true, + "qualified": false, + "available_unique_addresses": 4896, + "rows": [ + { + "address": 5369006896, + "name": "std::basic_string,std::allocator >::basic_string,std::allocator >", + "cell_id": "32", + "platform": "windows", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2-noinline/ninja/ninja.exe", + "id": "032" + }, + { + "address": 5368884160, + "name": "std::tuple,std::allocator > const &>::tuple,std::allocator > const &>,std::allocator > const &,0>", + "cell_id": "32", + "platform": "windows", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2-noinline/ninja/ninja.exe", + "id": "077" + }, + { + "address": 5369006064, + "name": "std::_Tree,0> >::max_size", + "cell_id": "32", + "platform": "windows", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2-noinline/ninja/ninja.exe", + "id": "122" + }, + { + "address": 5368885520, + "name": "std::vector,std::allocator >,enum EvalString::TokenType>,std::allocator,std::allocator >,enum EvalString::TokenType> > >::_Emplace_back_with_unused_capacity,std::allocator >,enum EvalString::TokenType> >", + "cell_id": "32", + "platform": "windows", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2-noinline/ninja/ninja.exe", + "id": "167" + }, + { + "address": 5368735072, + "name": "std::_Compressed_pair,std::_Compressed_pair >,std::_Tree_val >,1>,1>::_Compressed_pair,std::_Compressed_pair >,std::_Tree_val >,1>,1> const &,std::_Zero_then_variadic_args_t>", + "cell_id": "32", + "platform": "windows", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/ninja/ninja.exe", + "input": "inputs/windows/x86_64/O2-noinline/ninja/ninja.exe", + "id": "212" + } + ] + }, + { + "id": "33", + "state": "imported-pe-pdb-verified", + "project": "snappy", + "platform": "windows", + "architecture": "x86_64", + "optimization": "O2-noinline", + "source_repository": "google/snappy", + "source_revision": "2a6c78c781cb76ad191795449cb2267f33138150", + "binary": "windows/x86_64/O2-noinline/snappy/snappy.dll", + "binary_sha256": "15b49a51fa60b783b0d7807875109ff898a217e42ce1990e4b68c5feea0d0aac", + "stripped": "windows/x86_64/O2-noinline/snappy/snappy.dll.stripped", + "stripped_sha256": "0998482d359e03639280b7f1c90e4dfc41e55cef47e9fbed77f2075e43e1b5f4", + "pdb_sha256": "29e3b881a06c8c80ce106f09a61c9234bee9438682b9b340fa4be2b5fe581da1", + "pdb_guid": "3202EE82-D0B0-4CC3-B205-740E3837C7AF", + "pdb_age": 1, + "code_sections_equal": true, + "debug_directory_removed": true, + "project_owned_function_count": 407, + "project_ownership_method": "pdb-module-file-provenance", + "ownership_scope": "procedures in source-matched compilands; header-origin and dependency ownership need per-procedure verification", + "evidence_github_run": 38326743980, + "preprocessed_tu_count": 4, + "scoring_ready": false, + "remaining_gates": [ + "full source-range identity correspondence", + "MSVC source/ABI type evaluation adapter" + ], + "effective_optimization_profile_verified": true, + "build_environment": { + "image_version": "20260925.250.1", + "runner": "win25-vs2026" + }, + "preprocessed_archive": "windows/x86_64/O2-noinline/snappy/preprocessed.tar.gz", + "build_provenance": "windows/x86_64/O2-noinline/snappy/build-provenance.json", + "evidence_job_id": 115067056111, + "evidence_recipe_commit": "0bc8b97f2846f01b2f976722d28bf28cfa13c20d", + "evidence_run_url": "https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/38326743980", + "input": "inputs/windows/x86_64/O2-noinline/snappy/snappy.dll", + "original_source_sha256": "c3bddf6e337e8ca86620ac6ae668b4288f8352f2453dc3f7fa1500a7d56bcfb0", + "packaging_checks_passed": true, + "qualified": false, + "available_unique_addresses": 407, + "rows": [ + { + "address": 6442481776, + "name": "snappy::UNALIGNED_LOAD64", + "cell_id": "33", + "platform": "windows", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2-noinline/snappy/snappy.dll", + "id": "033" + }, + { + "address": 6442470240, + "name": "std::vector >::operator[]", + "cell_id": "33", + "platform": "windows", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2-noinline/snappy/snappy.dll", + "id": "078" + }, + { + "address": 6442480800, + "name": "snappy::SnappyIOVecReader::Skip", + "cell_id": "33", + "platform": "windows", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2-noinline/snappy/snappy.dll", + "id": "123" + }, + { + "address": 6442484096, + "name": "std::basic_string,std::allocator >::empty", + "cell_id": "33", + "platform": "windows", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2-noinline/snappy/snappy.dll", + "id": "168" + }, + { + "address": 6442479472, + "name": "snappy::RawCompressFromIOVec", + "cell_id": "33", + "platform": "windows", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/snappy/snappy.dll", + "input": "inputs/windows/x86_64/O2-noinline/snappy/snappy.dll", + "id": "213" + } + ] + }, + { + "id": "34", + "state": "imported-pe-pdb-verified", + "project": "tracy", + "platform": "windows", + "architecture": "x86_64", + "optimization": "O2-noinline", + "source_repository": "wolfpld/tracy", + "source_revision": "dd29819f3d3e668983d974507038c08dba06479c", + "binary": "windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "binary_sha256": "0484030e5f54e6f503bab2507b8542132bb0c6575664a9ee3973a549a710bea8", + "stripped": "windows/x86_64/O2-noinline/tracy/tracy-profiler.exe.stripped", + "stripped_sha256": "b5d346dc70c14e9db33ff70d330a8ab594b17783f8651d79baef5f39a324df1a", + "pdb_sha256": "bc927a765c670b8c861f07b751206a6ece91618a5f5e9e5f9b8e9931b525b944", + "pdb_guid": "C15D6E4C-3C47-4D49-9B93-1571A11688EA", + "pdb_age": 1, + "code_sections_equal": true, + "debug_directory_removed": true, + "project_owned_function_count": 26455, + "project_ownership_method": "pdb-module-file-provenance", + "ownership_scope": "procedures in source-matched compilands; header-origin and dependency ownership need per-procedure verification", + "evidence_github_run": 38326925658, + "preprocessed_tu_count": 131, + "scoring_ready": false, + "remaining_gates": [ + "full source-range identity correspondence", + "MSVC source/ABI type evaluation adapter", + "fetched dependency source revisions, licenses and original archives" + ], + "effective_optimization_profile_verified": true, + "external_dependency_preprocessed_tus": 10, + "build_environment": { + "runner": "win25-vs2026", + "image_version": "20260925.250.1" + }, + "preprocessed_archive": "windows/x86_64/O2-noinline/tracy/preprocessed.tar.gz", + "build_provenance": "windows/x86_64/O2-noinline/tracy/build-provenance.json", + "evidence_job_id": 115067644743, + "evidence_recipe_commit": "be3a21392523d756ca3a5cba3ffd4eecbe1951d4", + "evidence_run_url": "https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/38326925658", + "input": "inputs/windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "original_source_sha256": "ea59088fbeb24edc4503085417a607dbea43cf20ff2c07e2ff78106590d84bd6", + "packaging_checks_passed": true, + "qualified": false, + "available_unique_addresses": 26455, + "rows": [ + { + "address": 5368918560, + "name": "std::ranges::_Unwrap_sent const *,std::pair const *>", + "cell_id": "34", + "platform": "windows", + "project": "tracy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "id": "034" + }, + { + "address": 5370210544, + "name": "tracy::pdqsort_detail::sort2,std::equal_to >::Iter<1> > > >,", + "cell_id": "34", + "platform": "windows", + "project": "tracy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "id": "079" + }, + { + "address": 5370194528, + "name": "std::make_pair,std::equal_to >::InsertionState>", + "cell_id": "34", + "platform": "windows", + "project": "tracy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "id": "124" + }, + { + "address": 5369723472, + "name": "tracy::Vector,std::equal_to >::Iter<0> >::~Vector,std::equal_to >::Iter<0> >", + "cell_id": "34", + "platform": "windows", + "project": "tracy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "id": "169" + }, + { + "address": 5371373120, + "name": "std::addressof<", + "cell_id": "34", + "platform": "windows", + "project": "tracy", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "input": "inputs/windows/x86_64/O2-noinline/tracy/tracy-profiler.exe", + "id": "214" + } + ] + }, + { + "id": "35", + "state": "imported-pe-pdb-verified", + "project": "winsparkle", + "platform": "windows", + "architecture": "x86_64", + "optimization": "O2-noinline", + "source_repository": "vslavik/winsparkle", + "source_revision": "a8986caf620262f7d4581b241436ceaa0cc9370f", + "binary": "windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "binary_sha256": "8a090c9f377070536746797c1fc6801052efd54958ae1a5ca7e78a94b2bcc4ca", + "stripped": "windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll.stripped", + "stripped_sha256": "d65d12084251b25776f8d95176718399e9c2b0989c478c72ea523a54ce4d5492", + "pdb_sha256": "a9a28de0ac66c02ae32553727e981816c1a16840e34687b9e7eee1bf3c8304ef", + "pdb_guid": "0F07F185-301D-4397-875B-B40BB5C187AA", + "pdb_age": 1, + "code_sections_equal": true, + "debug_directory_removed": true, + "project_owned_function_count": 2160, + "evidence_github_run": 38208011147, + "scoring_ready": false, + "remaining_gates": [ + "original source archive and preprocessed TU replay", + "full source-range identity correspondence", + "MSVC source/ABI type evaluation adapter" + ], + "input": "inputs/windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "original_source_sha256": "e349da43394275abf8ba0c58323022354009e8d4fc9e70e8da3d00df965db122", + "packaging_checks_passed": true, + "qualified": false, + "evidence_recipe_commit": "38039cbf1aee3c6c9e516f09463493b4b67bdaad", + "evidence_run_url": "https://github.com/sjkim1127/Decbench_SampleSet/actions/runs/38208011147", + "available_unique_addresses": 2160, + "rows": [ + { + "address": 6442521088, + "name": "std::forward,std::allocator > >", + "cell_id": "35", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "id": "035" + }, + { + "address": 6442501184, + "name": "std::_Vector_const_iterator > >::operator*", + "cell_id": "35", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "id": "080" + }, + { + "address": 6442590884, + "name": "wxWindowBase::CanBeOutsideClientArea", + "cell_id": "35", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "id": "125" + }, + { + "address": 6442619508, + "name": "std::_Default_allocator_traits,std::allocator > > >::construct,std::allocator >,std::basic_string,std::allocator > >", + "cell_id": "35", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "id": "170" + }, + { + "address": 6442624904, + "name": "std::_Vector_iterator > >::_Unwrapped", + "cell_id": "35", + "platform": "windows", + "project": "winsparkle", + "optimization": "O2-noinline", + "binary": "windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "input": "inputs/windows/x86_64/O2-noinline/winsparkle/WinSparkle.dll", + "id": "215" + } + ] + }, + { + "id": "36", + "project": "OpenCC", + "platform": "macos", + "optimization": "O0", + "source_revision": "c363a7ba51d487950982bd8a589211ffbfd95ba1", + "state": "artifact-built-debug-verified", + "scoring_ready": false, + "binary": "macos/arm64/O0/OpenCC/opencc", + "binary_sha256": "c22b0ce950114f35e82282be36d1e0533f43a423466991859a7cb951b6f28402", + "stripped": "macos/arm64/O0/OpenCC/opencc.stripped", + "stripped_sha256": "67c815d39a1dfca4f436d0f756e97418aadd7963198a3a81e375b4c310f1939e", + "preprocessed_tus": 40, + "compiler": "Apple clang version 17.0.0 (clang-1700.6.4.2)", + "effective_flags": "-O0 -g -fno-builtin -save-temps=obj -DNDEBUG", + "remaining_gates": [ + "source/ABI type ground truth adapter" + ], + "elapsed_seconds": 11.485363541985862, + "code_sections_equal": true, + "debug_removed": true, + "dsym_uuid": "6EE6D361-76D9-4BF2-A537-836897399441", + "source_owned_function_records": 664, + "source_owned_cus": 24, + "debug_producers": [ + "Apple clang version 17.0.0 (clang-1700.6.4.2)" + ], + "input": "inputs/macos/arm64/O0/OpenCC/opencc", + "original_source_sha256": "3864f7b1c414f36a9d0aa547470806a4e07da8645697a3f6e1eeae3a83cad1a4", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 664, + "rejected_unmapped_range_records": 0, + "runtime_sections_equal": true, + "available_unique_addresses": 664, + "rows": [ + { + "address": 4295469832, + "name": "Convert", + "cell_id": "36", + "platform": "macos", + "project": "OpenCC", + "optimization": "O0", + "binary": "macos/arm64/O0/OpenCC/opencc", + "input": "inputs/macos/arm64/O0/OpenCC/opencc", + "id": "036" + }, + { + "address": 4295136360, + "name": "NewFromString", + "cell_id": "36", + "platform": "macos", + "project": "OpenCC", + "optimization": "O0", + "binary": "macos/arm64/O0/OpenCC/opencc", + "input": "inputs/macos/arm64/O0/OpenCC/opencc", + "id": "081" + }, + { + "address": 4295165176, + "name": "GetDefault", + "cell_id": "36", + "platform": "macos", + "project": "OpenCC", + "optimization": "O0", + "binary": "macos/arm64/O0/OpenCC/opencc", + "input": "inputs/macos/arm64/O0/OpenCC/opencc", + "id": "126" + }, + { + "address": 4295560260, + "name": "TakeErrorMessage", + "cell_id": "36", + "platform": "macos", + "project": "OpenCC", + "optimization": "O0", + "binary": "macos/arm64/O0/OpenCC/opencc", + "input": "inputs/macos/arm64/O0/OpenCC/opencc", + "id": "171" + }, + { + "address": 4295144012, + "name": "ConfigInternal", + "cell_id": "36", + "platform": "macos", + "project": "OpenCC", + "optimization": "O0", + "binary": "macos/arm64/O0/OpenCC/opencc", + "input": "inputs/macos/arm64/O0/OpenCC/opencc", + "id": "216" + } + ] + }, + { + "id": "37", + "project": "ninja", + "platform": "macos", + "optimization": "O0", + "source_revision": "e4a128055f7698522da6de2821ffe0761623f298", + "state": "artifact-built-debug-verified", + "scoring_ready": false, + "binary": "macos/arm64/O0/ninja/ninja", + "binary_sha256": "b5a512f903e47174cda7e21852bac8801336b3ac63d1b3bb5204307d53bf3d86", + "stripped": "macos/arm64/O0/ninja/ninja.stripped", + "stripped_sha256": "75cf0ed6a02b4ff1eebd959774a9c1be03d84aa77272c8feff683bb217de54b8", + "preprocessed_tus": 35, + "compiler": "Apple clang version 17.0.0 (clang-1700.6.4.2)", + "effective_flags": "-O0 -g -fno-builtin -save-temps=obj -DNDEBUG", + "remaining_gates": [ + "source/ABI type ground truth adapter" + ], + "elapsed_seconds": 9.716409458080307, + "source_owned_function_records": 814, + "source_owned_cus": 35, + "debug_producers": [ + "Apple clang version 17.0.0 (clang-1700.6.4.2)" + ], + "code_sections_equal": true, + "debug_removed": true, + "dsym_uuid": "25DD99D4-F44C-4CD5-84EA-D348AB6B9992", + "input": "inputs/macos/arm64/O0/ninja/ninja", + "original_source_sha256": "fcd9a184d69f2bd795ffcd8b76dbbea7f7f4d306230c27842289b237cb378023", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 814, + "rejected_unmapped_range_records": 0, + "runtime_sections_equal": true, + "available_unique_addresses": 814, + "rows": [ + { + "address": 4295304968, + "name": "Phony", + "cell_id": "37", + "platform": "macos", + "project": "ninja", + "optimization": "O0", + "binary": "macos/arm64/O0/ninja/ninja", + "input": "inputs/macos/arm64/O0/ninja/ninja", + "id": "037" + }, + { + "address": 4295101904, + "name": "~BuildLog", + "cell_id": "37", + "platform": "macos", + "project": "ninja", + "optimization": "O0", + "binary": "macos/arm64/O0/ninja/ninja", + "input": "inputs/macos/arm64/O0/ninja/ninja", + "id": "082" + }, + { + "address": 4295304616, + "name": "GetBinding", + "cell_id": "37", + "platform": "macos", + "project": "ninja", + "optimization": "O0", + "binary": "macos/arm64/O0/ninja/ninja", + "input": "inputs/macos/arm64/O0/ninja/ninja", + "id": "127" + }, + { + "address": 4295129500, + "name": "EdgeWanted", + "cell_id": "37", + "platform": "macos", + "project": "ninja", + "optimization": "O0", + "binary": "macos/arm64/O0/ninja/ninja", + "input": "inputs/macos/arm64/O0/ninja/ninja", + "id": "172" + }, + { + "address": 4295414460, + "name": "operator[]", + "cell_id": "37", + "platform": "macos", + "project": "ninja", + "optimization": "O0", + "binary": "macos/arm64/O0/ninja/ninja", + "input": "inputs/macos/arm64/O0/ninja/ninja", + "id": "217" + } + ] + }, + { + "id": "38", + "project": "snappy", + "platform": "macos", + "optimization": "O0", + "source_revision": "2a6c78c781cb76ad191795449cb2267f33138150", + "state": "artifact-built-debug-verified", + "scoring_ready": false, + "binary": "macos/arm64/O0/snappy/libsnappy.dylib", + "binary_sha256": "0895f1938fd0175b8c367c9f36a26cd5b09b1f29f7f40d16e137a32f7beb7463", + "stripped": "macos/arm64/O0/snappy/libsnappy.dylib.stripped", + "stripped_sha256": "4f90c07ed46bb47bcef75d19216f737818d5a5efb24a540e92c9fcecd2c94b40", + "preprocessed_tus": 4, + "compiler": "Apple clang version 17.0.0 (clang-1700.6.4.2)", + "effective_flags": "-O0 -g -fno-builtin -save-temps=obj -DNDEBUG", + "remaining_gates": [ + "source/ABI type ground truth adapter" + ], + "elapsed_seconds": 4.1910661670845, + "code_sections_equal": true, + "debug_removed": true, + "dsym_uuid": "DDCFB710-9C2F-4E63-A9B9-CE0F61FF6504", + "source_owned_function_records": 227, + "source_owned_cus": 4, + "debug_producers": [ + "Apple clang version 17.0.0 (clang-1700.6.4.2)" + ], + "input": "inputs/macos/arm64/O0/snappy/libsnappy.dylib", + "original_source_sha256": "c3bddf6e337e8ca86620ac6ae668b4288f8352f2453dc3f7fa1500a7d56bcfb0", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 227, + "rejected_unmapped_range_records": 0, + "runtime_sections_equal": true, + "available_unique_addresses": 227, + "rows": [ + { + "address": 38396, + "name": "GetBase", + "cell_id": "38", + "platform": "macos", + "project": "snappy", + "optimization": "O0", + "binary": "macos/arm64/O0/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O0/snappy/libsnappy.dylib", + "id": "038" + }, + { + "address": 17812, + "name": "~CompressionContext", + "cell_id": "38", + "platform": "macos", + "project": "snappy", + "optimization": "O0", + "binary": "macos/arm64/O0/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O0/snappy/libsnappy.dylib", + "id": "083" + }, + { + "address": 49728, + "name": "SlowAppend", + "cell_id": "38", + "platform": "macos", + "project": "snappy", + "optimization": "O0", + "binary": "macos/arm64/O0/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O0/snappy/libsnappy.dylib", + "id": "128" + }, + { + "address": 23648, + "name": "SnappyArrayWriter", + "cell_id": "38", + "platform": "macos", + "project": "snappy", + "optimization": "O0", + "binary": "macos/arm64/O0/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O0/snappy/libsnappy.dylib", + "id": "173" + }, + { + "address": 20972, + "name": "CompressFromIOVec", + "cell_id": "38", + "platform": "macos", + "project": "snappy", + "optimization": "O0", + "binary": "macos/arm64/O0/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O0/snappy/libsnappy.dylib", + "id": "218" + } + ] + }, + { + "id": "39", + "project": "OpenCC", + "platform": "macos", + "optimization": "O2", + "source_revision": "c363a7ba51d487950982bd8a589211ffbfd95ba1", + "state": "artifact-built-debug-verified", + "scoring_ready": false, + "binary": "macos/arm64/O2/OpenCC/opencc", + "binary_sha256": "5e668889f94a044abbba18bc7dba585edb06a358d659d358034523b933196f91", + "stripped": "macos/arm64/O2/OpenCC/opencc.stripped", + "stripped_sha256": "22cc4554475c4d2d5f7454b298cbb551f0d75d9d02ff69731bfac068573201f8", + "preprocessed_tus": 40, + "compiler": "Apple clang version 17.0.0 (clang-1700.6.4.2)", + "effective_flags": "-O2 -g -fno-builtin -save-temps=obj -DNDEBUG", + "remaining_gates": [ + "source/ABI type ground truth adapter" + ], + "elapsed_seconds": 14.688851209008135, + "code_sections_equal": true, + "debug_removed": true, + "dsym_uuid": "793E2B4D-8AC6-3C2F-BD05-8BE217ABC87E", + "source_owned_function_records": 323, + "source_owned_cus": 24, + "debug_producers": [ + "Apple clang version 17.0.0 (clang-1700.6.4.2)" + ], + "input": "inputs/macos/arm64/O2/OpenCC/opencc", + "original_source_sha256": "3864f7b1c414f36a9d0aa547470806a4e07da8645697a3f6e1eeae3a83cad1a4", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 323, + "rejected_unmapped_range_records": 0, + "runtime_sections_equal": true, + "available_unique_addresses": 323, + "rows": [ + { + "address": 4295331624, + "name": "ConstructBuffer", + "cell_id": "39", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2", + "binary": "macos/arm64/O2/OpenCC/opencc", + "input": "inputs/macos/arm64/O2/OpenCC/opencc", + "id": "039" + }, + { + "address": 4295071540, + "name": "NewFromFile", + "cell_id": "39", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2", + "binary": "macos/arm64/O2/OpenCC/opencc", + "input": "inputs/macos/arm64/O2/OpenCC/opencc", + "id": "084" + }, + { + "address": 4294972844, + "name": "CopyFileContents", + "cell_id": "39", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2", + "binary": "macos/arm64/O2/OpenCC/opencc", + "input": "inputs/macos/arm64/O2/OpenCC/opencc", + "id": "129" + }, + { + "address": 4295247992, + "name": "AppendConvertedSegment", + "cell_id": "39", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2", + "binary": "macos/arm64/O2/OpenCC/opencc", + "input": "inputs/macos/arm64/O2/OpenCC/opencc", + "id": "174" + }, + { + "address": 4295335864, + "name": "TextDict", + "cell_id": "39", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2", + "binary": "macos/arm64/O2/OpenCC/opencc", + "input": "inputs/macos/arm64/O2/OpenCC/opencc", + "id": "219" + } + ] + }, + { + "id": "40", + "project": "ninja", + "platform": "macos", + "optimization": "O2", + "source_revision": "e4a128055f7698522da6de2821ffe0761623f298", + "state": "artifact-built-debug-verified", + "scoring_ready": false, + "binary": "macos/arm64/O2/ninja/ninja", + "binary_sha256": "97efcaae68161e8a98d322e608e0cb1824a53a0772090706cddbbff22a2d9ba5", + "stripped": "macos/arm64/O2/ninja/ninja.stripped", + "stripped_sha256": "0628307f3fd492bbbec336ee590e604b4f1bd9c0e63d5f6e11fc9955e679931e", + "preprocessed_tus": 35, + "compiler": "Apple clang version 17.0.0 (clang-1700.6.4.2)", + "effective_flags": "-O2 -g -fno-builtin -save-temps=obj -DNDEBUG", + "remaining_gates": [ + "source/ABI type ground truth adapter" + ], + "elapsed_seconds": 10.892080999910831, + "source_owned_function_records": 437, + "source_owned_cus": 35, + "debug_producers": [ + "Apple clang version 17.0.0 (clang-1700.6.4.2)" + ], + "code_sections_equal": true, + "debug_removed": true, + "dsym_uuid": "049E24E5-7CB2-36D9-9795-A74CC79B3369", + "input": "inputs/macos/arm64/O2/ninja/ninja", + "original_source_sha256": "fcd9a184d69f2bd795ffcd8b76dbbea7f7f4d306230c27842289b237cb378023", + "packaging_checks_passed": true, + "qualified": false, + "unique_source_entry_addresses": 437, + "rejected_unmapped_range_records": 0, + "runtime_sections_equal": true, + "available_unique_addresses": 437, + "rows": [ + { + "address": 4295011732, + "name": "LogEntry", + "cell_id": "40", + "platform": "macos", + "project": "ninja", + "optimization": "O2", + "binary": "macos/arm64/O2/ninja/ninja", + "input": "inputs/macos/arm64/O2/ninja/ninja", + "id": "040" + }, + { + "address": 4295180224, + "name": "UnreadToken", + "cell_id": "40", + "platform": "macos", + "project": "ninja", + "optimization": "O2", + "binary": "macos/arm64/O2/ninja/ninja", + "input": "inputs/macos/arm64/O2/ninja/ninja", + "id": "085" + }, + { + "address": 4295116668, + "name": "~PosixJobserverPool", + "cell_id": "40", + "platform": "macos", + "project": "ninja", + "optimization": "O2", + "binary": "macos/arm64/O2/ninja/ninja", + "input": "inputs/macos/arm64/O2/ninja/ninja", + "id": "130" + }, + { + "address": 4295103556, + "name": "~EdgeEnv", + "cell_id": "40", + "platform": "macos", + "project": "ninja", + "optimization": "O2", + "binary": "macos/arm64/O2/ninja/ninja", + "input": "inputs/macos/arm64/O2/ninja/ninja", + "id": "175" + }, + { + "address": 4295045872, + "name": "PrintHeader", + "cell_id": "40", + "platform": "macos", + "project": "ninja", + "optimization": "O2", + "binary": "macos/arm64/O2/ninja/ninja", + "input": "inputs/macos/arm64/O2/ninja/ninja", + "id": "220" + } + ] + }, + { + "id": "41", + "project": "snappy", + "platform": "macos", + "optimization": "O2", + "source_revision": "2a6c78c781cb76ad191795449cb2267f33138150", + "state": "artifact-built-debug-verified", + "scoring_ready": false, + "binary": "macos/arm64/O2/snappy/libsnappy.dylib", + "binary_sha256": "290f83b127c2772cafdd2098c76341e224bff558c4639ebe09b39a3dd16f0454", + "stripped": "macos/arm64/O2/snappy/libsnappy.dylib.stripped", + "stripped_sha256": "d518851383b135b64296702fc6dd9d67d2cfc587da212358e8a7a67e921baffd", + "preprocessed_tus": 4, + "compiler": "Apple clang version 17.0.0 (clang-1700.6.4.2)", + "effective_flags": "-O2 -g -fno-builtin -save-temps=obj -DNDEBUG", + "remaining_gates": [ + "source/ABI type ground truth adapter" + ], + "elapsed_seconds": 4.437700125039555, + "code_sections_equal": true, + "debug_removed": true, + "dsym_uuid": "B5A6CC22-65C9-3097-8CEB-D942F15EC8C4", + "source_owned_function_records": 105, + "source_owned_cus": 4, + "debug_producers": [ + "Apple clang version 17.0.0 (clang-1700.6.4.2)" + ], + "input": "inputs/macos/arm64/O2/snappy/libsnappy.dylib", + "original_source_sha256": "c3bddf6e337e8ca86620ac6ae668b4288f8352f2453dc3f7fa1500a7d56bcfb0", + "packaging_checks_passed": true, + "qualified": false, + "runtime_sections_equal": true, + "unique_source_entry_addresses": 105, + "rejected_unmapped_range_records": 0, + "available_unique_addresses": 105, + "rows": [ + { + "address": 16172, + "name": "Compress", + "cell_id": "41", + "platform": "macos", + "project": "snappy", + "optimization": "O2", + "binary": "macos/arm64/O2/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2/snappy/libsnappy.dylib", + "id": "041" + }, + { + "address": 18392, + "name": "Skip", + "cell_id": "41", + "platform": "macos", + "project": "snappy", + "optimization": "O2", + "binary": "macos/arm64/O2/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2/snappy/libsnappy.dylib", + "id": "086" + }, + { + "address": 4736, + "name": "Peek", + "cell_id": "41", + "platform": "macos", + "project": "snappy", + "optimization": "O2", + "binary": "macos/arm64/O2/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2/snappy/libsnappy.dylib", + "id": "131" + }, + { + "address": 25764, + "name": "AppendFromSelf", + "cell_id": "41", + "platform": "macos", + "project": "snappy", + "optimization": "O2", + "binary": "macos/arm64/O2/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2/snappy/libsnappy.dylib", + "id": "176" + }, + { + "address": 25496, + "name": "TryFastAppend", + "cell_id": "41", + "platform": "macos", + "project": "snappy", + "optimization": "O2", + "binary": "macos/arm64/O2/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2/snappy/libsnappy.dylib", + "id": "221" + } + ] + }, + { + "id": "42", + "project": "OpenCC", + "platform": "macos", + "optimization": "O2-noinline", + "source_revision": "c363a7ba51d487950982bd8a589211ffbfd95ba1", + "state": "artifact-built-debug-verified", + "scoring_ready": false, + "binary": "macos/arm64/O2-noinline/OpenCC/opencc", + "binary_sha256": "12351a98acb5fe1ab3ff8b46e0e7267b458b65321b9acb9e64fd18a5de629fd0", + "stripped": "macos/arm64/O2-noinline/OpenCC/opencc.stripped", + "stripped_sha256": "52c1ebb86cb7fd77f7141944adeff8a1025c36393d681036927fee37edb44373", + "preprocessed_tus": 40, + "compiler": "Apple clang version 17.0.0 (clang-1700.6.4.2)", + "effective_flags": "-O2 -fno-inline -g -fno-builtin -save-temps=obj -DNDEBUG", + "remaining_gates": [ + "source/ABI type ground truth adapter" + ], + "elapsed_seconds": 12.922172625083476, + "code_sections_equal": true, + "debug_removed": true, + "dsym_uuid": "4B51FDD6-0B93-3726-850C-6EC00BBC7572", + "source_owned_function_records": 586, + "source_owned_cus": 24, + "debug_producers": [ + "Apple clang version 17.0.0 (clang-1700.6.4.2)" + ], + "input": "inputs/macos/arm64/O2-noinline/OpenCC/opencc", + "original_source_sha256": "3864f7b1c414f36a9d0aa547470806a4e07da8645697a3f6e1eeae3a83cad1a4", + "packaging_checks_passed": true, + "qualified": false, + "runtime_sections_equal": true, + "unique_source_entry_addresses": 586, + "rejected_unmapped_range_records": 0, + "available_unique_addresses": 586, + "rows": [ + { + "address": 4295091500, + "name": "ConversionInspectionResult", + "cell_id": "42", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/macos/arm64/O2-noinline/OpenCC/opencc", + "id": "042" + }, + { + "address": 4295320812, + "name": "SerializeToFile", + "cell_id": "42", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/macos/arm64/O2-noinline/OpenCC/opencc", + "id": "087" + }, + { + "address": 4294974572, + "name": "Null", + "cell_id": "42", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/macos/arm64/O2-noinline/OpenCC/opencc", + "id": "132" + }, + { + "address": 4295068292, + "name": "StrSingleValueDictEntry", + "cell_id": "42", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/macos/arm64/O2-noinline/OpenCC/opencc", + "id": "177" + }, + { + "address": 4295088464, + "name": "~ConversionChain", + "cell_id": "42", + "platform": "macos", + "project": "OpenCC", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/OpenCC/opencc", + "input": "inputs/macos/arm64/O2-noinline/OpenCC/opencc", + "id": "222" + } + ] + }, + { + "id": "43", + "project": "ninja", + "platform": "macos", + "optimization": "O2-noinline", + "source_revision": "e4a128055f7698522da6de2821ffe0761623f298", + "state": "artifact-built-debug-verified", + "scoring_ready": false, + "binary": "macos/arm64/O2-noinline/ninja/ninja", + "binary_sha256": "7b25333d9715669e8bfdf15d17e10ecc0306ff2c5f99555381f411d148391c10", + "stripped": "macos/arm64/O2-noinline/ninja/ninja.stripped", + "stripped_sha256": "d882afc6667e7d3325b33fcaef5957283e5709b3576326753cbb33f4e74f22b5", + "preprocessed_tus": 35, + "compiler": "Apple clang version 17.0.0 (clang-1700.6.4.2)", + "effective_flags": "-O2 -fno-inline -g -fno-builtin -save-temps=obj -DNDEBUG", + "remaining_gates": [ + "source/ABI type ground truth adapter" + ], + "elapsed_seconds": 10.408530166954733, + "source_owned_function_records": 759, + "source_owned_cus": 35, + "debug_producers": [ + "Apple clang version 17.0.0 (clang-1700.6.4.2)" + ], + "code_sections_equal": true, + "debug_removed": true, + "dsym_uuid": "08096672-A05F-3BD8-B2E0-63FCC824492B", + "input": "inputs/macos/arm64/O2-noinline/ninja/ninja", + "original_source_sha256": "fcd9a184d69f2bd795ffcd8b76dbbea7f7f4d306230c27842289b237cb378023", + "packaging_checks_passed": true, + "qualified": false, + "runtime_sections_equal": true, + "unique_source_entry_addresses": 759, + "rejected_unmapped_range_records": 0, + "available_unique_addresses": 759, + "rows": [ + { + "address": 4295172112, + "name": "TimerToMicros", + "cell_id": "43", + "platform": "macos", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/ninja/ninja", + "input": "inputs/macos/arm64/O2-noinline/ninja/ninja", + "id": "043" + }, + { + "address": 4294978148, + "name": "DeferGuessParallelism", + "cell_id": "43", + "platform": "macos", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/ninja/ninja", + "input": "inputs/macos/arm64/O2-noinline/ninja/ninja", + "id": "088" + }, + { + "address": 4295196936, + "name": "PrintStatus", + "cell_id": "43", + "platform": "macos", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/ninja/ninja", + "input": "inputs/macos/arm64/O2-noinline/ninja/ninja", + "id": "133" + }, + { + "address": 4295086932, + "name": "ParseEdge", + "cell_id": "43", + "platform": "macos", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/ninja/ninja", + "input": "inputs/macos/arm64/O2-noinline/ninja/ninja", + "id": "178" + }, + { + "address": 4295188472, + "name": "Node", + "cell_id": "43", + "platform": "macos", + "project": "ninja", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/ninja/ninja", + "input": "inputs/macos/arm64/O2-noinline/ninja/ninja", + "id": "223" + } + ] + }, + { + "id": "44", + "project": "snappy", + "platform": "macos", + "optimization": "O2-noinline", + "source_revision": "2a6c78c781cb76ad191795449cb2267f33138150", + "state": "artifact-built-debug-verified", + "scoring_ready": false, + "binary": "macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "binary_sha256": "84b84e8bb505f3c9d887d3236e62b5727aa18e33c56d20f7096cf3b125b599ba", + "stripped": "macos/arm64/O2-noinline/snappy/libsnappy.dylib.stripped", + "stripped_sha256": "816de59e4c6d2c077c56aa697a9ca2d97a18c0b4f39a1e420c09a8b9c6135477", + "preprocessed_tus": 4, + "compiler": "Apple clang version 17.0.0 (clang-1700.6.4.2)", + "effective_flags": "-O2 -fno-inline -g -fno-builtin -save-temps=obj -DNDEBUG", + "remaining_gates": [ + "source/ABI type ground truth adapter" + ], + "elapsed_seconds": 4.458489082986489, + "code_sections_equal": true, + "debug_removed": true, + "dsym_uuid": "6AA6B162-3C13-3737-9059-58679A9627B0", + "source_owned_function_records": 214, + "source_owned_cus": 4, + "debug_producers": [ + "Apple clang version 17.0.0 (clang-1700.6.4.2)" + ], + "input": "inputs/macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "original_source_sha256": "c3bddf6e337e8ca86620ac6ae668b4288f8352f2453dc3f7fa1500a7d56bcfb0", + "packaging_checks_passed": true, + "qualified": false, + "runtime_sections_equal": true, + "unique_source_entry_addresses": 214, + "rejected_unmapped_range_records": 0, + "available_unique_addresses": 214, + "rows": [ + { + "address": 22440, + "name": "GetBase", + "cell_id": "44", + "platform": "macos", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "id": "044" + }, + { + "address": 4732, + "name": "GetAppendBufferVariable", + "cell_id": "44", + "platform": "macos", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "id": "089" + }, + { + "address": 19576, + "name": "TryFastAppend", + "cell_id": "44", + "platform": "macos", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "id": "134" + }, + { + "address": 21628, + "name": "InternalUncompressAllTags", + "cell_id": "44", + "platform": "macos", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "id": "179" + }, + { + "address": 21516, + "name": "UnalignedCopy64", + "cell_id": "44", + "platform": "macos", + "project": "snappy", + "optimization": "O2-noinline", + "binary": "macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "input": "inputs/macos/arm64/O2-noinline/snappy/libsnappy.dylib", + "id": "224" + } + ] + } + ] +} \ No newline at end of file diff --git a/docs/validation/cpp-type-policy-report.md b/docs/validation/cpp-type-policy-report.md new file mode 100644 index 0000000..3b5a3c9 --- /dev/null +++ b/docs/validation/cpp-type-policy-report.md @@ -0,0 +1,67 @@ +# C++ policy and Fission address transport — 2026-10-13 + +## Result + +This is an upstream policy proposal / supported-subset prototype, not official +C++ qualification, decompiler quality improvement, or a public Union score. + +| Measurement | Fission | Kuna 1.808 | +| --- | ---: | ---: | +| Functions with direct variable PCs | 142/250 | 222/250 | +| Variables with direct PCs | 955 | 1019 | +| Validated variable/address pairs | 9474 | 6910 | +| Invalid variable/address pairs | 0 | 0 | +| Proposed source-view perfect | 0/5 | 1/5 | +| Proposed ABI-view perfect | 0/5 | 1/5 | +| Legacy Windows ABI diagnostic measured | 19/97 | 32/97 | + +Fission already had these 955 variable maps in frozen CLI output. The previous +evaluator ignored them when deciding whether a producer had native evidence, +requiring line maps instead. The adapter forwards them and the metric now uses +the native correspondence path. All 9,474 pairs are exact instruction starts +within their oracle function ranges. This validates membership, not the +semantic correctness of each variable's SSA association. + +The fresh Windows transport run also exposed a producer/PDB spelling mismatch +at a correct entry. Native validation now admits one exact entry identity when +names differ, and rejects ambiguous entries. The final live Windows mapping +retained all three PCs; the Linux run retained 99 and macOS retained four. + +Fission's legacy Windows diagnostic availability increased from 0 to 19, with +no new perfect function. This is transport availability, not improved recovery. +Thirteen of Kuna's 32 diagnostic functions still lack Fission native evidence; +other Windows exclusions are oracle/storage/identity limits. The diagnostic is +not an official C++ score and does not have the same producer denominator. + +The new source and ABI profiles instead have identical oracle eligibility for +both tools on all 45 images. Only 5 of 250 functions qualify under v1; +the remaining 245 are explicitly excluded. Object types, inline identities, +unsupported locations and missing correspondence dominate the exclusions. +These few functions cannot support a C++ ranking or broad accuracy claim. + +## Scope and reproducibility + +Dataset revision `07dd95dbe6214b8a78fc1e4a55961ab515986489`, fixed 250-address sample +and original producer outputs are pinned in `summary.json`. Generation received +stripped inputs plus entry addresses only; no oracle types/source were supplied. +Both profiles were remeasured with disk caches disabled. Native oracle indexing +is shared within one immutable image and bounded to one image per process. + +Three stripped functions from Linux Snappy O0, Windows Snappy O2 and macOS Ninja +O2 were generated twice through the final adapter. NIR code equalled the frozen +baseline; code and variable information were deterministic across repeats. +This is a transport check on three functions, not a fresh regeneration of 250 +functions or recovered-program execution parity. + +`measure_policy.py`, `run_cells.py`, `live_adapter.py` and per-image JSON files +provide the rerun path. This prototype passed scoped Black/Ruff and diff checks. +Formal pytest/CI and recovered-code execution were not run. The existing +type_match SIM105 warning is present on the prerequisite branch too. + +## Next required work + +Upstream must review the source/ABI contract and Union choice. The largest +coverage blocker is now structural object/field/base type ground truth and +recovered descriptors, followed by split/inline variable identity and reliable +formal/storage correspondence. Fission line occurrence and parameter/stack +provenance remain producer work; no line map was fabricated from variable names. diff --git a/docs/validation/cpp-type-policy-summary.json b/docs/validation/cpp-type-policy-summary.json new file mode 100644 index 0000000..9f96e9d --- /dev/null +++ b/docs/validation/cpp-type-policy-summary.json @@ -0,0 +1,129 @@ +{ + "policy": "cpp-types-v2-proposal", + "official_score": false, + "dataset_revision": "07dd95dbe6214b8a78fc1e4a55961ab515986489", + "population_sha256": "f432c528e3071efa60fdaecc132aafa8c351e386bf7203d252e2875d6de02620", + "requested": 250, + "platforms": { + "linux": 108, + "windows": 97, + "macos": 45 + }, + "producer_binary_sha256": "20b4ccf68b5562084953fb4441b0e84dea25c1151b1dc1b325d05e36505571d0", + "fission_baseline_source": "52c81dd15e9b2c9efa8cd29f4b5153f1a2690f45", + "rust_joern_revision": "9fd97f50885202596b125ebd2504e64910874bcb", + "profiles": { + "fission": { + "source": { + "eligible": 26, + "perfect": 0, + "mean": 0.009615384615384616 + }, + "abi": { + "eligible": 26, + "perfect": 0, + "mean": 0.04807692307692308 + }, + "legacy_pdb_abi_diagnostic": { + "eligible": 19, + "perfect": 0, + "mean": 0.013157894736842105 + } + }, + "kuna": { + "source": { + "eligible": 26, + "perfect": 1, + "mean": 0.09615384615384616 + }, + "abi": { + "eligible": 26, + "perfect": 1, + "mean": 0.09615384615384616 + }, + "legacy_pdb_abi_diagnostic": { + "eligible": 32, + "perfect": 0, + "mean": 0.015625 + } + } + }, + "exclusions": { + "DWARF function missing or entry identity ambiguous": 16, + "oracle variable correspondence unavailable": 5, + "anonymous object identity unsupported": 31, + "unsupported DWARF graph DW_TAG_subroutine_type": 10, + "incomplete DWARF object definition": 10, + "inline variable identity policy unsupported": 1, + "partial/composite/computed DWARF location": 67, + "PDB identity/storage/formal classification incomplete": 64, + "no observable variable ground truth": 1, + "ambiguous/incomplete CodeView forward record": 11, + "CodeView vptr physical offset unavailable": 8 + }, + "native_validation": { + "fission": { + "functions_seen": 250, + "functions_with_address_provenance": 144, + "functions_resolved": 144, + "functions_unresolved": 0, + "functions_modified": 0, + "line_mapping_rows_seen": 237, + "line_mapping_rows_dropped": 0, + "line_mapping_addresses_seen": 237, + "line_mapping_addresses_dropped": 0, + "line_mapping_addresses_normalized": 0, + "line_mapping_address_duplicates_removed": 0, + "variable_addresses_seen": 9482, + "variable_addresses_dropped": 0, + "variable_addresses_normalized": 0, + "variable_address_duplicates_removed": 0, + "variable_line_numbers_dropped": 0, + "legacy_additive_fields_hydrated": 0 + }, + "kuna": { + "functions_seen": 250, + "functions_with_address_provenance": 250, + "functions_resolved": 250, + "functions_unresolved": 0, + "functions_modified": 0, + "line_mapping_rows_seen": 5341, + "line_mapping_rows_dropped": 0, + "line_mapping_addresses_seen": 8042, + "line_mapping_addresses_dropped": 0, + "line_mapping_addresses_normalized": 0, + "line_mapping_address_duplicates_removed": 0, + "variable_addresses_seen": 6910, + "variable_addresses_dropped": 0, + "variable_addresses_normalized": 0, + "variable_address_duplicates_removed": 0, + "variable_line_numbers_dropped": 0, + "legacy_additive_fields_hydrated": 0 + } + }, + "line_transport": { + "requested": 250, + "successful_code": 250, + "functions_with_lines": 105, + "line_rows": 237, + "by_platform": { + "linux": 57, + "windows": 24, + "macos": 24 + }, + "changed_code_or_direct_variables": [] + }, + "code_hashes": { + "decbench/metrics/cpp_types.py": "fe2cfbbce486685aba6a6aee5bd566a10fbb6d798d9e35f810e4ef079332c6d8", + "decbench/metrics/cpp_type_match.py": "b3e90e9d2d9ece2b7c812cf1ba28e7a1b7fe5a5120952cbda6fe230e981adf40", + "decbench/metrics/cpp_recovered_types.py": "3f187bd522d619dd7551a9367e5a983b37c289a7690e5cb74112ec433025c7c1", + "decbench/metrics/cpp_type_graph.py": "1f84f2276e910792c27111c331bad2869941f24db5cf294d10520197f8ec29f1", + "decbench/utils/pdb_info.py": "5cb3ccbd90e184a5a7088a63cc0f3285fb2ecc047465df9d5d4da0bfd3ff0994", + "decbench/decompilers/raw/fission_raw.py": "ed1c46f39f66cdb4f3fdf4d9fc86e9a791d03ba94d96246034fd907615691d5c", + "decbench/models/decompilation.py": "588d57d94ab688660c44ae6f973b4d576e9db20538458394ed5a17fd59a305f5", + "decbench/metrics/variable_features.py": "687684f44310359de7c72cea28869fe94d3a29944fbf4b635d29400ce301857b", + "decbench/utils/source_extract.py": "bd6ebab375c229601761e2dbb817e5577637e2ef95428bb819ee8dd5c16440ae" + }, + "eligible_with_object_graph": 21, + "excluded": 224 +} diff --git a/docs/validation/cpp-type-policy.md b/docs/validation/cpp-type-policy.md new file mode 100644 index 0000000..a842ccd --- /dev/null +++ b/docs/validation/cpp-type-policy.md @@ -0,0 +1,157 @@ +# C++ object policy and Fission line provenance — local validation + +Status: experimental upstream proposal; no official policy, Union or ranking update. + +## Population and producer scope + +Frozen qualification-v2 revision `07dd95dbe6214b8a78fc1e4a55961ab515986489`; 250 address-guided stripped inputs: +Linux 108, Windows 97, macOS 45, spanning 45 project/platform/optimization cells. +The corpus still lacks complete original-source/command qualification gates. +Population SHA256 `f432c528e3071efa60fdaecc132aafa8c351e386bf7203d252e2875d6de02620`. Original debug files stay with the +evaluator; producers receive only isolated stripped images and entry addresses. + +Fission baseline source `52c81dd15e9b2c9efa8cd29f4b5153f1a2690f45` was built in a clean worktree. +Both baseline and final candidate generated 250 nonempty functions without output reuse. +Final producer binary SHA256 `20b4ccf68b5562084953fb4441b0e84dea25c1151b1dc1b325d05e36505571d0`. Kuna 1.808 outputs are +reused from the earlier frozen 250-function run; Kuna was not freshly generated here. + +## Fission transport results + +* **105/250 functions, 237 scoped NIR definition-line mappings**: Linux 57, + Windows 24, macOS 24 functions. All 237 PCs passed exact native instruction checks. +* **0 changed NIR texts or direct variable lists** across the same-source 250 baseline/candidate pairs. +* 144 functions carried direct variable PCs; all 9,482 variable/PC pairs survived validation. + Pairs are counted per variable, not distinct instruction addresses. +* Linux Snappy O0, Windows Snappy O2 and macOS Ninja O2 anchors were generated twice + using the final binary. Code, variables and scoped line maps were deterministic; + Linux/macOS yielded 8/2 maps, the Windows anchor conservatively yielded none. +* Maps are partial result-definition origins, not full RHS/call/load/branch lineage. + Phi/input/cloned/repeated-definition/partial/unproved cases remain unmapped. +* Text equality and compiler/debug probes are not execution parity or a decompiler + semantic/readability improvement. No binary or recovered function was executed. + +## Type policy results + +Both tools and both views have the same **26/250 eligible functions**, including +**21 functions with complete object graphs**. v1 had 5 eligible scalar functions; +that difference is broader measurement coverage, not a decompiler quality delta. + +| Producer | Source perfect | ABI perfect | Source mean | ABI mean | +| --- | ---: | ---: | ---: | ---: | +| Fission candidate | 0/26 | 0/26 | 0.009615 | 0.048077 | +| Kuna 1.808 (frozen output) | 1/26 | 1/26 | 0.096154 | 0.096154 | + +An opaque object spelling cannot earn name/size-only credit. Complete fields, +offsets, pointee graphs and base relations are compared after type-blind variable +matching. Source and ABI remain distinct views. Producer missingness is a miss; +unsupported oracle data excludes the whole function identically for both tools. +These rates do not represent the 224 unsupported functions or an official ranking. + +Legacy PDB ABI diagnostic controls remain unchanged: Fission 19/97 eligible, +0 perfect, mean 0.013158; Kuna 32/97 eligible, 0 perfect, mean 0.015625. +The default public PDB/type policy is not replaced by this experimental metric. + +### Exclusions (224 functions) + +* 16: DWARF function missing or entry identity ambiguous +* 5: oracle variable correspondence unavailable +* 31: anonymous object identity unsupported +* 10: unsupported DWARF graph DW_TAG_subroutine_type +* 10: incomplete DWARF object definition +* 1: inline variable identity policy unsupported +* 67: partial/composite/computed DWARF location +* 64: PDB identity/storage/formal classification incomplete +* 1: no observable variable ground truth +* 11: ambiguous/incomplete CodeView forward record +* 8: CodeView vptr physical offset unavailable + +Computed/split locations, hidden/formal/storage ambiguities, anonymous/forward +objects, function/member pointers, incomplete native vptr positions and inline +identity remain explicit bounds. Virtual-base locators are preserved, never +converted to offset zero; cross-debug-format equivalence is not inferred. + +## Formal validation + +* Fission: 1,328 nextest tests passed, 1 skipped; pcode/decompiler/CLI checks, + release build, workspace fmt and owner-boundary scan passed. +* DecBench C++ CI lane locally: 156 passed, 5 platform-specific tests skipped. + New graph tests compile real DWARF and CodeView/PDB fixtures, exercise ordinary + bases/fields, opaque/incomplete rejection, pointer cycles and missing-recovery + population invariance. Pure CodeView fixtures cover virtual locators/bitfields. +* New policy mypy: 4 files clean. New owner Black/Ruff checks passed. +* Full local repository pytest baseline: 806 passed, 23 failed, 26 skipped, 24 + errors. Final run: 864 passed, 19 failed, + 28 skipped, 24 errors, with no new failure identities. Two overload/cache fixture + failures were fixed; two GNU/ELF ICF tests now skip honestly on this Mac. + Remaining failures include host compiler/object-format assumptions, site-content + expectations, evalkit ELF fixtures and backend/source-native tests. They are + recorded, not reported as full repository success. +* Full-repository style/type checks remain non-green: Ruff 20 findings, Black + 11 files needing format, and mypy 84 errors in 25 files. Scoped new policy + checks are clean; these repository-wide findings are retained in the logs. +* The new Ubuntu workflow includes all relevant identity/provenance tests and + compiles real ELF/PDB fixtures. Remote CI has not run before submission. + Rust-Joern (existing CFG dependency) is pinned to + `9fd97f50885202596b125ebd2504e64910874bcb` for reproducibility. + +## Reproduction and evidence + +`cpp-type-policy-population.json` records immutable selection and input/debug +hashes. `cpp-type-policy-summary.json` records code hashes and aggregate decisions. +Use `scripts/evaluate_cpp_type_policy.py` on canonical `DecompilationResult` JSON +with an explicit original debug image, requested-function list, view and data model. +No score/decompilation cache is used. Source units, if supplied, are evaluator-only. + +Full native outputs, paired baseline/candidate generation, cell scores, exclusions, +logs and repeated probes are retained locally under +`benchmark/artifacts/cpp-type-policy/2026-10-13/` in the Fission workspace. +The code is based on DecBench main `14e14b87429043aa06f64e067847ac0a925e41a6` +and includes the still-open #91 prerequisites. Submit as a dependent draft and +remove the prerequisite diff once #91 lands. Human review is required by DecBench +AGENTS.md before submission; no leaderboard/site publication is requested. + +## Correctness review follow-up (base 9f50ebf) + +Three review findings are fixed locally and remain subject to submission review: + +* The C++ type evaluator rejects multiple producer records at one native address + after requested-address filtering and before oracle preparation. Missing output + stays empty recovery; extra records outside the plan do not expand the population. +* The shared parser selects an error-free C++ AST when C parsing fails, including + ordinary `int&`/`int&&` parameter declarations without special function names. + Valid C inputs retain their C grammar. +* Nominal object indirections tokenize `&&`, `&` and `*`. An object rvalue reference + has one `rref` layer and one pointer layer in ABI projection. Invalid nested + references abstain; ILP32 object-layout requirements are unchanged. + +C++ type cache generation is 4; the shared-parser change bumps legacy type-match +cache generation from 24 to 25. No default C++ metric or policy adoption is added. + +Pre-fix reproduction: 14 failed / 6 passed. Current focused workflow locally: +268 passed / 5 platform skips, with scoped Black/Ruff and C++ owner mypy clean. +Full baseline: 927 passed / 19 failed / 28 skipped / 24 errors. Candidate: +949 passed / 19 failed / 28 skipped / 24 errors. All 43 existing failure/error +identities are unchanged. Real DWARF and clang-cl/PDB fixtures compare recovered +rvalue-reference object graphs in source and ABI views without running them; +these graph fixtures do not qualify the whole native MSVC evaluation pipeline. + +Fresh type-policy evaluation of all frozen 250 requests across 45 cells completed +for both baseline and candidate. Eligibility stays 26/250 for both tools/views; +source/ABI perfect remains Fission 0/26 and frozen Kuna 1.808 1/26. Every +function result and exclusion list is identical. This is evaluator regression +evidence, not a new producer run, GED measurement or ranking update. + +The local replay harness prepares native debug readers, source indexes and +oracle rows once per immutable image process, independently for baseline and +candidate. Metric caches are disabled and metric results are computed fresh. +An initial unshared attempt exceeded 300 seconds on a large image; it is retained +as incomplete evidence. Nineteen completed cell executions agree with their +unshared controls after excluding only timing/preparation metadata. The final +45-cell replay has no failed or timed-out cell. Complete patch, commands, input +hashes and reports are retained in the Fission workspace under +`benchmark/artifacts/cpp-review-correctness/2026-10-18/`. + +PR #91 is still open at this review snapshot. Prerequisite removal/rebase and +exact submitted-head upstream CI remain merge gates. The prior head's green fork +run is not evidence for this unsubmitted delta. Virtual-base cross-format policy +and a future malformed-PDB fuzz/property gate remain separate follow-up work. diff --git a/pyproject.toml b/pyproject.toml index 51b457e..598324d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "pyelftools>=0.29", "tree-sitter>=0.26,<0.27", "tree-sitter-c>=0.24,<0.25", + "tree-sitter-cpp>=0.23,<0.24", "cfgutils>=1.10.2", # GED's CFG parser, rust-joern, is not on PyPI and needs a native build: # see docs/benchmarking.md. diff --git a/scripts/audit_cpp_toolchains.py b/scripts/audit_cpp_toolchains.py new file mode 100644 index 0000000..2369eb8 --- /dev/null +++ b/scripts/audit_cpp_toolchains.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +"""Compile an explicit fixture without linking/executing it; never emit C++ scores.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import shutil +import subprocess +from pathlib import Path +from typing import Any + +from decbench.utils.binfmt import BinInfo, object_info + +TARGETS = ("aarch64-unknown-linux-gnu", "x86_64-pc-windows-msvc", "arm64-apple-macos13") +OPTIMIZATIONS = (("O0", ("-O0",)), ("O2", ("-O2",)), ("O2-noinline", ("-O2", "-fno-inline"))) + + +def run(command: list[str]) -> str: + return subprocess.run(command, check=True, capture_output=True, text=True, timeout=120).stdout + + +def text_sections(path: Path) -> list[tuple[str, str]]: + import lief + + binary: Any = lief.parse(str(path)) + if isinstance(binary, lief.MachO.FatBinary): + binary = binary.at(0) + if binary is None: + raise ValueError("compiler output is not a readable object") + return [ + (section.name, hashlib.sha256(bytes(section.content)).hexdigest()) + for section in binary.sections + if section.name in {".text", "__text"} and section.content + ] + + +def audit(compiler: str, source: Path, output: Path) -> dict[str, Any]: + output.mkdir(parents=True, exist_ok=True) + source = source.resolve() + report: dict[str, Any] = { + "profile": "cpp-toolchain-fixture-v1-diagnostic", + "official_score": False, + "compiler": str(Path(compiler).resolve()), + "version": run([compiler, "--version"]), + "source_sha256": hashlib.sha256(source.read_bytes()).hexdigest(), + "source": str(source), + "executed_programs": False, + "cells": [], + "bounds": [ + "Explicit fixture only, not corpus qualification or recovered-code recompilation", + "Clang cross-target output does not qualify GCC or native MSVC equivalence", + "No standard-library, runtime linking, or execution parity qualification", + ], + } + for target in TARGETS: + for name, optimization in OPTIMIZATIONS: + cell = output / target / name + cell.mkdir(parents=True, exist_ok=True) + flags = [ + "-target", + target, + "-std=c++17", + "-g", + "-fexceptions", + "-fcxx-exceptions", + *optimization, + ] + original, preprocessed, replay = ( + cell / "original.o", + cell / "unit.ii", + cell / "replay.o", + ) + commands = [ + [compiler, *flags, "-c", str(source), "-o", str(original)], + [compiler, *flags, "-E", str(source), "-o", str(preprocessed)], + [ + compiler, + *flags, + "-x", + "c++-cpp-output", + "-c", + str(preprocessed), + "-o", + str(replay), + ], + [compiler, *flags, "-S", "-emit-llvm", str(source), "-o", str(cell / "unit.ll")], + [compiler, *flags, "-Xclang", "-ast-dump=json", "-fsyntax-only", str(source)], + ] + for command in commands[:-1]: + run(command) + ast_text = run(commands[-1]) + (cell / "ast.json").write_text(ast_text) + ast = json.loads(ast_text) + kinds: dict[str, int] = {} + + pending = [ast] + while pending: + node = pending.pop() + kind = node.get("kind", "") + kinds[kind] = kinds.get(kind, 0) + 1 + pending.extend(node.get("inner", [])) + ir = (cell / "unit.ll").read_text() + identity = object_info(original) + expected = { + "aarch64-unknown-linux-gnu": BinInfo("elf", "aarch64", 64), + "x86_64-pc-windows-msvc": BinInfo("pe", "x86-64", 64), + "arm64-apple-macos13": BinInfo("macho", "aarch64", 64), + }[target] + if identity != expected or object_info(replay) != expected: + raise ValueError("compiler objects do not match the requested target") + first, second = text_sections(original), text_sections(replay) + if not first or first != second: + raise ValueError("preprocessing roundtrip changes code sections") + if not all( + kinds.get(k) + for k in [ + "FunctionTemplateDecl", + "TemplateArgument", + "CXXConstructExpr", + "CXXThrowExpr", + ] + ): + raise ValueError("required C++ AST witnesses absent") + cleanup = "cleanuppad" if "windows" in target else "landingpad" + if "invoke " not in ir or "personality" not in ir or cleanup not in ir: + raise ValueError("required exceptional cleanup IR absent") + report["cells"].append( + { + "target": target, + "optimization": name, + "object_identity": vars(identity), + "commands": commands, + "text_sections_identical": True, + "text_sections": first, + "ast_kinds": kinds, + "exception_ir_present": True, + "cleanup_ir_present": True, + "preprocessed_sha256": hashlib.sha256(preprocessed.read_bytes()).hexdigest(), + "ir_sha256": hashlib.sha256(ir.encode()).hexdigest(), + "ast_sha256": hashlib.sha256(ast_text.encode()).hexdigest(), + } + ) + return report + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--compiler", default=shutil.which("clang++")) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + if args.compiler is None: + parser.error("clang++ or --compiler required") + report = audit(args.compiler, args.source, args.output) + (args.output / "REPORT.json").write_text(json.dumps(report, indent=2) + "\n") + print(f"{len(report['cells'])} fixture cells verified; no corpus score produced") + + +if __name__ == "__main__": + main() diff --git a/scripts/audit_historical_ged_iso.py b/scripts/audit_historical_ged_iso.py index 4460ba2..e0f47a3 100644 --- a/scripts/audit_historical_ged_iso.py +++ b/scripts/audit_historical_ged_iso.py @@ -20,7 +20,6 @@ import math import multiprocessing as mp import pickle -import re from collections import defaultdict from pathlib import Path from typing import Any @@ -31,6 +30,8 @@ extract_cfgs_from_source, resolved_source_for_binary, ) +from decbench.utils.function_identity import parse_function_storage_key +from decbench.utils.results_tree import split_functions if __package__: from scripts.reeval_ged import ( @@ -69,7 +70,6 @@ HISTORICAL_ISO_SCHEMA_VERSION = 4 HISTORICAL_ISO_SEMANTICS = "directed-role-aware-networkx-all-large-pairs-v3" -_MARKER = re.compile(r"^// Function: (\S+) @ 0x[0-9a-fA-F]+\s*$", re.M) Artifact = tuple[str, str, str, str, str] ReplayTask = tuple[str, str, str, str, dict[str, dict[str, Any]], dict[str, Any]] @@ -223,8 +223,14 @@ def eval_historical_iso_one(task: ReplayTask) -> tuple[str, dict[str, dict], dic slice_key, c_path, same_opt_path, legacy_path, targets, metadata = task _opt, _project, stem, _decompiler = slice_key.split("::", 3) artifact = Path(c_path) - text = artifact.read_text(errors="replace") - markers = set(_MARKER.findall(text)) + # Historical target keys are source-level function names, so compare against + # the semantic half of each storage key rather than the key itself. Reading + # through split_functions also keeps a marker whose name contains a space + # visible, which the previous local `(\S+)` pattern silently skipped -- and + # a skipped marker here raises below rather than degrading quietly. + markers = { + parse_function_storage_key(storage_key)[0] for storage_key in split_functions(artifact) + } missing_markers = set(targets) - markers if missing_markers: raise RuntimeError(f"{slice_key} missing function markers: {sorted(missing_markers)}") diff --git a/scripts/evaluate_cpp_ged.py b/scripts/evaluate_cpp_ged.py new file mode 100644 index 0000000..b7eb5ed --- /dev/null +++ b/scripts/evaluate_cpp_ged.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Audit a DecompilationResult JSON against preprocessed C++ and native debug. + +Run with DECBENCH_NO_CACHE=1 for a fresh measurement. No corpus code is executed. +The output is a bounded diagnostic report, never a leaderboard/results overlay. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from decbench.models.decompilation import DecompilationResult +from decbench.pipeline.cpp_ged import evaluate_cpp_ged + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("decompilation", type=Path) + parser.add_argument("--requested-functions", type=Path, required=True) + parser.add_argument("--compilation-database", type=Path) + parser.add_argument("--source", type=Path, action="append", default=[]) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + result = DecompilationResult.model_validate_json(args.decompilation.read_text()) + rows = json.loads(args.requested_functions.read_text()) + requested = [ + ( + row["name"], + int(row["address"], 0) if isinstance(row["address"], str) else int(row["address"]), + ) + for row in rows + ] + report = evaluate_cpp_ged( + result, + args.source, + requested_functions=requested, + compilation_database=args.compilation_database, + ) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(report.model_dump_json(indent=2) + "\n") + + +if __name__ == "__main__": + main() diff --git a/scripts/evaluate_cpp_type_policy.py b/scripts/evaluate_cpp_type_policy.py new file mode 100644 index 0000000..3077007 --- /dev/null +++ b/scripts/evaluate_cpp_type_policy.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Re-score one canonical producer artifact under the opt-in C++ proposal. + +The original debug image and source units belong only to the evaluator. Missing +requested functions are empty recovery; this tool never calls a producer or +publishes a leaderboard. Use a separate output file for each source/ABI view. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from decbench.metrics.base import MetricConfig +from decbench.metrics.cpp_type_match import CppTypeMatchMetric +from decbench.models.decompilation import DecompilationResult + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--binary", type=Path, required=True, help="original debug oracle image") + parser.add_argument( + "--decompilation", type=Path, required=True, help="DecompilationResult JSON" + ) + parser.add_argument( + "--requested-functions", + type=Path, + required=True, + help="fixed JSON list of objects with name and address", + ) + parser.add_argument("--view", choices=["source", "abi"], required=True) + parser.add_argument("--data-model", choices=["LP64", "LLP64", "ILP32"], required=True) + parser.add_argument("--source", type=Path, action="append", default=[]) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + result = DecompilationResult.model_validate_json(args.decompilation.read_text()) + result.binary_path = args.binary.resolve() + rows = json.loads(args.requested_functions.read_text()) + requested = [ + (r["name"], int(r["address"], 0) if isinstance(r["address"], str) else int(r["address"])) + for r in rows + ] + if len({a for _, a in requested}) != len(requested): + parser.error("requested function entries must have unique addresses") + if any(a < 0 for _, a in requested): + parser.error("negative requested address") + metric = CppTypeMatchMetric( + MetricConfig( + use_cache=False, extra_options={"view": args.view, "data_model": args.data_model} + ) + ) + score = metric.compute_for_binary( + result, requested_functions=requested, preprocessed_sources=args.source + ) + payload = { + "official_score": False, + "view": args.view, + "data_model": args.data_model, + "requested_functions": len(requested), + "result": score.model_dump(mode="json"), + } + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(payload, indent=2) + "\n") + + +if __name__ == "__main__": + main() diff --git a/scripts/rebuild_function_data.py b/scripts/rebuild_function_data.py index 8160bac..f538465 100644 --- a/scripts/rebuild_function_data.py +++ b/scripts/rebuild_function_data.py @@ -18,7 +18,6 @@ from __future__ import annotations import json -import re import sys from collections import Counter from pathlib import Path @@ -41,27 +40,15 @@ ) from decbench.scoring.scoreboard import build_scoreboard_from_function_data from decbench.scoring.view_samples import DIFFICULTY_TIERS, select_view_functions -from decbench.utils.results_tree import resolve_binary +from decbench.utils.function_identity import parse_function_storage_key +from decbench.utils.results_tree import resolve_binary, split_functions from decbench.utils.source_extract import function_source, function_source_ex -MARKER = re.compile(r"^// Function: (\S+) @ (0x[0-9a-fA-F]+)\s*$", re.M) PERFECT = {"ged": 0.0, "type_match": 1.0, "byte_match": 1.0} PER_TIER = 100 HARDEST_PER = 12 -def split_functions(c_path: Path) -> dict[str, str]: - """name -> decompiled block for one decompiled .c (code only).""" - text = c_path.read_text(errors="replace") - out: dict[str, str] = {} - ms = list(MARKER.finditer(text)) - for i, m in enumerate(ms): - start = m.end() - end = ms[i + 1].start() if i + 1 < len(ms) else len(text) - out[m.group(1)] = text[start:end].strip() - return out - - class DiskReader: """Lazily reads decompiled blocks and resolves original binaries on disk.""" @@ -82,7 +69,10 @@ def decompiled(self, opt: str, proj: str, stem: str, dec: str) -> dict[str, str] key = (opt, proj, stem, dec) if key not in self._dec_cache: cf = self.root / opt / proj / "decompiled" / f"{dec}_{stem}.c" - self._dec_cache[key] = split_functions(cf) if cf.exists() else {} + blocks = split_functions(cf) if cf.exists() else {} + self._dec_cache[key] = { + storage_key: code for storage_key, (_address, code) in blocks.items() + } return self._dec_cache[key] @@ -112,7 +102,8 @@ def build_samples(fd: FunctionData, reader: DiskReader) -> list[SampleEntry]: decompiled[dec] = code if not decompiled: continue - source, source_status = function_source_ex(binary, f.function) + semantic_name, func_address = parse_function_storage_key(f.function) + source, source_status = function_source_ex(binary, semantic_name, func_address) out.append( SampleEntry( project=g.project, @@ -166,6 +157,7 @@ def build_hardest(fd: FunctionData, reader: DiskReader) -> list[HardestEntry]: if not code: continue binary = reader.binary(g.opt_level, g.project, g.binary) + semantic_name, func_address = parse_function_storage_key(f.function) out.append( HardestEntry( metric=metric, @@ -179,7 +171,9 @@ def build_hardest(fd: FunctionData, reader: DiskReader) -> list[HardestEntry]: size=f.size, labels=f.labels, decompiled_code=code, - source_code=function_source(binary, f.function) if binary else None, + source_code=( + function_source(binary, semantic_name, func_address) if binary else None + ), ) ) kept += 1 diff --git a/scripts/reeval_bytematch.py b/scripts/reeval_bytematch.py index 812f98e..39229b3 100644 --- a/scripts/reeval_bytematch.py +++ b/scripts/reeval_bytematch.py @@ -19,8 +19,10 @@ import multiprocessing as mp import os import sys +from collections import Counter from pathlib import Path +from decbench.utils.function_identity import parse_function_storage_key from decbench.utils.results_tree import OPT_LEVELS, resolve_binary, split_functions DECOMPILERS = ("angr", "ghidra", "ida", "binja", "kuna", "r2dec", "dewolf") @@ -63,10 +65,21 @@ def eval_one(task: tuple[str, str, str, str, str, str]) -> tuple[str, dict]: binary = Path(binary_path) out: dict[str, dict] = {} funcs = split_functions(Path(c_path)) - context = derive_context_decls({n: c for n, (_a, c) in funcs.items()}) - for name, (addr, code) in funcs.items(): + semantic_names = { + storage_key: parse_function_storage_key(storage_key)[0] for storage_key in funcs + } + name_counts = Counter(semantic_names.values()) + context = derive_context_decls( + { + semantic_names[storage_key]: code + for storage_key, (_addr, code) in funcs.items() + if name_counts[semantic_names[storage_key]] == 1 + and parse_function_storage_key(storage_key)[1] is None + } + ) + for storage_key, (addr, code) in funcs.items(): fd = FunctionDecompilation( - name=name, + name=semantic_names[storage_key], address=addr, decompiled_code=code, line_count=code.count("\n") + 1, @@ -74,7 +87,7 @@ def eval_one(task: tuple[str, str, str, str, str, str]) -> tuple[str, dict]: try: mv = metric.compute_for_function(fd, original_binary_path=binary, context_decls=context) except Exception as e: # noqa: BLE001 - out[name] = {"value": 0.0, "compilable": False, "error": str(e)[:120]} + out[storage_key] = {"value": 0.0, "compilable": False, "error": str(e)[:120]} continue md = mv.metadata or {} # Abstain rather than score 0: the per-function call still returns 0 when no @@ -82,7 +95,7 @@ def eval_one(task: tuple[str, str, str, str, str, str]) -> tuple[str, dict]: # Omitting it makes rebuild_function_data drop byte_match for the function. if md.get("skipped"): continue - out[name] = { + out[storage_key] = { "value": float(mv.value), "compilable": bool(md.get("compilable", False)), "dist": md.get("changed_lines"), diff --git a/scripts/reeval_ged.py b/scripts/reeval_ged.py index 1e95a40..06b8afa 100644 --- a/scripts/reeval_ged.py +++ b/scripts/reeval_ged.py @@ -42,7 +42,9 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from decbench.utils.function_identity import parse_function_storage_key # noqa: E402 from decbench.utils.langs import preprocessed_by_stem # noqa: E402 +from decbench.utils.results_tree import FUNCTION_MARKER # noqa: E402 PREVIOUS_GED_MAX_NODES = 60 CHECKPOINT_SCHEMA_VERSION = 8 @@ -747,8 +749,6 @@ def eval_one( and DROPS non-finite (empty-prototype/degenerate source) results so they are excluded from GED's denominator instead of counting as failures. """ - import re - ( opt, project, @@ -857,13 +857,13 @@ def eval_one( # Joern keys CFGs by the parsed BODY name, which can differ from the marker name # (ida's `_rl_set_screen_size` over a `rl_set_screen_size` body). Emitting only # marker-declared functions avoids attributing a row the decompiler never owned. - markers = set( - re.findall( - r"^// Function: (\S+) @ 0x[0-9a-fA-F]+\s*$", - decompiled_text, - re.M, - ) - ) + # Historical score keys and Joern CFG keys are both source-level names, so + # take the semantic half of each storage key rather than the key itself. + # Sharing the marker pattern also keeps a name containing a space visible. + markers = { + parse_function_storage_key(match.group(1))[0] + for match in FUNCTION_MARKER.finditer(decompiled_text) + } if old_values: selected_source_functions = ( set(historical_src_cfgs) if historical_overlay_covered else set(src_cfgs) diff --git a/scripts/repair_decompiled_flags.py b/scripts/repair_decompiled_flags.py index 42321c1..bc84a44 100644 --- a/scripts/repair_decompiled_flags.py +++ b/scripts/repair_decompiled_flags.py @@ -30,14 +30,13 @@ from __future__ import annotations import json -import re import sys from collections import Counter from pathlib import Path from decbench.models.function_data import FunctionData +from decbench.utils.results_tree import split_functions -MARKER = re.compile(r"^// Function: (\S+) @ (0x[0-9a-fA-F]+)\s*$", re.M) LLM_DECOMPILERS = ("codex", "claude-code") PER_DEC_FIELDS = ("values", "perfects", "distances", "decompiled", "compiles") @@ -45,15 +44,20 @@ def marker_names( root: Path, opt: str, proj: str, stem: str, dec: str, cache: dict[tuple, set[str] | None] ) -> set[str] | None: - """Function names present in one decompiled artifact (None = no artifact).""" + """Storage keys present in one decompiled artifact (None = no artifact). + + These are compared against ``FunctionData`` per-function keys, which are + storage keys, so the collision-qualified form is what belongs here. Reading + through ``split_functions`` also keeps a marker whose name contains a space + (``operator new``) visible, which a local ``(\\S+)`` pattern dropped. + """ key = (opt, proj, stem, dec) if key not in cache: cf = root / opt / proj / "decompiled" / f"{dec}_{stem}.c" if not cf.is_file(): cache[key] = None else: - text = cf.read_text(errors="replace") - cache[key] = {m.group(1) for m in MARKER.finditer(text)} + cache[key] = set(split_functions(cf)) return cache[key] diff --git a/scripts/run_benchmark.py b/scripts/run_benchmark.py index c27c7e4..7675458 100644 --- a/scripts/run_benchmark.py +++ b/scripts/run_benchmark.py @@ -243,22 +243,40 @@ def _stripped_copy(binary: Path, strip_dir: Path) -> Path: def _relabel_to_dwarf( - result: DecompilationResult, addr2name: dict[int, str], unstripped: Path + result: DecompilationResult, + addr2name: dict[int, str], + unstripped: Path, + identity_addr2name: dict[int, str] | None = None, ) -> None: - """Re-symbolize a stripped-binary decompilation for evaluation. - - The decompiler analysed the stripped binary, so it named functions by address - (``FUN_00102530`` / ``sub_...``). Map each back to the DWARF name at its - address — renaming in BOTH the code (so GED's Joern parse keys by the right - name) and the function key — and point eval at the UNSTRIPPED (DWARF) binary. - This is pure bookkeeping so name-based eval/GED line up; the decompiler got no - help (its analysis ran on the stripped binary). + """Canonicalize stripped-binary results against the full DWARF target set. + + ``addr2name`` is the selected benchmark target set for this run. + ``identity_addr2name`` is the complete binary-wide source-function universe + used only to decide whether a short name is ambiguous. Keeping those maps + separate makes storage identity invariant under sample-set slicing: selecting + one member of an overload group does not turn ``foo@0x...`` back into ``foo``. + + Backend addresses are normalized back to the canonical DWARF ``low_pc`` + (including historical PE-RVA and Thumb-bit variants), so address-keyed + metrics and source recovery consume the same identity primitive. Output + that cannot be mapped to a requested target is outside the gated benchmark + universe and is dropped. """ from decbench.decompilers.raw import common - # Pre-fix PE decompiles stored bare RVAs; adding the ImageBase recovers the - # DWARF key. Harmless for ELF, where the base is already folded into fd.address. base = common.elf_min_vaddr(unstripped) + + identity_universe = identity_addr2name or addr2name + name_counts: dict[str, int] = {} + for target_name in identity_universe.values(): + name_counts[target_name] = name_counts.get(target_name, 0) + 1 + ambiguous_names = {name for name, count in name_counts.items() if count > 1} + + def storage_key(name: str, address: int) -> str: + if name in ambiguous_names: + return f"{name}@0x{address:x}" + return name + thumb_names: dict[int, tuple[int, str]] = {} blocked_thumb_addresses: set[int] = set() info = binfmt.detect(unstripped) @@ -285,49 +303,69 @@ def _relabel_to_dwarf( for address in blocked_thumb_addresses: thumb_names.pop(address, None) + def dwarf_target(raw_address: int) -> tuple[int, str] | None: + candidates = (raw_address,) if not base else (raw_address, raw_address + base) + for candidate in candidates: + target_name = addr2name.get(candidate) + if target_name is not None: + return candidate, target_name + for candidate in candidates: + thumb_binding = thumb_names.get(candidate & ~1) + if thumb_binding is not None: + return thumb_binding + return None + new_funcs: dict[str, object] = {} - dropped = 0 + recovered_targets: set[int] = set() + dropped_unmapped = 0 + for fd in list(result.functions.values()): - addr = int(fd.address) - candidates = (addr,) if not base else (addr, addr + base) - binding = next( - ( - (candidate, addr2name[candidate]) - for candidate in candidates - if candidate in addr2name - ), - None, - ) - if binding is None: - binding = next( - ( - thumb_names[canonical] - for candidate in candidates - if (canonical := candidate & ~1) in thumb_names - ), - None, - ) - if binding is None: - dropped += 1 + target = dwarf_target(int(fd.address)) + if target is None: + dropped_unmapped += 1 continue - matched_address, dwarf_name = binding - fd.address = matched_address - if dwarf_name != fd.name: + + target_address, target_name = target + recovered_targets.add(target_address) + old_name = fd.name + if target_name != old_name: fd.decompiled_code = re.sub( - r"\b" + re.escape(fd.name) + r"\b", dwarf_name, fd.decompiled_code + r"\b" + re.escape(old_name) + r"\b", + target_name, + fd.decompiled_code, ) - fd.name = dwarf_name - prev = new_funcs.get(fd.name) - if prev is None or len(fd.decompiled_code or "") >= len( - getattr(prev, "decompiled_code", "") or "" + fd.name = target_name + fd.address = target_address + + key = storage_key(target_name, target_address) + previous = new_funcs.get(key) + if previous is None or len(fd.decompiled_code or "") >= len( + getattr(previous, "decompiled_code", "") or "" ): - new_funcs[fd.name] = fd + new_funcs[key] = fd + result.functions = new_funcs # type: ignore[assignment] + + was_all_failure = result.decompiler.failed_functions == ["all"] + missing = [ + storage_key(addr2name[address], address) + for address in sorted(addr2name) + if address not in recovered_targets + ] + if not (was_all_failure and not recovered_targets): + result.decompiler.failed_functions = missing + + result.decompiler.extra = { + **(result.decompiler.extra or {}), + "function_identity": "dwarf-low-pc", + "ambiguous_source_names": len(ambiguous_names), + "dropped_unmapped_functions": dropped_unmapped, + } result.binary_path = unstripped - if dropped: + if dropped_unmapped: result.decompiler.extra = { **(result.decompiler.extra or {}), - "source_filter_unmatched_dropped": dropped, + "source_filter_unmatched_dropped": dropped_unmapped, } @@ -476,15 +514,17 @@ def decompile_project_timed( opt: OptimizationLevel, source_fn_map: dict[str, dict[int, str]], decompilers: list[str] | None = None, + identity_fn_map: dict[str, dict[int, str]] | None = None, ) -> tuple[dict, dict[str, int]]: """Decompile all of a project's binaries at one opt level, with timeouts. Concurrency is managed by a thread pool whose threads each block on a decompile subprocess (the work happens in the child, so the GIL is free). - ``source_fn_map`` maps binary stem -> {low_pc: name} for the project's own - source functions. The decompiler is run on a STRIPPED copy (no debug info / - symbols) and restricted to those ADDRESSES; results are then re-labeled with - the DWARF names and pointed back at the unstripped binary for evaluation. + ``source_fn_map`` maps binary stem -> {low_pc: name} for the selected + targets. ``identity_fn_map`` optionally carries the complete pre-slice source + universe used to keep collision-qualified storage keys stable. The decompiler + is run on a STRIPPED copy (no debug info / symbols) and restricted to the + selected ADDRESSES; results are then re-labeled with DWARF names. ``decompilers`` (default: the global set) lets callers run a SUBSET — used for incremental runs that add/redo only one or two decompilers. Returns (results_dict binary->dec->DecompilationResult, stats). @@ -539,7 +579,8 @@ def decompile_project_timed( amap = source_fn_map.get(stem) or {} if orig is not None: if amap: - _relabel_to_dwarf(res, amap, orig) + identity_map = (identity_fn_map or {}).get(stem) or amap + _relabel_to_dwarf(res, amap, orig, identity_map) else: res.binary_path = orig sanitize_native_provenance(res, orig) @@ -575,6 +616,37 @@ def discover(project: Project, out_dir: Path) -> int: return sum(len(v) for v in project.compiled_binaries.values()) +def _canonical_target_keys(addr2name: dict[int, str]) -> dict[int, str]: + """Binary-wide canonical storage key for every DWARF target address.""" + counts: dict[str, int] = {} + for name in addr2name.values(): + counts[name] = counts.get(name, 0) + 1 + return { + address: (f"{name}@0x{address:x}" if counts[name] > 1 else name) + for address, name in addr2name.items() + } + + +def _select_manifest_targets( + addr2name: dict[int, str], allowed: set[str] | None +) -> dict[int, str]: + """Select a manifest slice without losing binary-wide function identity. + + New manifests may contain canonical ``name@0xADDR`` keys and therefore + select one exact overload. Historical plain-name manifests remain accepted; + when such a name is ambiguous they retain the legacy behavior of selecting + every matching address rather than silently picking one. + """ + if not allowed: + return {} + canonical = _canonical_target_keys(addr2name) + return { + address: name + for address, name in addr2name.items() + if canonical[address] in allowed or name in allowed + } + + def _present_decompilers( decompile_data: dict, project: Project | None = None, @@ -682,17 +754,20 @@ def main() -> int: ts = time.time() source_stems = set(project.preprocessed_sources.get(opt, {}).keys()) src_fn_names: dict[str, dict[int, str]] = {} + src_fn_identity: dict[str, dict[int, str]] = {} src_fn_owners: dict[str, dict[int, tuple[str, str]]] = {} kept_binaries = [] needed_stems: set[str] = set() for b in project.compiled_binaries[opt]: addr_stem: dict[int, str] = {} fns = project_source_functions(b, source_stems, stem_out=addr_stem) + identity_fns = dict(fns) if SAMPLESET_GATE is not None: allowed = SAMPLESET_GATE.get((name, opt.value, b.stem)) - fns = {a: nm for a, nm in fns.items() if allowed and nm in allowed} + fns = _select_manifest_targets(fns, allowed) if fns: src_fn_names[b.stem] = fns + src_fn_identity[b.stem] = identity_fns src_fn_owners[b.stem] = { addr: (func_name, addr_stem[addr]) for addr, func_name in fns.items() @@ -736,7 +811,12 @@ def main() -> int: ) try: dec, dstats = decompile_project_timed( - project, out_dir, opt, src_fn_names, decompilers=to_run + project, + out_dir, + opt, + src_fn_names, + decompilers=to_run, + identity_fn_map=src_fn_identity, ) except Exception as e: # noqa: BLE001 print(f"[{name}/{opt.value}] decompile ERROR: {e}", flush=True) diff --git a/tests/fixtures/cpp_qualification.cpp b/tests/fixtures/cpp_qualification.cpp new file mode 100644 index 0000000..4c3ad4f --- /dev/null +++ b/tests/fixtures/cpp_qualification.cpp @@ -0,0 +1,16 @@ +#ifdef _WIN32 +static_assert(sizeof(long) == 4); +#else +static_assert(sizeof(long) == 8); +#endif +static_assert(sizeof(void*) == 8); +struct Guard { Guard(); ~Guard(); }; +template __attribute__((noinline)) int select_value(int x) { + if constexpr (N > 0) return x + N; + else return x - N; +} +extern "C" int qualification_probe(int x) { + Guard guard; + if (x < 0) throw x; + return select_value<2>(x) + select_value<-2>(x); +} diff --git a/tests/test_cpp_first_pc_locations.py b/tests/test_cpp_first_pc_locations.py new file mode 100644 index 0000000..77fb143 --- /dev/null +++ b/tests/test_cpp_first_pc_locations.py @@ -0,0 +1,160 @@ +"""Multiple native first-PC rows must converge without picking a convenient line.""" + +from pathlib import Path + +import pytest + +from decbench.models.decompilation import ( + DecompilationResult, + DecompilerMetadata, + FunctionDecompilation, +) +from decbench.utils.cfg_identity import CfgJoin, NativeSource, SourceCfgIndex, SourceLocation + + +def _resolve(text: str, locations: tuple[SourceLocation, ...], name: str = "f") -> CfgJoin: + index = SourceCfgIndex() + index.add(text, "unit.ii") + result = DecompilationResult( + binary_path=Path("unused"), + binary_name="fixture", + decompiler=DecompilerMetadata(decompiler_name="fixture"), + ) + result.add_function(FunctionDecompilation(name=name, address=16, decompiled_code="")) + record = NativeSource( + 16, name, ("/src/unit.cpp",), None, "native:16", candidate_locations=locations + ) + return index.resolve(result, {16: [record]}) + + +def test_all_first_pc_rows_must_prove_the_same_definition() -> None: + rows = (SourceLocation("/src/unit.cpp", 1), SourceLocation("/src/unit.cpp", 2)) + joined = _resolve('# 1 "/src/unit.cpp"\nint f(int x) {\nreturn x + 1;\n}\n', rows) + assert set(joined.cfgs) == {"f"} + assert joined.evidence["f"]["native_location_candidates"] == [ + {"path": row.path, "line": row.line} for row in rows + ] + assert joined.evidence["f"]["native_location_policy"] == ( + "all_first_pc_locations_same_definition" + ) + + +@pytest.mark.parametrize( + "text,locations", + [ + ( + '# 1 "/src/unit.cpp"\nint f(){return 1;}\nint g(){return 2;}\n', + (SourceLocation("/src/unit.cpp", 1), SourceLocation("/src/unit.cpp", 2)), + ), + ( + '# 1 "/src/unit.cpp"\nint f(){return 1;}\n', + (SourceLocation("/src/unit.cpp", 1), SourceLocation("/src/unit.cpp", 20)), + ), + ( + '# 1 "/src/unit.cpp"\nint f(){return 1;}\n', + (SourceLocation("/src/unit.cpp", 1), SourceLocation("/other/unit.cpp", 1)), + ), + ( + '# 1 "/src/unit.cpp"\nint f(int x){return x;} int f(double x){return 1;}\n', + (SourceLocation("/src/unit.cpp", 1), SourceLocation("/src/unit.cpp", 1)), + ), + ], +) +def test_unmapped_or_conflicting_candidates_remain_ambiguous( + text: str, locations: tuple[SourceLocation, ...] +) -> None: + joined = _resolve(text, locations) + assert not joined.cfgs + assert joined.evidence["f"]["status"] == "ambiguous_native_source_location" + + +@pytest.mark.parametrize( + "text,name,status", + [ + ( + '# 1 "/src/unit.cpp"\ntemplate int f(T x){\nreturn x;\n}\n', + "f", + "unsupported_template_specialization", + ), + ( + '# 1 "/src/unit.cpp"\nint g(int x){\nreturn x;\n}\n', + "f", + "source_name_mismatch", + ), + ], +) +def test_convergence_does_not_bypass_other_qualification(text: str, name: str, status: str) -> None: + joined = _resolve( + text, (SourceLocation("/src/unit.cpp", 1), SourceLocation("/src/unit.cpp", 2)), name + ) + assert not joined.cfgs + assert joined.evidence[name]["status"] == status + + +def test_zero_line_is_not_removed_to_make_other_locations_converge() -> None: + joined = _resolve( + '# 1 "/src/unit.cpp"\nint f(){return 1;}\n', + (SourceLocation("/src/unit.cpp", 0), SourceLocation("/src/unit.cpp", 1)), + ) + assert not joined.cfgs + + +def test_singular_native_location_keeps_its_legacy_contract() -> None: + record = NativeSource( + 16, "f", ("/src/unit.cpp",), SourceLocation("/src/unit.cpp", 1), "native:16" + ) + assert record.locations == (record.location,) + + +def test_native_transport_keeps_candidates_separate_from_legacy_location( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from decbench.utils import binfmt + from decbench.utils.cfg_identity import native_sources + from decbench.utils.pdb_info import PdbFunction, PdbInfo + + image = tmp_path / "native.exe" + image.write_bytes(b"native identity fixture") + function = PdbFunction( + "f", + 16, + 24, + "unit.obj", + ("/src/unit.cpp",), + 16, + 24, + first_source_locations=(("/src/unit.cpp", 1), ("/src/unit.cpp", 2)), + ) + info = PdbInfo(tmp_path / "native.pdb", "fixture", 1, (function,)) + monkeypatch.setattr(binfmt, "dwarf_info", lambda path: None) + monkeypatch.setattr(binfmt, "pdb_info", lambda path: info) + monkeypatch.setattr(binfmt, "native_debug_paths", lambda path: ()) + native = native_sources(image, {16})[16][0] + assert native.location is None + assert native.locations == ( + SourceLocation("/src/unit.cpp", 1), + SourceLocation("/src/unit.cpp", 2), + ) + assert function.source_file is None and function.source_line == 0 + + +def test_c13_retains_distinct_source_rows_at_identical_native_pc( + monkeypatch: pytest.MonkeyPatch, +) -> None: + import struct + + from decbench.utils import pdb_info + + name = b"/src/unit.cpp\0" + strings = struct.pack(" dict: + return { + "directory": "/build", + "file": source, + "arguments": [ + "clang++", + optimization, + "-g", + "-save-temps=obj", + "-o", + "obj/a.cpp.o", + "-c", + source, + ], + } + + +def test_saved_temps_require_exact_output_and_original_input(tmp_path: Path) -> None: + database = tmp_path / "compile_commands.json" + database.write_text(json.dumps([_command()])) + bindings = saved_temp_units(database) + assert set(bindings) == {"/build/obj/a.cpp"} + assert bindings["/build/obj/a.cpp"].source_file == "/src/a.cpp" + assert "/different/obj/a.cpp" not in bindings + assert "/build/other/a.cpp" not in bindings + assert compilation_path("src/a.cpp", "/build") == "/build/src/a.cpp" + assert compilation_path("src/a.cpp") == "src/a.cpp" + + +def test_conflicting_commands_at_reused_object_output_abstain(tmp_path: Path) -> None: + database = tmp_path / "compile_commands.json" + database.write_text(json.dumps([_command(), _command(optimization="-O2")])) + assert saved_temp_units(database) == {} + database.write_text(json.dumps([_command(), _command()])) + assert len(saved_temp_units(database)) == 1 + database.write_text(json.dumps([_command(), _command(source="/other/a.cpp")])) + assert saved_temp_units(database) == {} + + +def test_missing_flags_and_inconsistent_source_are_not_guessed(tmp_path: Path) -> None: + database = tmp_path / "compile_commands.json" + entry = _command() + entry["arguments"].remove("-save-temps=obj") + database.write_text(json.dumps([entry])) + assert saved_temp_units(database) == {} + entry = _command() + entry["file"] = "/different/a.cpp" + database.write_text(json.dumps([entry])) + with pytest.raises(ValueError, match="absent from recorded arguments"): + saved_temp_units(database) + + +@pytest.mark.skipif(shutil.which("clang++") is None, reason="clang++ required") +@pytest.mark.parametrize("optimization", ["-O0", "-O2"]) +def test_real_saved_temps_and_relative_markers_join(tmp_path: Path, optimization: str) -> None: + from decbench.models.decompilation import ( + DecompilationResult, + DecompilerMetadata, + FunctionDecompilation, + ) + from decbench.pipeline.cpp_ged import evaluate_cpp_ged + from decbench.utils.cfg_identity import native_sources + from decbench.utils.function_identity import dwarf_function_identities + + compiler = shutil.which("clang++") + assert compiler is not None + source = tmp_path / "unit.cpp" + source.write_text("namespace n { int f(int x){return x+1;} }\nint main(){return n::f(0);}\n") + obj = tmp_path / "objects/unit.cpp.o" + obj.parent.mkdir() + arguments = [ + compiler, + optimization, + "-g", + "-save-temps=obj", + "-o", + "objects/unit.cpp.o", + "-c", + "unit.cpp", + ] + subprocess.run(arguments, cwd=tmp_path, check=True, capture_output=True) + binary = tmp_path / "unit" + subprocess.run([compiler, str(obj), "-o", str(binary)], check=True, capture_output=True) + if binary.read_bytes()[:4] != b"\x7fELF": + subprocess.run(["dsymutil", str(binary)], check=True, capture_output=True) + preprocessed = tmp_path / "objects/unit.ii" + assert preprocessed.exists() + database = tmp_path / "compile_commands.json" + database.write_text( + json.dumps([{"directory": str(tmp_path), "file": "unit.cpp", "arguments": arguments}]) + ) + identity = next(row for row in dwarf_function_identities(binary) if row.name == "f") + result = DecompilationResult( + binary_path=binary, + binary_name="unit", + decompiler=DecompilerMetadata(decompiler_name="fixture"), + ) + result.add_function( + FunctionDecompilation( + name="f", address=identity.address, decompiled_code="int sub_10(int x){return x+1;}" + ) + ) + report = evaluate_cpp_ged(result, [preprocessed], compilation_database=database) + assert ( + report.source_eligible == report.paired == report.metric.perfect_count == 1 + ), report.model_dump() + assert report.compilation_database_sha256 + index = native_sources(binary, {identity.address}) + if index[identity.address][0].physical_unit != str(source): + unbound = evaluate_cpp_ged(result, [preprocessed], native_index=index) + assert unbound.source_eligible == 0 + + +def test_relative_origin_uses_native_comp_dir_only() -> None: + from decbench.models.decompilation import ( + DecompilationResult, + DecompilerMetadata, + FunctionDecompilation, + ) + from decbench.utils.cfg_identity import NativeSource, SourceCfgIndex, SourceLocation + + index = SourceCfgIndex() + index.add('# 1 "src/a.cpp"\nint f(){return 1;}\n', "locally-renamed.ii") + result = DecompilationResult( + binary_path=Path("unused"), + binary_name="unit", + decompiler=DecompilerMetadata(decompiler_name="fixture"), + ) + result.add_function( + FunctionDecompilation(name="f", address=16, decompiled_code="int f(){return 1;}") + ) + record = NativeSource( + 16, + "f", + ("/build/src/a.cpp",), + SourceLocation("/build/src/a.cpp", 1), + "fixture", + compilation_directory="/build", + ) + assert index.resolve(result, {16: [record]}).evidence["f"]["status"] == "matched" + record = NativeSource( + 16, + "f", + ("/other/src/a.cpp",), + SourceLocation("/other/src/a.cpp", 1), + "fixture", + compilation_directory="/build", + ) + assert not index.resolve(result, {16: [record]}).cfgs + + +def test_recorded_command_requires_native_directory_and_rejects_mutation(tmp_path: Path) -> None: + from decbench.utils.cfg_identity import NativeSource, NativeSourceIndex + + binary = tmp_path / "fixture" + binary.write_bytes(b"synthetic debug") + database = tmp_path / "compile_commands.json" + database.write_text(json.dumps([_command()])) + record = NativeSource( + 16, + "f", + ("/build/obj/a.cpp",), + None, + "fixture", + compilation_directory="/different", + physical_unit="/build/obj/a.cpp", + ) + index = NativeSourceIndex(binary, {16: [record]}).with_compilation_database(database) + assert index[16][0].unit_binding is None + database.write_text("[]") + with pytest.raises(ValueError, match="compilation database changed"): + index.validate(binary) diff --git a/tests/test_cpp_ged_identity.py b/tests/test_cpp_ged_identity.py new file mode 100644 index 0000000..a8af385 --- /dev/null +++ b/tests/test_cpp_ged_identity.py @@ -0,0 +1,597 @@ +"""Exact native/source/producer CFG joins, independent of display-name reduction.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import pytest + +from decbench.metrics.variable_features import function_definitions +from decbench.models.decompilation import ( + DecompilationResult, + DecompilerMetadata, + FunctionDecompilation, +) +from decbench.pipeline.cpp_ged import evaluate_cpp_ged +from decbench.utils.cfg_identity import ( + NativeSource, + NativeSourceIndex, + SourceCfgIndex, + SourceLocation, + decompiled_cfgs, + origin_source, + source_path, +) + + +def _result(*functions: tuple[str, int, str]) -> DecompilationResult: + result = DecompilationResult( + binary_path=Path("unused"), + binary_name="fixture", + decompiler=DecompilerMetadata(decompiler_name="fixture"), + ) + for name, address, code in functions: + result.add_function(FunctionDecompilation(name=name, address=address, decompiled_code=code)) + return result + + +def _index(text: str, filename: str = "unit.ii") -> SourceCfgIndex: + index = SourceCfgIndex() + index.add(text, filename) + return index + + +def _native( + address: int, name: str, path: str, line: int, unit: str = "/src/a.cpp" +) -> NativeSource: + return NativeSource(address, name, (unit,), SourceLocation(path, line), f"fixture:{address:x}") + + +def test_gcc_and_msvc_origins_preserve_physical_lines() -> None: + source = origin_source( + '# 0 "/src/a.cpp"\n# 1 "/usr/include/x.h" 1 3\nint hidden();\n' + '# 12 "/src/a.cpp" 2\nint f() { return 1; }\n' + ) + assert source.primary == "/src/a.cpp" + assert source.origins == {5: SourceLocation("/src/a.cpp", 12)} + windows = origin_source('#line 15 "D:\\\\src\\\\a.cpp"\r\nint g(){return 2;}\r\n') + assert windows.origins == {2: SourceLocation("d:/src/a.cpp", 15)} + assert len(source.text.splitlines()) == 5 + assert source_path(r"C:\new\a.cpp") == "c:/new/a.cpp" + + +def test_definition_spans_use_bytes_with_unicode_and_large_line_counts() -> None: + code = "// 한글\n" + "\n" * 10000 + "int f(){return 1;}\nint g(){return 2;}\n" + definitions = function_definitions(code) + assert [(d.leaf, d.start_line, d.end_line) for d in definitions] == [ + ("f", 10002, 10002), + ("g", 10003, 10003), + ] + + +def test_overloads_keep_each_definition_and_exact_storage_key(tmp_path: Path) -> None: + index = _index( + '# 1 "/src/a.cpp"\nint f(int x){if(x)return 1;return 0;}\nint f(double x){return 2;}\n' + ) + result = _result( + ("f", 16, "int sub_10(int x){if(x)return 1;return 0;}"), + ("f", 32, "int sub_20(double x){return 2;}"), + ) + native = {16: [_native(16, "f", "/src/a.cpp", 1)], 32: [_native(32, "f", "/src/a.cpp", 2)]} + binary = tmp_path / "fixture" + binary.write_bytes(b"synthetic debug fixture") + result.binary_path = binary + report = evaluate_cpp_ged( + result, source_index=index, native_index=NativeSourceIndex(binary, native) + ) + assert report.paired == 2 + assert report.metric.perfect_count == 2 + assert set(report.metric.function_results) == {"f@0x10", "f@0x20"} + assert report.official_score is False + + +def test_same_basename_wrong_tu_and_missing_origin_do_not_fallback() -> None: + index = _index('# 1 "/other/a.cpp"\nint f(){return 1;}\n') + result = _result(("f", 16, "int sub_10(){return 1;}")) + joined = index.resolve(result, {16: [_native(16, "f", "/src/a.cpp", 1)]}) + assert not joined.cfgs + + +def test_same_line_overloads_and_icf_abstain() -> None: + index = _index('# 1 "/src/a.cpp"\nint f(int x){return x;} int f(double x){return 2;}\n') + result = _result(("f", 16, "int sub_10(){return 1;}")) + row = _native(16, "f", "/src/a.cpp", 1) + assert ( + index.resolve(result, {16: [row]}).evidence["f"]["status"] == "ambiguous_source_definition" + ) + assert ( + index.resolve(result, {16: [row, row]}).evidence["f"]["status"] + == "ambiguous_native_identity" + ) + + +def test_source_location_cannot_borrow_different_named_function() -> None: + index = _index('# 1 "/src/a.cpp"\nint g(){return 1;}\n') + result = _result(("f", 16, "int sub_10(){return 1;}")) + assert ( + index.resolve(result, {16: [_native(16, "f", "/src/a.cpp", 1)]}).evidence["f"]["status"] + == "source_name_mismatch" + ) + + +@pytest.mark.parametrize("native_name", ["bad", "good"]) +@pytest.mark.parametrize("prefix", ["", "template "]) +def test_error_recovery_span_is_not_evidence_of_a_name_mismatch( + native_name: str, prefix: str, monkeypatch: pytest.MonkeyPatch +) -> None: + text = prefix + "int bad(){ ?\n int good(){return 1;}\n}\n" + index = _index('# 1 "/src/a.cpp"\n' + text) + definitions = function_definitions(text) + assert len(definitions) == 1 and definitions[0].has_error + + def unexpected_parse(*args: Any, **kwargs: Any) -> None: + pytest.fail("an erroneous enclosing AST must never reach CFG parsing") + + monkeypatch.setattr("decbench.utils.cfg_identity.parse_definitions", unexpected_parse) + result = _result((native_name, 16, "int output(){return 1;}")) + joined = index.resolve(result, {16: [_native(16, native_name, "/src/a.cpp", 2)]}) + assert not joined.cfgs + assert joined.evidence[native_name]["status"] == "source_syntax_error" + + +def test_template_definition_abstains_without_specialization_model() -> None: + index = _index('# 1 "/src/a.cpp"\ntemplate int f(T x){return x;}\n') + result = _result(("f", 16, "int sub_10(){return 1;}")) + assert ( + index.resolve(result, {16: [_native(16, "f", "/src/a.cpp", 1)]}).evidence["f"]["status"] + == "unsupported_template_specialization" + ) + + +def test_producer_records_are_isolated_and_prototypes_are_not_bodies() -> None: + result = _result( + ("semantic", 16, "extern int helper(); int sub_10(){return helper();}"), + ("bad", 32, "int bad(){if(}"), + ("another", 48, "int sub_30(){return 2;}"), + ) + joined = decompiled_cfgs(result) + assert set(joined.cfgs) == {"semantic", "another"} + assert joined.evidence["bad"]["status"] in {"parser_failure", "missing_definition"} + + +def test_multiple_producer_definitions_never_choose_largest_or_name() -> None: + result = _result(("f", 16, "int helper(){return 0;} int f(int x){if(x)return 1;return 2;}")) + joined = decompiled_cfgs(result) + assert not joined.cfgs + assert joined.evidence["f"]["status"] == "ambiguous_definitions" + + +def test_parser_failures_are_reported_and_missing_native_is_explicit() -> None: + index = _index('# 1 "/src/a.cpp"\nint f(){return 1;}\n') + result = _result(("f", 16, "int sub_10(){return 1;}")) + joined = index.resolve(result, {}) + assert joined.evidence["f"]["status"] == "missing_native_identity" + + +def test_generic_cpp_entry_requires_preprocessed_inputs(tmp_path: Path) -> None: + path = tmp_path / "raw.cpp" + path.write_text("int f(){return 1;}") + with pytest.raises(ValueError, match="preprocessed"): + evaluate_cpp_ged(_result(), [path]) + + +@pytest.mark.parametrize("dwarf_version", [4, 5]) +@pytest.mark.parametrize("optimization", ["-O0", "-O2"]) +def test_real_native_dwarf_definition_join( + tmp_path: Path, dwarf_version: int, optimization: str +) -> None: + import shutil + import subprocess + + from decbench.utils.cfg_identity import native_sources + from decbench.utils.function_identity import dwarf_function_identities + + compiler = shutil.which("clang++") + if compiler is None: + pytest.skip("clang++ required for native debug fixture") + source = tmp_path / "native.cpp" + source.write_text( + "namespace outer {\nint f(int x){if(x)return x;return 2;}\n" + "int f(long x){return 3;}\n}\nint main(){return outer::f(0);}\n" + ) + obj, binary, preprocessed = tmp_path / "native.o", tmp_path / "native", tmp_path / "native.ii" + subprocess.run( + [ + compiler, + optimization, + "-g", + f"-gdwarf-{dwarf_version}", + "-c", + str(source), + "-o", + str(obj), + ], + check=True, + capture_output=True, + ) + subprocess.run([compiler, str(obj), "-o", str(binary)], check=True, capture_output=True) + if binary.read_bytes()[:4] != b"\x7fELF": + subprocess.run(["dsymutil", str(binary)], check=True, capture_output=True) + preprocessed.write_bytes( + subprocess.run([compiler, "-E", str(source)], check=True, capture_output=True).stdout + ) + identities = [row for row in dwarf_function_identities(binary) if row.name == "f"] + assert len(identities) == 2 + result = _result( + *[("f", row.address, f"int sub_{row.address:x}(){{return 1;}}") for row in identities] + ) + result.binary_path = binary + native = native_sources(binary, {row.address for row in identities}) + assert all(len(records) == 1 for records in native.values()) + report = evaluate_cpp_ged(result, [preprocessed], native_index=native) + assert report.source_eligible == report.paired == 2, report.model_dump() + assert all(row["path"] == str(source) for row in report.source_evidence.values()) + + +def test_real_pdb_msvc_line_directive_definition_join(tmp_path: Path) -> None: + import shutil + import subprocess + + from decbench.utils.function_identity import dwarf_function_identities + + compiler, linker = shutil.which("clang-cl"), shutil.which("lld-link") + if compiler is None or linker is None: + pytest.skip("clang-cl/lld-link required for native PDB fixture") + source = tmp_path / "native.cpp" + source.write_text( + "namespace outer {\nint f(int x){if(x)return x;return 2;}\n" + 'int f(long x){return 3;}\n}\nextern "C" int entry(){return outer::f(0);}\n' + ) + obj, binary, pdb = tmp_path / "native.obj", tmp_path / "native.exe", tmp_path / "native.pdb" + preprocessed = tmp_path / "native.ii" + subprocess.run( + [ + compiler, + "--target=x86_64-pc-windows-msvc", + "/c", + "/Z7", + "/Od", + "/GS-", + str(source), + f"/Fo{obj}", + ], + check=True, + capture_output=True, + ) + subprocess.run( + [ + linker, + str(obj), + "/entry:entry", + "/subsystem:console", + "/nodefaultlib", + "/debug:full", + "/opt:noicf", + f"/pdb:{pdb}", + f"/out:{binary}", + ], + check=True, + capture_output=True, + ) + subprocess.run( + [compiler, "--target=x86_64-pc-windows-msvc", "/P", str(source), f"/Fi{preprocessed}"], + check=True, + capture_output=True, + ) + identities = [row for row in dwarf_function_identities(binary) if row.name == "outer::f"] + assert len(identities) == 2 + result = _result( + *[("f", row.address, f"int sub_{row.address:x}(){{return 1;}}") for row in identities] + ) + result.binary_path = binary + report = evaluate_cpp_ged(result, [preprocessed]) + assert report.source_eligible == report.paired == 2, report.model_dump() + + +def test_isolated_definition_retains_full_tu_cfg_for_supported_control_flow() -> None: + from decbench.metrics.ged import _is_isomorphic + from decbench.utils.cfg_identity import parse_definitions + + code = ( + "extern int read_value(); namespace n { int f(int x){" + "while(x>0){if(read_value())break;--x;} return x; } }" + ) + full = parse_definitions(code, "fixture.cpp", "c++") + definition = function_definitions(code)[0] + isolated = parse_definitions(definition.code, "isolated.cpp", "c++") + assert len(full) == len(isolated) == 1 + assert _is_isomorphic(full[0].cfg, isolated[0].cfg) + + +def test_qualified_scope_must_match_even_with_same_leaf_and_location() -> None: + index = _index('# 1 "/src/a.cpp"\nnamespace wrong {int f(){return 1;}}\n') + result = _result(("f", 16, "int sub_10(){return 1;}")) + native = NativeSource( + 16, "f", ("/src/a.cpp",), SourceLocation("/src/a.cpp", 1), "fixture", "right::f" + ) + joined = index.resolve(result, {16: [native]}) + assert not joined.cfgs + assert joined.evidence["f"]["status"] == "source_name_mismatch" + + +def test_infrastructure_and_unexpected_evaluator_errors_do_not_abstain( + monkeypatch: pytest.MonkeyPatch, +) -> None: + import rust_joern + + result = _result(("f", 16, "int sub_10(){return 1;}")) + + def fail(*args: Any, **kwargs: Any) -> None: + raise RuntimeError("library unavailable") + + monkeypatch.setattr(rust_joern, "parse_code", fail) + with pytest.raises(RuntimeError, match="library unavailable"): + decompiled_cfgs(result) + + +def test_missing_tu_and_missing_header_have_distinct_reasons() -> None: + index = _index('# 1 "/src/a.cpp"\nint f(){return 1;}\n') + result = _result(("f", 16, "int sub_10(){return 1;}")) + tu_miss = index.resolve(result, {16: [_native(16, "f", "/src/b.cpp", 1, "/src/b.cpp")]}) + assert tu_miss.evidence["f"]["status"] == "missing_source_tu" + header_miss = index.resolve(result, {16: [_native(16, "f", "/usr/include/b.h", 1)]}) + assert header_miss.evidence["f"]["status"] == "missing_source_origin" + + +def test_unc_paths_match_after_marker_unquoting() -> None: + path = r"\\server\share\a.cpp" + source = origin_source('#line 1 "\\\\\\\\server\\\\share\\\\a.cpp"\nint f(){return 1;}\n') + assert source.primary == source_path(path) + + +def test_unclosed_lexical_scope_is_not_proven() -> None: + definitions = function_definitions("namespace n { int f(){return 1;}\n") + assert definitions and definitions[0].has_error + + +def test_native_index_rejects_other_or_changed_binary(tmp_path: Path) -> None: + a, b = tmp_path / "a", tmp_path / "b" + a.write_bytes(b"a") + b.write_bytes(b"a") + index = NativeSourceIndex(a, {}) + with pytest.raises(ValueError, match="another binary"): + index.validate(b) + a.write_bytes(b"changed") + with pytest.raises(ValueError, match="contents changed"): + index.validate(a) + + +def test_nonfinite_ged_is_evaluator_failure_not_population_exclusion( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from decbench.metrics.ged import GEDMetric + from decbench.models.metrics import MetricValue + + binary = tmp_path / "fixture" + binary.write_bytes(b"synthetic debug fixture") + result = _result(("f", 16, "int sub_10(){return 1;}")) + result.binary_path = binary + index = _index('# 1 "/src/a.cpp"\nint f(){return 1;}\n') + native = NativeSourceIndex(binary, {16: [_native(16, "f", "/src/a.cpp", 1)]}) + monkeypatch.setattr( + GEDMetric, "compute_for_function", lambda *a, **k: MetricValue(value=float("inf")) + ) + with pytest.raises(RuntimeError, match="GED evaluator failed"): + evaluate_cpp_ged(result, source_index=index, native_index=native) + + +def test_fixed_requested_population_keeps_empty_recovery(tmp_path: Path) -> None: + binary = tmp_path / "fixture" + binary.write_bytes(b"synthetic debug fixture") + result = _result(("extra", 32, "int extra(){return 1;}")) + result.binary_path = binary + source = _index('# 1 "/src/a.cpp"\nint f(){return 1;}\n') + native = NativeSourceIndex(binary, {16: [_native(16, "f", "/src/a.cpp", 1)]}) + before = result.model_dump() + report = evaluate_cpp_ged( + result, source_index=source, native_index=native, requested_functions=[("f", 16)] + ) + assert report.requested == report.source_eligible == 1 + assert report.paired == 0 + assert report.decompiled_statuses == {"empty_output": 1} + assert result.model_dump() == before + with pytest.raises(ValueError, match="distinct nonnegative"): + evaluate_cpp_ged( + result, + source_index=source, + native_index=native, + requested_functions=[("f", 16), ("other", 16)], + ) + + +def test_cpp_ged_cli_smoke(tmp_path: Path) -> None: + import json + import os + import shutil + import subprocess + import sys + + from decbench.utils.function_identity import dwarf_function_identities + + compiler = shutil.which("clang++") + if compiler is None: + pytest.skip("clang++ required for CLI native debug fixture") + source, obj, binary = tmp_path / "cli.cpp", tmp_path / "cli.o", tmp_path / "cli" + preprocessed = tmp_path / "cli.ii" + source.write_text("int f(int x){return x+1;}\nint main(){return f(0);}\n") + subprocess.run( + [compiler, "-g", "-O0", "-c", str(source), "-o", str(obj)], check=True, capture_output=True + ) + subprocess.run([compiler, str(obj), "-o", str(binary)], check=True, capture_output=True) + if binary.read_bytes()[:4] != b"\x7fELF": + subprocess.run(["dsymutil", str(binary)], check=True, capture_output=True) + preprocessed.write_bytes( + subprocess.run([compiler, "-E", str(source)], check=True, capture_output=True).stdout + ) + identity = next(row for row in dwarf_function_identities(binary) if row.name == "f") + result = _result(("f", identity.address, "int sub_10(int x){return x+1;}")) + result.binary_path = binary + artifact, requested, output = ( + tmp_path / "result.json", + tmp_path / "requested.json", + tmp_path / "report.json", + ) + artifact.write_text(result.model_dump_json()) + requested.write_text(json.dumps([{"name": "f", "address": identity.address}])) + root = Path(__file__).resolve().parents[1] + env = dict(os.environ, PYTHONPATH=str(root), DECBENCH_NO_CACHE="1") + subprocess.run( + [ + sys.executable, + str(root / "scripts/evaluate_cpp_ged.py"), + str(artifact), + "--requested-functions", + str(requested), + "--source", + str(preprocessed), + "--output", + str(output), + ], + check=True, + capture_output=True, + env=env, + ) + report = json.loads(output.read_text()) + assert report["paired"] == report["metric"]["perfect_count"] == 1 + assert report["metric"]["metric_name"] == "cpp_ged_diagnostic" + assert report["official_score"] is False + + +def test_filtered_origins_are_distinct_from_absent_source() -> None: + text = '# 1 "/src/a.cpp"\n# 1 "/usr/include/target.hpp" 1 3\nint f(){return 1;}\n' + index = _index(text) + native = {16: [_native(16, "f", "/usr/include/target.hpp", 1)]} + joined = index.resolve(_result(("f", 16, "int f(){return 1;}")), native) + assert joined.evidence["f"]["status"] == "filtered_source_origin" + assert index.inputs[0]["filtered_origin_paths"] == ["/usr/include/target.hpp"] + assert index.inputs[0]["retained_origin_paths"] == [] + + +def test_native_origin_retention_is_exact_and_scoped_to_proven_unit(tmp_path: Path) -> None: + text = ( + '# 1 "/src/a.cpp"\n# 1 "/usr/include/target.hpp" 1 3\nint f(){return 1;}\n' + '# 1 "/usr/include/unrequested.hpp" 1 3\nint other(){return 2;}\n' + ) + path = tmp_path / "captured.ii" + path.write_text(text) + binary = tmp_path / "debug" + binary.write_bytes(b"synthetic debug") + result = _result(("f", 16, "int f(){return 1;}")) + result.binary_path = binary + row = _native(16, "f", "/usr/include/target.hpp", 1) + report = evaluate_cpp_ged(result, [path], native_index=NativeSourceIndex(binary, {16: [row]})) + assert report.paired == 1 + assert report.source_inputs[0]["retained_origin_paths"] == ["/usr/include/target.hpp"] + assert report.source_inputs[0]["filtered_origin_paths"] == ["/usr/include/unrequested.hpp"] + wrong = _native(16, "f", "/usr/include/target.hpp", 1, "/other/a.cpp") + rejected = evaluate_cpp_ged( + result, [path], native_index=NativeSourceIndex(binary, {16: [wrong]}) + ) + assert rejected.source_eligible == 0 + assert rejected.source_inputs[0]["retained_origin_paths"] == [] + ambiguous = evaluate_cpp_ged( + result, [path], native_index=NativeSourceIndex(binary, {16: [row, row]}) + ) + assert ambiguous.source_eligible == 0 + assert ambiguous.source_inputs[0]["retained_origin_paths"] == [] + + +def test_retention_requires_full_path_and_native_compilation_directory() -> None: + from dataclasses import replace + + text = '# 1 "src/a.cpp"\n# 10 "include/h.hpp" 1 3\nint f(){return 1;}\n' + row = NativeSource( + 16, + "f", + ("/build/src/a.cpp",), + SourceLocation("/build/include/h.hpp", 10), + "fixture", + compilation_directory="/build", + ) + index = SourceCfgIndex() + index.add(text, "local.ii", native_records=(row,)) + assert index.inputs[0]["retained_origin_paths"] == ["include/h.hpp"] + result = _result(("f", 16, "int f(){return 1;}")) + assert index.resolve(result, {16: [row]}).evidence["f"]["status"] == "matched" + index = SourceCfgIndex() + index.add( + text, + "local.ii", + native_records=(replace(row, location=SourceLocation("/other/h.hpp", 10)),), + ) + assert index.inputs[0]["retained_origin_paths"] == [] + synthetic = origin_source( + '# 1 ""\nint fake(){return 0;}\n', retain_paths=frozenset({""}) + ) + assert not synthetic.origins + + +def test_real_native_system_header_recovery(tmp_path: Path) -> None: + import shutil + import subprocess + + from decbench.utils.cfg_identity import native_sources + from decbench.utils.function_identity import dwarf_function_identities + + compiler = shutil.which("clang++") + if compiler is None: + pytest.skip("clang++ required") + include = tmp_path / "sysinclude" + include.mkdir() + header = include / "helper.hpp" + header.write_text( + "namespace n {\n__attribute__((noinline)) int helper(int x){if(x)return 3;return 4;}\n}\n" + ) + source, binary, obj, preprocessed = ( + tmp_path / n for n in ("unit.cpp", "unit", "unit.o", "unit.ii") + ) + source.write_text("#include \nint main(){return n::helper(0);}\n") + flags = [compiler, "-O0", "-g", "-isystem", str(include)] + subprocess.run([*flags, "-c", str(source), "-o", str(obj)], check=True, capture_output=True) + subprocess.run([compiler, str(obj), "-o", str(binary)], check=True, capture_output=True) + if binary.read_bytes()[:4] != b"\x7fELF": + subprocess.run(["dsymutil", str(binary)], check=True, capture_output=True) + preprocessed.write_bytes( + subprocess.run([*flags, "-E", str(source)], check=True, capture_output=True).stdout + ) + row = next(r for r in dwarf_function_identities(binary) if r.name == "helper") + native = native_sources(binary, {row.address}) + result = _result(("helper", row.address, "int sub_10(int x){if(x)return 3;return 4;}")) + result.binary_path = binary + before = SourceCfgIndex() + before.add(preprocessed.read_text(), str(preprocessed)) + assert before.resolve(result, native).evidence["helper"]["status"] == "filtered_source_origin" + report = evaluate_cpp_ged(result, [preprocessed], native_index=native) + assert report.source_eligible == report.paired == report.metric.perfect_count == 1 + assert report.source_inputs[0]["retained_origin_paths"] == [str(header)] + + +def test_retained_header_does_not_replace_primary_unit() -> None: + text = ( + '# 1 "/usr/include/prelude.hpp" 3\nint f(){return 1;}\n' + '# 1 "/src/a.cpp"\nint g(){return 2;}\n' + ) + source = origin_source(text, retain_paths=frozenset({"/usr/include/prelude.hpp"})) + assert source.primary == "/src/a.cpp" + assert source.origins[2] == SourceLocation("/usr/include/prelude.hpp", 1) + + +def test_selected_source_definition_has_exact_body_digest() -> None: + import hashlib + + body = "int f(){return 1;}" + index = _index('# 1 "/src/a.cpp"\n' + body + "\n") + row = _native(16, "f", "/src/a.cpp", 1) + evidence = index.resolve(_result(("f", 16, body)), {16: [row]}).evidence["f"] + assert evidence["definition_sha256"] == hashlib.sha256(body.encode()).hexdigest() diff --git a/tests/test_cpp_identity_backends.py b/tests/test_cpp_identity_backends.py new file mode 100644 index 0000000..bddddf2 --- /dev/null +++ b/tests/test_cpp_identity_backends.py @@ -0,0 +1,40 @@ +"""Backend-level regressions for C++ same-name function preservation.""" + +from decbench.decompilers.raw.kuna_raw import RawKunaDecompiler +from decbench.utils.function_identity import insert_function + + +def test_kuna_records_are_keyed_by_address_not_name() -> None: + payload = { + "functions": [ + {"name": "same", "address": 0x1000, "code": "int same(void) { return 1; }"}, + {"name": "same", "address": 0x2000, "code": "int same(void) { return 2; }"}, + ] + } + + records = RawKunaDecompiler._records_by_address(payload) + + assert set(records) == {0x1000, 0x2000} + assert records[0x1000]["name"] == "same" + assert records[0x2000]["name"] == "same" + assert records[0x1000]["code"] != records[0x2000]["code"] + + +def test_kuna_same_name_records_can_both_enter_decompilation_result() -> None: + backend = RawKunaDecompiler() + payload = { + "functions": [ + {"name": "same", "address": 0x1000, "code": "int same(void) { return 1; }"}, + {"name": "same", "address": 0x2000, "code": "int same(void) { return 2; }"}, + ] + } + records = backend._records_by_address(payload) + stored = {} + + for address, record in sorted(records.items()): + function = backend._build_function(record, str(record["name"]), address) + assert function is not None + insert_function(stored, function) + + assert set(stored) == {"same@0x1000", "same@0x2000"} + assert {function.address for function in stored.values()} == {0x1000, 0x2000} diff --git a/tests/test_cpp_identity_deep_validation.py b/tests/test_cpp_identity_deep_validation.py new file mode 100644 index 0000000..b82cbc7 --- /dev/null +++ b/tests/test_cpp_identity_deep_validation.py @@ -0,0 +1,242 @@ +"""Extra validation for the C++ function-identity contribution candidate.""" + +from __future__ import annotations + +import importlib.util +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +from decbench.models.decompilation import ( + DecompilationResult, + DecompilerMetadata, + FunctionDecompilation, +) +from decbench.models.function_data import BinaryGroup, FunctionData, FunctionRecord +from decbench.scoring.scoreboard import build_scoreboard_from_function_data +from decbench.utils.function_identity import ( + dwarf_function_identities, + identities_by_name, +) + +CPP = r""" +namespace alpha { __attribute__((noinline)) int collide(int x) { return x + 1; } } +namespace beta { __attribute__((noinline)) int collide(int x) { return x + 2; } } +__attribute__((noinline)) int collide(int x) { return x + 3; } +__attribute__((noinline)) int collide(double x) { return static_cast(x) + 4; } +struct A { __attribute__((noinline)) int Next(int x) const; }; +struct B { __attribute__((noinline)) int Next(int x) const; }; +int A::Next(int x) const { return x + 5; } +int B::Next(int x) const { return x + 6; } +volatile int seed = 1; +int main() { + A a; B b; + int x = seed; + return alpha::collide(x) + beta::collide(x) + collide(x) + + collide(static_cast(x)) + a.Next(x) + b.Next(x); +} +""" + + +def _driver(): + path = Path(__file__).resolve().parents[1] / "scripts" / "run_benchmark.py" + spec = importlib.util.spec_from_file_location("deep_identity_driver", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _compile(tmp_path: Path, mode: str) -> Path: + flags = { + "O0": ["-O0"], + "O2": ["-O2"], + "O2-noinline": ["-O2", "-fno-inline"], + }[mode] + source = tmp_path / f"fixture-{mode}.cpp" + binary = tmp_path / f"fixture-{mode}" + source.write_text(CPP) + subprocess.run( + ["g++", "-std=c++17", *flags, "-g", "-fno-builtin", str(source), "-o", str(binary)], + check=True, + capture_output=True, + text=True, + ) + return binary + + +def _fd(name: str, address: int) -> FunctionDecompilation: + return FunctionDecompilation( + name=f"FUN_{address:x}", + address=address, + decompiled_code=f"int FUN_{address:x}(void) {{ return 0; }}", + ) + + +@pytest.mark.skipif(shutil.which("g++") is None, reason="g++ required") +@pytest.mark.parametrize("mode", ["O0", "O2", "O2-noinline"]) +def test_real_cpp_identity_survives_optimization_modes(tmp_path: Path, mode: str) -> None: + binary = _compile(tmp_path, mode) + grouped = identities_by_name(dwarf_function_identities(binary)) + + collide = grouped.get("collide", []) + next_rows = grouped.get("Next", []) + assert len({row.address for row in collide}) == 4 + assert len({row.address for row in next_rows}) == 2 + + selected = [*collide, *next_rows] + addr2name = {row.address: row.name for row in selected} + result = DecompilationResult( + binary_path=binary, + binary_name=binary.stem, + decompiler=DecompilerMetadata(decompiler_name="fixture"), + functions={f"raw-{i}": _fd(row.name, row.address) for i, row in enumerate(selected)}, + ) + _driver()._relabel_to_dwarf(result, addr2name, binary) + + expected = {f"{name}@0x{address:x}" for address, name in addr2name.items()} + assert set(result.functions) == expected + assert {fd.address for fd in result.functions.values()} == set(addr2name) + assert result.decompiler.failed_functions == [] + + +def test_identity_storage_is_insertion_order_deterministic() -> None: + from decbench.utils.function_identity import insert_function + + rows = [ + FunctionDecompilation(name="same", address=0x1000, decompiled_code="a"), + FunctionDecompilation(name="same", address=0x2000, decompiled_code="b"), + FunctionDecompilation(name="same", address=0x3000, decompiled_code="c"), + ] + forward: dict[str, FunctionDecompilation] = {} + reverse: dict[str, FunctionDecompilation] = {} + for row in rows: + insert_function(forward, row.model_copy(deep=True)) + for row in reversed(rows): + insert_function(reverse, row.model_copy(deep=True)) + + assert set(forward) == set(reverse) == { + "same@0x1000", + "same@0x2000", + "same@0x3000", + } + assert {key: value.address for key, value in forward.items()} == { + key: value.address for key, value in reverse.items() + } + + +def test_same_address_different_name_aliases_do_not_collapse() -> None: + from decbench.utils.function_identity import insert_function + + functions: dict[str, FunctionDecompilation] = {} + insert_function( + functions, + FunctionDecompilation(name="foo", address=0x1000, decompiled_code="a"), + ) + insert_function( + functions, + FunctionDecompilation(name="bar", address=0x1000, decompiled_code="b"), + ) + assert set(functions) == {"foo", "bar"} + assert {fd.name for fd in functions.values()} == {"foo", "bar"} + + +def test_pydantic_json_roundtrip_preserves_collision_identity(tmp_path: Path) -> None: + result = DecompilationResult( + binary_path=tmp_path / "a.out", + binary_name="a.out", + decompiler=DecompilerMetadata(decompiler_name="fixture"), + ) + result.add_function(FunctionDecompilation(name="foo", address=0x1000, decompiled_code="a")) + result.add_function(FunctionDecompilation(name="foo", address=0x2000, decompiled_code="b")) + + restored = DecompilationResult.model_validate_json(result.model_dump_json()) + assert set(restored.functions) == {"foo@0x1000", "foo@0x2000"} + assert [restored.functions[key].name for key in sorted(restored.functions)] == ["foo", "foo"] + assert {fd.address for fd in restored.functions.values()} == {0x1000, 0x2000} + + +def test_legacy_plain_name_json_still_loads() -> None: + legacy = { + "binary_path": "legacy.bin", + "binary_name": "legacy", + "decompiler": {"decompiler_name": "legacy"}, + "functions": { + "foo": { + "name": "foo", + "address": 4096, + "decompiled_code": "int foo(void) { return 0; }", + } + }, + } + restored = DecompilationResult.model_validate_json(json.dumps(legacy)) + assert set(restored.functions) == {"foo"} + assert restored.functions["foo"].name == "foo" + assert restored.functions["foo"].address == 0x1000 + + +def test_toml_writer_keeps_distinct_collision_keys(tmp_path: Path) -> None: + import toml + + result = DecompilationResult( + binary_path=tmp_path / "a.out", + binary_name="a.out", + decompiler=DecompilerMetadata(decompiler_name="fixture"), + ) + result.add_function(FunctionDecompilation(name="foo", address=0x1000, decompiled_code="a")) + result.add_function(FunctionDecompilation(name="foo", address=0x2000, decompiled_code="b")) + out = tmp_path / "result.toml" + result.to_toml(out) + data = toml.load(out) + + assert "functions.foo@0x1000" in data + assert "functions.foo@0x2000" in data + assert data["functions.foo@0x1000"]["address"] == "0x1000" + assert data["functions.foo@0x2000"]["address"] == "0x2000" + + +def test_ged_abstention_does_not_boost_shared_scoreboard_denominator() -> None: + # f3 has GED only for decompiler B. A's abstention must count as a miss because + # GED is measurable for someone. f4 has GED for nobody and is uniformly excluded. + records = [ + FunctionRecord( + function="f1", + values={"A": {"ged": 0.0}, "B": {"ged": 0.0}}, + perfects={"A": {"ged": True}, "B": {"ged": True}}, + ), + FunctionRecord( + function="f2", + values={"A": {"ged": 1.0}, "B": {"ged": 1.0}}, + perfects={"A": {"ged": False}, "B": {"ged": False}}, + ), + FunctionRecord( + function="ambiguous-measurable", + values={"A": {}, "B": {"ged": 1.0}}, + perfects={"A": {}, "B": {"ged": False}}, + ), + FunctionRecord( + function="ambiguous-unmeasurable", + values={"A": {}, "B": {}}, + perfects={"A": {}, "B": {}}, + ), + ] + fd = FunctionData( + decompilers=["A", "B"], + metrics=["ged"], + groups=[BinaryGroup(project="p", opt_level="O2", binary="b", functions=records)], + ) + scoreboard = build_scoreboard_from_function_data(fd) + a = scoreboard.decompiler_scores["A"].metric_scores["ged"] + b = scoreboard.decompiler_scores["B"].metric_scores["ged"] + + assert a.total_count == b.total_count == 3 + assert a.perfect_count == b.perfect_count == 1 + assert a.perfect_percentage == pytest.approx(100 / 3) + assert b.perfect_percentage == pytest.approx(100 / 3) + # Means intentionally summarize only measured values; headline perfect-% uses + # the shared measurable universe and therefore cannot improve by abstaining. + assert a.mean == pytest.approx(0.5) + assert b.mean == pytest.approx(2 / 3) diff --git a/tests/test_cpp_identity_end_to_end.py b/tests/test_cpp_identity_end_to_end.py new file mode 100644 index 0000000..5735157 --- /dev/null +++ b/tests/test_cpp_identity_end_to_end.py @@ -0,0 +1,246 @@ +"""End-to-end regressions for collision-qualified C++ function identity.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +from decbench.metrics.byte_match import ByteMatchMetric +from decbench.models.decompilation import ( + DecompilationResult, + DecompilerMetadata, + FunctionDecompilation, +) +from decbench.models.metrics import MetricValue +from decbench.scoring import report_extras +from decbench.utils import results_tree, source_extract +from decbench.utils.function_identity import parse_function_storage_key + + +def _result(tmp_path: Path) -> DecompilationResult: + result = DecompilationResult( + binary_path=tmp_path / "fixture", + binary_name="fixture", + decompiler=DecompilerMetadata(decompiler_name="fixture-dec"), + ) + result.add_function( + FunctionDecompilation( + name="same", + address=0x1000, + decompiled_code="int same(int x) { return x + 1; }", + ) + ) + result.add_function( + FunctionDecompilation( + name="same", + address=0x2000, + decompiled_code="double same(double x) { return x + 2.0; }", + ) + ) + result.add_function( + FunctionDecompilation( + name="unique", + address=0x3000, + decompiled_code="int unique(void) { return 3; }", + ) + ) + return result + + +def _load_reeval_module(): + path = Path(__file__).resolve().parents[1] / "scripts" / "reeval_bytematch.py" + spec = importlib.util.spec_from_file_location("reeval_identity_fixture", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_parse_function_storage_key_preserves_legacy_and_collision_forms() -> None: + assert parse_function_storage_key("unique") == ("unique", None) + assert parse_function_storage_key("same@0x1a2b") == ("same", 0x1A2B) + + +def test_c_artifact_round_trip_preserves_collision_storage_keys(tmp_path: Path) -> None: + result = _result(tmp_path) + artifact = tmp_path / "fixture.c" + result.to_c_file(artifact) + + blocks = results_tree.split_functions(artifact) + assert set(blocks) == {"same@0x1000", "same@0x2000", "unique"} + assert blocks["same@0x1000"][0] == 0x1000 + assert blocks["same@0x2000"][0] == 0x2000 + assert blocks["unique"][0] == 0x3000 + + text = artifact.read_text() + assert "// Function: same@0x1000 @ 0x1000" in text + assert "// Function: same@0x2000 @ 0x2000" in text + assert "// Function: unique @ 0x3000" in text + + +def test_sample_set_reader_keeps_collision_storage_keys(tmp_path: Path) -> None: + result = _result(tmp_path) + decompiled = tmp_path / "O2" / "proj" / "decompiled" + decompiled.mkdir(parents=True) + result.to_c_file(decompiled / "fixture-dec_fixture.c") + + reader = report_extras.SampleSetReader(tmp_path) + blocks = reader.decompiled("O2", "proj", "fixture", "fixture-dec") + assert set(blocks) == {"same@0x1000", "same@0x2000", "unique"} + + +def test_report_source_lookup_uses_semantic_name_and_address( + tmp_path: Path, monkeypatch +) -> None: + result = _result(tmp_path) + nested = {"proj": {"O2": {"fixture": {"fixture-dec": result}}}} + seen: list[tuple[Path, str, int | None]] = [] + + def fake_source( + binary_path: Path | None, func_name: str, func_address: int | None = None + ) -> tuple[str | None, str]: + assert binary_path is not None + seen.append((Path(binary_path), func_name, func_address)) + return "source", "source" + + monkeypatch.setattr(source_extract, "function_source_ex", fake_source) + source, status = report_extras._lookup_source_ex( + nested, "proj", "O2", "fixture", "same@0x1000" + ) + + assert (source, status) == ("source", "source") + assert seen == [(result.binary_path, "same", 0x1000)] + + +def test_report_source_lookup_parses_key_when_backend_result_is_missing( + tmp_path: Path, monkeypatch +) -> None: + result = _result(tmp_path) + result.functions.pop("same@0x1000") + nested = {"proj": {"O2": {"fixture": {"fixture-dec": result}}}} + seen: list[tuple[str, int | None]] = [] + + def fake_source( + _binary_path: Path | None, func_name: str, func_address: int | None = None + ) -> tuple[str | None, str]: + seen.append((func_name, func_address)) + return "source", "source" + + monkeypatch.setattr(source_extract, "function_source_ex", fake_source) + report_extras._lookup_source_ex(nested, "proj", "O2", "fixture", "same@0x1000") + assert seen == [("same", 0x1000)] + + +def test_byte_match_context_uses_only_unambiguous_semantic_names( + tmp_path: Path, monkeypatch +) -> None: + result = _result(tmp_path) + captured: dict[str, str] = {} + + def fake_derive(function_codes: dict[str, str]) -> dict[str, str]: + captured.update(function_codes) + return {"unique": "int unique(void);"} + + monkeypatch.setattr("decbench.metrics.fixup.derive_context_decls", fake_derive) + metric = ByteMatchMetric() + + def fake_compute(*_args, **_kwargs) -> MetricValue: + return MetricValue(value=1.0) + + monkeypatch.setattr(metric, "compute_for_function", fake_compute) + metric.compute_for_binary(result) + + assert set(captured) == {"unique"} + assert "same@0x1000" not in captured + assert "same@0x2000" not in captured + assert "same" not in captured + + +def test_disk_bytematch_reeval_uses_semantic_names_but_keeps_storage_keys( + tmp_path: Path, monkeypatch +) -> None: + result = _result(tmp_path) + artifact = tmp_path / "fixture.c" + result.to_c_file(artifact) + seen_names: list[str] = [] + captured_context: dict[str, str] = {} + + class FakeMetric: + def compute_for_function(self, fd, **_kwargs): + seen_names.append(fd.name) + return MetricValue(value=1.0, metadata={"compilable": True}) + + def fake_derive(function_codes: dict[str, str]) -> dict[str, str]: + captured_context.update(function_codes) + return {} + + monkeypatch.setattr("decbench.metrics.byte_match.ByteMatchMetric", FakeMetric) + monkeypatch.setattr("decbench.metrics.fixup.derive_context_decls", fake_derive) + + module = _load_reeval_module() + _key, values = module.eval_one( + ("O2", "proj", "fixture", "fixture-dec", str(result.binary_path), str(artifact)) + ) + + assert set(values) == {"same@0x1000", "same@0x2000", "unique"} + assert seen_names.count("same") == 2 + assert seen_names.count("unique") == 1 + assert set(captured_context) == {"unique"} + + +def test_legacy_c_artifact_duplicate_names_are_recovered_by_address(tmp_path: Path) -> None: + artifact = tmp_path / "legacy.c" + artifact.write_text( + "// Function: same @ 0x1000\n" + "int same(int x) { return x + 1; }\n\n" + "// Function: same @ 0x2000\n" + "double same(double x) { return x + 2.0; }\n" + ) + + blocks = results_tree.split_functions(artifact) + assert set(blocks) == {"same@0x1000", "same@0x2000"} + assert blocks["same@0x1000"][0] == 0x1000 + assert blocks["same@0x2000"][0] == 0x2000 + + +def test_c_artifact_marker_supports_spaced_cpp_names(tmp_path: Path) -> None: + result = DecompilationResult( + binary_path=tmp_path / "fixture", + binary_name="fixture", + decompiler=DecompilerMetadata(decompiler_name="fixture-dec"), + ) + result.add_function( + FunctionDecompilation( + name="operator new", + address=0x4000, + decompiled_code="void *operator_new(unsigned long n) { return (void *)n; }", + ) + ) + artifact = tmp_path / "operator.c" + result.to_c_file(artifact) + + blocks = results_tree.split_functions(artifact) + assert set(blocks) == {"operator new"} + assert blocks["operator new"][0] == 0x4000 + + +def test_report_source_lookup_keeps_plain_unique_name_only( + tmp_path: Path, monkeypatch +) -> None: + result = _result(tmp_path) + nested = {"proj": {"O2": {"fixture": {"fixture-dec": result}}}} + seen: list[tuple[str, int | None]] = [] + + def fake_source( + _binary_path: Path | None, func_name: str, func_address: int | None = None + ) -> tuple[str | None, str]: + seen.append((func_name, func_address)) + return "source", "source" + + monkeypatch.setattr(source_extract, "function_source_ex", fake_source) + source, status = report_extras._lookup_source_ex( + nested, "proj", "O2", "fixture", "unique" + ) + + assert (source, status) == ("source", "source") + assert seen == [("unique", None)] diff --git a/tests/test_cpp_identity_icf.py b/tests/test_cpp_identity_icf.py new file mode 100644 index 0000000..ea2714f --- /dev/null +++ b/tests/test_cpp_identity_icf.py @@ -0,0 +1,138 @@ +"""Validate linker-ICF sentinel handling without rejecting real address-zero code.""" + +from __future__ import annotations + +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest +from elftools.elf.elffile import ELFFile + +from decbench.utils import binfmt +from decbench.utils.function_identity import dwarf_function_identities + +pytestmark = pytest.mark.skipif( + sys.platform != "linux" or shutil.which("g++") is None or shutil.which("ld.lld") is None, + reason="ELF-producing GNU g++ and ld.lld required", +) + +ICF_SRC = r""" +extern "C" __attribute__((noinline)) int foo(int x) { return x + 1; } +extern "C" __attribute__((noinline)) int bar(int x) { return x + 1; } +int (*volatile fp_foo)(int) = &foo; +int (*volatile fp_bar)(int) = &bar; +volatile int seed = 1; +int main() { int x = seed; return fp_foo(x) + fp_bar(x); } +""" + +ZERO_SRC = r""" +__attribute__((noinline)) int zero(int x) { return x + 1; } +""" + + +def _build_icf(directory: Path) -> Path: + source = directory / "icf.cpp" + binary = directory / "icf" + source.write_text(ICF_SRC) + subprocess.run( + [ + "g++", + "-std=c++17", + "-O2", + "-g", + "-fno-ipa-icf", + "-ffunction-sections", + "-fuse-ld=lld", + "-Wl,--icf=all", + str(source), + "-o", + str(binary), + ], + check=True, + capture_output=True, + text=True, + ) + return binary + + +def _symbol_addresses(binary: Path, names: set[str]) -> dict[str, int]: + with binary.open("rb") as stream: + symtab = ELFFile(stream).get_section_by_name(".symtab") + assert symtab is not None + return { + symbol.name: int(symbol["st_value"]) + for symbol in symtab.iter_symbols() + if symbol.name in names + } + + +def test_linker_icf_sentinel_zero_is_not_a_concrete_binary_target(tmp_path: Path) -> None: + selections: list[str] = [] + for index in range(2): + directory = tmp_path / str(index) + directory.mkdir() + binary = _build_icf(directory) + + symbols = _symbol_addresses(binary, {"foo", "bar"}) + assert set(symbols) == {"foo", "bar"} + assert symbols["foo"] == symbols["bar"] + shared = symbols["foo"] + assert shared != 0 + assert binfmt.executable_address_status(binary, 0) is False + assert binfmt.dwarf_low_pc_is_concrete(binary, 0) is False + assert binfmt.executable_address_status(binary, shared) is True + assert binfmt.dwarf_low_pc_is_concrete(binary, shared) is True + + identities = [ + row for row in dwarf_function_identities(binary) if row.name in {"foo", "bar"} + ] + assert len(identities) == 1 + assert identities[0].address == shared + assert identities[0].name in {"foo", "bar"} + + owners = binfmt.source_function_owners( + binary, + {"icf"}, + follow_abstract_origin=True, + ) + alias_targets = { + address: name for address, (name, _stem) in owners.items() if name in {"foo", "bar"} + } + assert alias_targets == {shared: identities[0].name} + selections.append(identities[0].name) + + assert selections[0] == selections[1] + + +def test_real_executable_address_zero_is_preserved(tmp_path: Path) -> None: + source = tmp_path / "zero.c" + obj = tmp_path / "zero.o" + source.write_text(ZERO_SRC) + subprocess.run( + [ + "gcc", + "-O0", + "-g", + "-ffreestanding", + "-ffunction-sections", + "-fno-asynchronous-unwind-tables", + "-fno-unwind-tables", + "-c", + str(source), + "-o", + str(obj), + ], + check=True, + capture_output=True, + text=True, + ) + + # ET_REL keeps executable section addresses and symbol values section-relative. + # pyelftools does not apply its DWARF relocations here, so this fixture tests + # the concrete-address policy directly rather than DWARF extraction itself. + symbols = _symbol_addresses(obj, {"zero"}) + assert symbols == {"zero": 0} + assert binfmt.executable_address_status(obj, 0) is True + assert binfmt.dwarf_low_pc_is_concrete(obj, 0) is True diff --git a/tests/test_cpp_identity_optimized_dwarf.py b/tests/test_cpp_identity_optimized_dwarf.py new file mode 100644 index 0000000..09a7fea --- /dev/null +++ b/tests/test_cpp_identity_optimized_dwarf.py @@ -0,0 +1,160 @@ +"""Validate function identity with real optimized/inlined C++ DWARF.""" + +from __future__ import annotations + +import importlib.util +import shutil +import subprocess +import tempfile +from pathlib import Path + +import pytest +from elftools.elf.elffile import ELFFile + +from decbench.metrics.type_match import extract_ground_truth_types_by_address +from decbench.models.decompilation import ( + DecompilationResult, + DecompilerMetadata, + FunctionDecompilation, +) +from decbench.utils.function_identity import dwarf_function_identities, identities_by_name + + +def _emits_elf() -> bool: + """Whether the local ``g++`` links ELF, which pyelftools needs to read DWARF. + + Presence of ``g++`` is not enough: on macOS it is a clang driver that emits + Mach-O, so every DWARF assertion in this module raised ``ELFError`` instead + of skipping. Probe the linker output once and skip honestly off Linux. + """ + if shutil.which("g++") is None: + return False + with tempfile.TemporaryDirectory() as directory: + source = Path(directory) / "probe.cpp" + binary = Path(directory) / "probe" + source.write_text("int main() { return 0; }\n") + try: + subprocess.run( + ["g++", "-g", str(source), "-o", str(binary)], + check=True, + capture_output=True, + ) + except (OSError, subprocess.CalledProcessError): + return False + return binary.read_bytes()[:4] == b"\x7fELF" + + +pytestmark = pytest.mark.skipif( + not _emits_elf(), + reason="an ELF-producing g++ is required (pyelftools cannot read Mach-O)", +) + +SRC = r""" +namespace alpha { int same(int x) { return x + 1; } } +namespace beta { int same(int x) { return x + 2; } } +inline int inline_helper(int x) { return x * 7 + 3; } +volatile int seed = 1; +int (*volatile alpha_ptr)(int) = &alpha::same; +int (*volatile beta_ptr)(int) = &beta::same; +int main() { + int x = seed; + return alpha_ptr(x) + beta_ptr(x) + inline_helper(x); +} +""" + + +def _driver(): + path = Path(__file__).resolve().parents[1] / "scripts" / "run_benchmark.py" + spec = importlib.util.spec_from_file_location("optimized_identity_driver", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_o2_inlined_dwarf_does_not_confuse_concrete_same_name_identity(tmp_path: Path) -> None: + source = tmp_path / "optimized.cpp" + binary = tmp_path / "optimized" + source.write_text(SRC) + subprocess.run( + ["g++", "-std=c++17", "-O2", "-g", "-fno-builtin", str(source), "-o", str(binary)], + check=True, + capture_output=True, + text=True, + ) + + with binary.open("rb") as stream: + dwarf = ELFFile(stream).get_dwarf_info() + inlined = 0 + referenced = 0 + for cu in dwarf.iter_CUs(): + for die in cu.iter_DIEs(): + if die.tag == "DW_TAG_inlined_subroutine": + inlined += 1 + if ( + "DW_AT_abstract_origin" in die.attributes + or "DW_AT_specification" in die.attributes + ): + referenced += 1 + assert inlined > 0 + assert referenced > 0 + + grouped = identities_by_name(dwarf_function_identities(binary)) + rows = grouped.get("same", []) + assert len({row.address for row in rows}) == 2 + addr2name = {row.address: row.name for row in rows} + + result = DecompilationResult( + binary_path=binary, + binary_name=binary.stem, + decompiler=DecompilerMetadata(decompiler_name="optimized-fixture"), + functions={ + f"raw-{index}": FunctionDecompilation( + name=f"sub_{row.address:x}", + address=row.address, + decompiled_code="int sub(void) { return 0; }", + ) + for index, row in enumerate(rows) + }, + ) + _driver()._relabel_to_dwarf(result, addr2name, binary) + + assert set(result.functions) == {f"same@0x{address:x}" for address in addr2name} + assert {fd.address for fd in result.functions.values()} == set(addr2name) + + +def test_namespace_scoped_overloads_reach_the_address_keyed_ground_truth(tmp_path: Path) -> None: + """TypeMatch's address map must see subprograms nested in a C++ scope. + + ``alpha::same`` and ``beta::same`` are ``DW_TAG_subprogram`` DIEs inside a + ``DW_TAG_namespace``, so a walk over the compile unit's *direct children* + never reaches them. Identity extraction already walks the whole DIE tree; if + the ground-truth walk is shallower, an overload is correctly named + ``same@0x...`` and then has no entry at its address, so it drops out of the + metric silently instead of scoring. + + The assertion is on the address being *present*: at O2 a parameter may be + optimized away, and an empty-but-present list is the authoritative answer + this extractor is designed to give. + """ + source = tmp_path / "scoped.cpp" + binary = tmp_path / "scoped" + source.write_text(SRC) + subprocess.run( + ["g++", "-std=c++17", "-O2", "-g", "-fno-builtin", str(source), "-o", str(binary)], + check=True, + capture_output=True, + text=True, + ) + + rows = identities_by_name(dwarf_function_identities(binary)).get("same", []) + addresses = {row.address for row in rows} + assert len(addresses) == 2, "fixture must produce two distinct concrete overloads" + + by_address = extract_ground_truth_types_by_address(binary) + + missing = addresses - set(by_address) + assert not missing, ( + "namespace-scoped overloads are absent from the address-keyed ground " + f"truth: {sorted(hex(address) for address in missing)}" + ) diff --git a/tests/test_cpp_identity_progress.py b/tests/test_cpp_identity_progress.py new file mode 100644 index 0000000..ab1fb45 --- /dev/null +++ b/tests/test_cpp_identity_progress.py @@ -0,0 +1,53 @@ +"""Validate partial progress snapshots preserve collision-safe identities.""" + +from __future__ import annotations + +import pickle +from pathlib import Path + +from decbench.decompilers.raw.common import dump_progress +from decbench.models.decompilation import ( + DecompilationResult, + DecompilerMetadata, + FunctionDecompilation, +) + + +def test_partial_progress_pickle_preserves_collision_keys_and_semantics(tmp_path: Path) -> None: + result = DecompilationResult( + binary_path=tmp_path / "fixture", + binary_name="fixture", + decompiler=DecompilerMetadata( + decompiler_name="progress-fixture", + failed_functions=["same@0x3000"], + extra={"partial": True, "function_identity": "dwarf-low-pc"}, + ), + ) + result.add_function( + FunctionDecompilation( + name="same", + address=0x1000, + decompiled_code="int a(void) { return 1; }", + ) + ) + result.add_function( + FunctionDecompilation( + name="same", + address=0x2000, + decompiled_code="int b(void) { return 2; }", + ) + ) + + progress = tmp_path / "progress.pkl" + dump_progress(progress, result) + assert progress.is_file() + assert not progress.with_suffix(".pkl.tmp").exists() + + restored = pickle.loads(progress.read_bytes()) + assert isinstance(restored, DecompilationResult) + assert set(restored.functions) == {"same@0x1000", "same@0x2000"} + assert {fd.address for fd in restored.functions.values()} == {0x1000, 0x2000} + assert {fd.name for fd in restored.functions.values()} == {"same"} + assert restored.decompiler.failed_functions == ["same@0x3000"] + assert restored.decompiler.extra["partial"] is True + assert restored.decompiler.extra["function_identity"] == "dwarf-low-pc" diff --git a/tests/test_cpp_identity_slice_invariance.py b/tests/test_cpp_identity_slice_invariance.py new file mode 100644 index 0000000..ccea6a1 --- /dev/null +++ b/tests/test_cpp_identity_slice_invariance.py @@ -0,0 +1,224 @@ +"""Slice/eval-kit regressions for canonical C++ function identity.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +import networkx as nx + +from decbench.evalkit import resolve +from decbench.metrics.base import Metric +from decbench.metrics.byte_match import ByteMatchMetric +from decbench.metrics.type_match import TypeMatchMetric +from decbench.models.decompilation import ( + DecompilationResult, + DecompilerMetadata, + FunctionDecompilation, + VariableInfo, +) +from decbench.models.metrics import MetricValue +from decbench.utils import binfmt + + +def _load_driver(): + path = Path(__file__).resolve().parents[1] / "scripts" / "run_benchmark.py" + spec = importlib.util.spec_from_file_location("decbench_run_benchmark_slice_test", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _fd(name: str, address: int) -> FunctionDecompilation: + return FunctionDecompilation( + name=name, + address=address, + decompiled_code=f"int {name}(void) {{ return 1; }}", + ) + + +def test_sampleset_exact_key_selects_one_overload_but_keeps_binary_wide_identity( + monkeypatch, +): + driver = _load_driver() + from decbench.decompilers.raw import common + + full = {0x1000: "same", 0x2000: "same", 0x3000: "unique"} + selected = driver._select_manifest_targets(full, {"same@0x2000"}) + assert selected == {0x2000: "same"} + assert driver._select_manifest_targets(full, {"same"}) == { + 0x1000: "same", + 0x2000: "same", + } + + result = DecompilationResult( + binary_path=Path("stripped.bin"), + binary_name="fixture", + decompiler=DecompilerMetadata(decompiler_name="fixture"), + functions={"raw": _fd("sub_2000", 0x2000)}, + ) + monkeypatch.setattr(common, "elf_min_vaddr", lambda _path: 0) + driver._relabel_to_dwarf( + result, + selected, + Path("unstripped.bin"), + identity_addr2name=full, + ) + + assert set(result.functions) == {"same@0x2000"} + assert result.decompiler.failed_functions == [] + + +class _CfgMetric(Metric): + name = "slice_cfg_identity" + requires_source_cfg = True + requires_decompiled_cfg = True + + def compute_for_function(self, decompiled, source_cfg=None, decompiled_cfg=None, **kwargs): + assert source_cfg is not None and decompiled_cfg is not None + return MetricValue(value=1.0) + + +def _graph() -> nx.DiGraph: + graph = nx.DiGraph() + graph.add_node(0) + return graph + + +def test_qualified_singleton_does_not_fall_back_to_plain_cfg(tmp_path: Path): + result = DecompilationResult( + binary_path=tmp_path / "fixture", + binary_name="fixture", + decompiler=DecompilerMetadata(decompiler_name="fixture"), + functions={"same@0x1000": _fd("same", 0x1000)}, + ) + scored = _CfgMetric().compute_for_binary( + result, + source_cfgs={"same": _graph()}, + decompiled_cfgs={"same": _graph()}, + ) + assert scored.function_results == {} + + +def test_qualified_singleton_does_not_borrow_name_keyed_type_ground_truth( + tmp_path: Path, +): + fd = FunctionDecompilation( + name="same", + address=0x1000, + decompiled_code="int same(int x) { return x; }", + variables=[VariableInfo(name="x", type="int", kind="arg", arg_index=0)], + ) + result = DecompilationResult( + binary_path=tmp_path / "fixture", + binary_name="fixture", + decompiler=DecompilerMetadata(decompiler_name="fixture"), + functions={"same@0x1000": fd}, + ) + gt = [ + { + "name": "x", + "type": ["int"], + "rbp_offset": [], + "size": 4, + "is_arg": True, + "arg_index": 0, + } + ] + metric = TypeMatchMetric() + key = str(result.binary_path) + metric._ground_truth_cache[key] = {"same": {0x2000: gt}} + metric._ground_truth_address_cache[key] = {0x2000: gt} + + scored = metric.compute_for_binary(result) + assert scored.function_results == {} + + metric._ground_truth_address_cache[key] = {0x1000: []} + scored = metric.compute_for_binary(result) + assert scored.function_results == {} + + +def test_qualified_singleton_is_not_used_as_bytematch_context(tmp_path: Path, monkeypatch): + qualified = _fd("same", 0x1000) + unique = _fd("unique", 0x3000) + result = DecompilationResult( + binary_path=tmp_path / "missing-binary", + binary_name="fixture", + decompiler=DecompilerMetadata(decompiler_name="fixture"), + functions={"same@0x1000": qualified, "unique": unique}, + ) + captured: dict[str, str] = {} + + def fake_derive(function_codes: dict[str, str]) -> dict[str, str]: + captured.update(function_codes) + return {} + + metric = ByteMatchMetric() + monkeypatch.setattr("decbench.metrics.fixup.derive_context_decls", fake_derive) + monkeypatch.setattr( + metric, + "compute_for_function", + lambda *_args, **_kwargs: MetricValue(value=1.0), + ) + metric.compute_for_binary(result) + assert set(captured) == {"unique"} + + +def test_evalkit_resolves_canonical_manifest_key_exactly(monkeypatch): + mapping = {0x1000: "same", 0x2000: "same", 0x3000: "unique"} + monkeypatch.setattr(resolve, "_dwarf_addr_to_name", lambda _binary, _stems: mapping) + + binary = Path("fixture") + assert resolve.name_to_addr(binary, {"same@0x2000"}) == {"same@0x2000": 0x2000} + assert resolve.name_to_addr(binary, {"same"}) == {"same": 0x1000} + assert resolve.name_to_addr(binary, {"same@0x9999"}) == {} + + lookup = resolve.AddrLookup({0x401000: "same"}, min_vaddr=0x400000) + assert lookup.resolve(0x1000) == (0x401000, "same") + assert lookup.name_for(0x1000) == "same" + + +def test_dwarf_function_range_prefers_exact_address(monkeypatch): + class Attr: + def __init__(self, value, form="DW_FORM_data4"): + self.value = value + self.form = form + + class Die: + tag = "DW_TAG_subprogram" + + def __init__(self, address: int, size: int): + self.attributes = { + "DW_AT_name": Attr(b"same"), + "DW_AT_low_pc": Attr(address, "DW_FORM_addr"), + "DW_AT_high_pc": Attr(size, "DW_FORM_data4"), + } + + class CU: + def iter_DIEs(self): + return iter([Die(0x1000, 0x10), Die(0x2000, 0x20)]) + + class DW: + def iter_CUs(self): + return iter([CU()]) + + monkeypatch.setattr(binfmt, "dwarf_info", lambda _path: DW()) + monkeypatch.setattr( + binfmt, + "dwarf_low_pc_is_concrete", + lambda _path, _address: True, + ) + monkeypatch.setattr( + binfmt, + "die_str_attr", + lambda die, _name: die.attributes["DW_AT_name"].value.decode(), + ) + assert binfmt._dwarf_function_range(Path("fixture"), "same", 0x2000) == ( + 0x2000, + 0x2020, + ) + assert binfmt._dwarf_function_range(Path("fixture"), "same") == ( + 0x1000, + 0x1010, + ) diff --git a/tests/test_cpp_identity_thumb.py b/tests/test_cpp_identity_thumb.py new file mode 100644 index 0000000..53797d9 --- /dev/null +++ b/tests/test_cpp_identity_thumb.py @@ -0,0 +1,118 @@ +"""ARM/Thumb canonical C++ identity validation.""" + +from __future__ import annotations + +import importlib.util +import shutil +import subprocess +from pathlib import Path + +import pytest +from elftools.elf.elffile import ELFFile + +from decbench.models.decompilation import ( + DecompilationResult, + DecompilerMetadata, + FunctionDecompilation, +) +from decbench.utils.function_identity import dwarf_function_identities + +pytestmark = pytest.mark.skipif( + shutil.which("arm-none-eabi-g++") is None, + reason="arm-none-eabi-g++ required", +) + +SRC = r""" +namespace alpha { __attribute__((noinline)) int same(int x) { return x + 1; } } +namespace beta { __attribute__((noinline)) int same(int x) { return x + 2; } } +extern "C" int thumb_entry(void) { + volatile int x = 1; + return alpha::same(x) + beta::same(x); +} +""" + + +def _driver(): + path = Path(__file__).resolve().parents[1] / "scripts" / "run_benchmark.py" + spec = importlib.util.spec_from_file_location("thumb_identity_driver", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_thumb_bit_normalizes_to_dwarf_low_pc(tmp_path: Path) -> None: + source = tmp_path / "thumb.cpp" + obj = tmp_path / "thumb.o" + elf = tmp_path / "thumb.elf" + source.write_text(SRC) + common = [ + "-mcpu=cortex-m4", + "-mthumb", + "-O0", + "-g", + "-fno-exceptions", + "-fno-rtti", + "-fno-unwind-tables", + "-fno-asynchronous-unwind-tables", + ] + subprocess.run( + ["arm-none-eabi-g++", *common, "-c", str(source), "-o", str(obj)], + check=True, + capture_output=True, + text=True, + ) + subprocess.run( + [ + "arm-none-eabi-g++", + *common, + "-nostdlib", + "-Wl,--entry=thumb_entry", + "-Wl,-Ttext=0x08000000", + str(obj), + "-o", + str(elf), + ], + check=True, + capture_output=True, + text=True, + ) + + identities = [row for row in dwarf_function_identities(elf) if row.name == "same"] + assert len({row.address for row in identities}) == 2 + assert all(row.linkage_name for row in identities) + + with elf.open("rb") as stream: + symtab = ELFFile(stream).get_section_by_name(".symtab") + assert symtab is not None + symbols = {symbol.name: int(symbol["st_value"]) for symbol in symtab.iter_symbols()} + + raw_addresses = [] + for row in identities: + assert row.linkage_name is not None + raw = symbols[row.linkage_name] + assert raw & 1 == 1, (row.linkage_name, hex(raw)) + assert raw & ~1 == row.address + raw_addresses.append(raw) + + result = DecompilationResult( + binary_path=elf, + binary_name=elf.stem, + decompiler=DecompilerMetadata(decompiler_name="thumb-fixture"), + functions={ + f"raw-{index}": FunctionDecompilation( + name=f"FUN_{raw:x}", + address=raw, + decompiled_code=f"int FUN_{raw:x}(void) {{ return 0; }}", + ) + for index, raw in enumerate(raw_addresses) + }, + ) + addr2name = {row.address: row.name for row in identities} + _driver()._relabel_to_dwarf(result, addr2name, elf) + + expected = {f"same@0x{address:x}" for address in addr2name} + assert set(result.functions) == expected + assert {fd.address for fd in result.functions.values()} == set(addr2name) + assert all(fd.address & 1 == 0 for fd in result.functions.values()) + assert result.decompiler.failed_functions == [] diff --git a/tests/test_cpp_native_inputs.py b/tests/test_cpp_native_inputs.py new file mode 100644 index 0000000..ae29cdd --- /dev/null +++ b/tests/test_cpp_native_inputs.py @@ -0,0 +1,243 @@ +"""C++ oracle preparation must track the image and separate debug inputs.""" + +from pathlib import Path + +import pytest + +from decbench.metrics.cpp_type_match import CppOracleContext +from decbench.utils import binfmt +from decbench.utils.cfg_identity import NativeSourceIndex + + +@pytest.mark.parametrize("owner", ["ged", "types"]) +@pytest.mark.parametrize("mutation", ["replace", "remove", "appear"]) +def test_reused_context_rejects_changed_debug_companion( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, owner: str, mutation: str +) -> None: + image = tmp_path / "image" + image.write_bytes(b"unchanged native image") + debug = tmp_path / "image.debug" + if mutation != "appear": + debug.write_bytes(b"original debug") + monkeypatch.setattr(binfmt, "native_debug_paths", lambda path: (debug,), raising=False) + if owner == "types": + monkeypatch.setattr(binfmt, "dwarf_info", lambda path: None) + monkeypatch.setattr(binfmt, "pdb_info", lambda path: None) + context = CppOracleContext(image) + else: + context = NativeSourceIndex(image, {}) + context.validate(image) + if mutation == "remove": + debug.unlink() + elif mutation == "replace": + replacement = tmp_path / "new" + replacement.write_bytes(b"replacement debug") + replacement.replace(debug) + else: + debug.write_bytes(b"newly available debug") + with pytest.raises(ValueError, match="debug inputs changed"): + context.validate(image) + + +def test_compile_database_binding_preserves_debug_snapshot( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + image = tmp_path / "image" + image.write_bytes(b"image") + debug = tmp_path / "debug" + debug.write_bytes(b"debug") + monkeypatch.setattr(binfmt, "native_debug_paths", lambda path: (debug,), raising=False) + index = NativeSourceIndex(image, {}) + database = tmp_path / "compile_commands.json" + database.write_text("[]") + bound = index.with_compilation_database(database) + debug.unlink() + with pytest.raises(ValueError, match="debug inputs changed"): + bound.validate(image) + + +@pytest.mark.parametrize("target", ["native", "windows"]) +def test_real_companion_mutation_invalidates_both_owners(tmp_path: Path, target: str) -> None: + import shutil + import subprocess + + from decbench.utils.cfg_identity import native_sources + + compiler = shutil.which("clang++") + if compiler is None: + pytest.skip("clang++ required") + source = tmp_path / "unit.cpp" + source.write_text('int f(int x){return x+1;} extern "C" int entry(){return f(0);}') + binary = tmp_path / ("unit.exe" if target == "windows" else "unit") + if target == "windows": + cc, link = shutil.which("clang-cl"), shutil.which("lld-link") + if cc is None or link is None: + pytest.skip("clang-cl/lld-link required") + obj = tmp_path / "unit.obj" + subprocess.run( + [ + cc, + "--target=x86_64-pc-windows-msvc", + "/c", + "/Z7", + "/Od", + "/GS-", + str(source), + f"/Fo{obj}", + ], + check=True, + capture_output=True, + ) + subprocess.run( + [ + link, + str(obj), + "/entry:entry", + "/nodefaultlib", + "/subsystem:console", + "/debug:full", + f"/pdb:{tmp_path/'unit.pdb'}", + f"/out:{binary}", + ], + check=True, + capture_output=True, + ) + else: + import sys + + if sys.platform != "darwin" or shutil.which("dsymutil") is None: + pytest.skip("native dSYM fixture requires Darwin") + subprocess.run( + [compiler, "-g", str(source), "-Wl,-e,_entry", "-nostartfiles", "-o", str(binary)], + check=True, + capture_output=True, + ) + subprocess.run(["dsymutil", str(binary)], check=True, capture_output=True) + paths = binfmt.native_debug_paths(binary) + assert len(paths) == 1 and paths[0].is_file() + contexts = [native_sources(binary, set()), CppOracleContext(binary)] + for context in contexts: + context.validate(binary) + paths[0].write_bytes(paths[0].read_bytes() + b"changed") + for context in contexts: + with pytest.raises(ValueError, match="debug inputs changed"): + context.validate(binary) + + +def test_missing_location_keeps_exact_native_unit_for_recollection() -> None: + from decbench.models.decompilation import ( + DecompilationResult, + DecompilerMetadata, + FunctionDecompilation, + ) + from decbench.utils.cfg_identity import NativeSource, SourceCfgIndex + + result = DecompilationResult( + binary_path=Path("unused"), + binary_name="fixture", + decompiler=DecompilerMetadata(decompiler_name="fixture"), + ) + result.add_function( + FunctionDecompilation(name="f", address=16, decompiled_code="int f(){return 1;}") + ) + native = NativeSource( + 16, + "f", + ("/src/a.cpp",), + None, + "native:16", + physical_unit="/build/a.o", + compilation_directory="/build", + ) + joined = SourceCfgIndex().resolve(result, {16: [native]}) + evidence = joined.evidence["f"] + assert evidence["status"] == "missing_native_source_location" + assert evidence["native_units"] == ["/src/a.cpp"] + assert evidence["physical_unit"] == "/build/a.o" + assert not joined.cfgs + + +def test_source_index_hashes_exact_prepared_text() -> None: + import hashlib + + from decbench.utils.cfg_identity import SourceCfgIndex + + text = '# 1 "/src/a.cpp"\nint f(){return 1;}\n' + index = SourceCfgIndex() + index.add(text, "unit.ii") + assert index.inputs[0]["text_sha256"] == hashlib.sha256(text.encode()).hexdigest() + + +def test_debug_change_during_native_preparation_is_not_accepted( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from decbench.utils.cfg_identity import native_sources + + image = tmp_path / "image" + image.write_bytes(b"image") + debug = tmp_path / "debug" + debug.write_bytes(b"original") + monkeypatch.setattr(binfmt, "native_debug_paths", lambda path: (debug,)) + + def dwarf(path: Path) -> None: + debug.write_bytes(b"changed during preparation") + + monkeypatch.setattr(binfmt, "dwarf_info", dwarf) + monkeypatch.setattr(binfmt, "pdb_info", lambda path: None) + with pytest.raises(ValueError, match="debug inputs changed"): + native_sources(image, set()) + debug.write_bytes(b"reset") + with pytest.raises(ValueError, match="debug inputs changed"): + CppOracleContext(image) + + +def test_native_index_tracks_companion_beside_requested_alias( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from decbench.utils.cfg_identity import native_sources + + actual = tmp_path / "actual" + actual.mkdir() + alias = tmp_path / "alias" + alias.mkdir() + image = actual / "image" + image.write_bytes(b"image") + requested = alias / "image" + requested.symlink_to(image) + debug = alias / "debug" + debug.write_bytes(b"debug") + monkeypatch.setattr(binfmt, "native_debug_paths", lambda path: (path.parent / "debug",)) + monkeypatch.setattr(binfmt, "dwarf_info", lambda path: None) + monkeypatch.setattr(binfmt, "pdb_info", lambda path: None) + context = native_sources(requested, set()) + debug.write_bytes(b"changed") + with pytest.raises(ValueError, match="debug inputs changed"): + context.validate(requested) + + +def test_embedded_pe_dwarf_does_not_require_unused_pdb( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + import decbench.utils.pdb_info as pdb_module + + monkeypatch.setattr(binfmt, "detect", lambda path: binfmt.BinInfo("pe", "x86-64", 64)) + monkeypatch.setattr(binfmt, "pe_dwarf_info", lambda path: object()) + + def forbidden(path: Path) -> None: + raise AssertionError("embedded DWARF must not consult a PDB") + + monkeypatch.setattr(pdb_module, "pdb_path", forbidden) + assert binfmt.native_debug_paths(tmp_path / "image.exe") == () + + +def test_embedded_macho_dwarf_does_not_watch_unused_dsym( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from types import SimpleNamespace + + monkeypatch.setattr(binfmt, "detect", lambda path: binfmt.BinInfo("macho", "aarch64", 64)) + image = SimpleNamespace( + sections=[SimpleNamespace(segment_name="__DWARF", name="__debug_info", content=b"info")] + ) + monkeypatch.setattr(binfmt, "_mach_o_binary", lambda path: image) + assert binfmt.native_debug_paths(tmp_path / "image") == () diff --git a/tests/test_cpp_review_contracts.py b/tests/test_cpp_review_contracts.py new file mode 100644 index 0000000..3943b2f --- /dev/null +++ b/tests/test_cpp_review_contracts.py @@ -0,0 +1,297 @@ +"""Evaluator failures, unambiguous scoped provenance and executable transport.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path +from types import SimpleNamespace +from typing import Any + +import pytest + +import decbench.metrics.cpp_type_match as cpp_metric +from decbench.decompilers.raw.fission_raw import RawFissionDecompiler +from decbench.metrics.base import MetricConfig +from decbench.metrics.cpp_type_match import CppTypeMatchMetric +from decbench.metrics.cpp_types import UnsupportedType +from decbench.metrics.variable_match import VariableEvidence, extract_decompiler_evidence +from decbench.models.decompilation import ( + DecompilationResult, + DecompilerMetadata, + FunctionDecompilation, + LineMapping, + VariableInfo, +) + + +def _evaluation( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> tuple[CppTypeMatchMetric, DecompilationResult, Any, list[bool]]: + metric = CppTypeMatchMetric( + MetricConfig(use_cache=False, extra_options={"view": "source", "data_model": "LP64"}) + ) + result = DecompilationResult( + binary_path=tmp_path / "input", + binary_name="input", + decompiler=DecompilerMetadata(decompiler_name="fixture"), + ) + result.add_function( + FunctionDecompilation( + name="f", + address=0x1000, + decompiled_code="int f(int x){return x;}", + variables=[VariableInfo(name="x", type="int", kind="arg", arg_index=0)], + ) + ) + oracle = SimpleNamespace( + validate=lambda path: None, + native=None, + info=SimpleNamespace(arch="x86_64"), + dwarf=object(), + pdb=None, + index={}, + ) + monkeypatch.setattr(cpp_metric, "sanitize_native_provenance", lambda *a, **kw: {}) + monkeypatch.setattr(metric, "_dwarf_rows", lambda *a: [{"identity": "source:0"}]) + monkeypatch.setattr( + cpp_metric, + "build_source_evidence", + lambda *a: SimpleNamespace(variables=[VariableEvidence("source:0", arg_index=0)]), + ) + closed: list[bool] = [] + original_close = cpp_metric.PreprocessedSourceContext.close + + def close(context: Any) -> None: + closed.append(True) + original_close(context) + + monkeypatch.setattr(cpp_metric.PreprocessedSourceContext, "close", close) + return metric, result, oracle, closed + + +@pytest.mark.parametrize("stage", ["oracle", "source", "recovery"]) +def test_unexpected_evaluator_value_error_is_not_oracle_abstention( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, stage: str +) -> None: + metric, result, oracle, closed = _evaluation(monkeypatch, tmp_path) + + def fail(*args: Any, **kwargs: Any) -> Any: + raise ValueError("unexpected evaluator fault") + + if stage == "oracle": + monkeypatch.setattr(metric, "_dwarf_rows", fail) + elif stage == "source": + monkeypatch.setattr(cpp_metric, "build_source_evidence", fail) + else: + monkeypatch.setattr(metric, "compute_for_function", fail) + with pytest.raises(ValueError, match="unexpected evaluator fault"): + metric.compute_for_binary(result, oracle_context=oracle) + assert closed == [True] + assert result.functions["f"].decompiled_code == "int f(int x){return x;}" + + +def test_unsupported_oracle_abstains_but_unsupported_recovery_does_not_exclude( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + metric, result, oracle, closed = _evaluation(monkeypatch, tmp_path) + + def fail(*args: Any, **kwargs: Any) -> Any: + raise UnsupportedType("known unsupported representation") + + monkeypatch.setattr(metric, "_dwarf_rows", fail) + excluded = metric.compute_for_binary(result, oracle_context=oracle) + assert excluded.function_results == {} + assert excluded.errors == ["f: known unsupported representation"] + monkeypatch.setattr(metric, "_dwarf_rows", lambda *a: [{"identity": "source:0"}]) + monkeypatch.setattr(metric, "compute_for_function", fail) + with pytest.raises(UnsupportedType, match="known unsupported representation"): + metric.compute_for_binary(result, oracle_context=oracle) + assert closed == [True, True] + + +def test_known_pdb_format_limit_is_typed_oracle_abstention( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + from decbench.utils.pdb_info import PdbFormatError, _Types + + metric, result, oracle, closed = _evaluation(monkeypatch, tmp_path) + types = _Types(b"") + types.records = {0x1000: (0x1203, b"\x04\x14\x00\x00\x00\x10\x00\x00")} + with pytest.raises(PdbFormatError, match="cyclic"): + types.field_records(0x1000) + + def fail(*args: Any, **kwargs: Any) -> Any: + raise PdbFormatError("unsupported CodeView field leaf") + + monkeypatch.setattr(metric, "_dwarf_rows", fail) + excluded = metric.compute_for_binary(result, oracle_context=oracle) + assert excluded.function_results == {} + assert excluded.errors == ["f: unsupported CodeView field leaf"] + assert closed == [True] + + +def _scoped_record() -> dict[str, Any]: + return { + "code_nir": "int f() {\n int x = 0;\n { int x = 1; }\n return y;\n}", + "variables": [ + {"name": "x", "type": "int"}, + {"name": "x", "type": "int"}, + {"name": "y", "type": "int"}, + ], + "line_mappings_nir": [ + {"line_number": 2, "addresses": [0x1000], "defined_variables": ["x", "y"]}, + {"line_number": 3, "addresses": [0x1004], "defined_variables": ["x"]}, + ], + } + + +def test_adapter_does_not_bind_duplicate_display_names_to_scoped_lines() -> None: + record = _scoped_record() + record["variables"][0]["addresses"] = [0x1008] + function = RawFissionDecompiler._build_function(record, "f", 0x1000) + assert function is not None + assert [v.line_numbers for v in function.variables] == [[], [], [2]] + rows = extract_decompiler_evidence(function, backend="test") + assert [r.addresses for r in rows] == [frozenset({0x1008}), frozenset(), frozenset({0x1000})] + + +def test_shared_matcher_rejects_ambiguous_scope_even_with_claimed_occurrences() -> None: + function = RawFissionDecompiler._build_function(_scoped_record(), "f", 0x1000) + assert function is not None + for variable in function.variables: + variable.line_numbers = [2, 3] + rows = extract_decompiler_evidence(function, backend="other-producer") + assert [r.addresses for r in rows] == [frozenset(), frozenset(), frozenset({0x1000})] + function.line_mappings = [LineMapping(line_number=2, addresses=[0x100C])] + assert all( + r.addresses == frozenset({0x100C}) + for r in extract_decompiler_evidence(function, backend="legacy") + ) + + +def test_unnamed_variable_cannot_gain_scoped_name_evidence() -> None: + function = FunctionDecompilation( + name="f", + address=0x1000, + decompiled_code="int f(){return 0;}", + variables=[VariableInfo(name="", line_numbers=[1])], + line_mappings=[LineMapping(line_number=1, addresses=[0x1000], defined_variables=[""])], + ) + assert not extract_decompiler_evidence(function, backend="test", include_unnamed=True)[ + 0 + ].addresses + + +def test_relative_fission_executable_survives_changed_child_directory( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + import decbench.decompilers.raw.fission_raw as fission + + caller, worker = tmp_path / "caller directory", tmp_path / "worker directory" + caller.mkdir() + worker.mkdir() + executable = caller / "fixture CLI" + payload = [{"address": "0x1000", "code_nir": "int f(){return 0;}", "variables": []}] + executable.write_text( + f"#!{sys.executable}\nimport json,sys\n" + "if '--version' in sys.argv: print('fixture version')\n" + f"else: print({json.dumps(payload)!r})\n" + ) + executable.chmod(0o755) + binary = caller / "input.bin" + binary.write_bytes(b"not executed") + monkeypatch.chdir(caller) + monkeypatch.setenv("FISSION_BIN", executable.name) + monkeypatch.setenv("FISSION_WORKING_DIR", str(worker)) + monkeypatch.delenv("FISSION_RESOURCE_ROOT", raising=False) + monkeypatch.setattr(fission, "sanitize_native_provenance", lambda *a, **kw: {}) + adapter = RawFissionDecompiler() + assert adapter._binary() == str(executable.resolve()) + result = adapter.decompile_binary(binary, functions=[("f", 0x1000)]) + assert result.decompiler.decompiler_version == "fixture version" + assert result.functions["f"].decompiled_code == payload[0]["code_nir"] + + +def test_relative_path_lookup_is_also_frozen_before_child_cwd( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.chdir(tmp_path) + executable = tmp_path / "bin" / "fission_cli" + executable.parent.mkdir() + executable.touch() + executable.chmod(0o755) + monkeypatch.delenv("FISSION_BIN", raising=False) + monkeypatch.setenv("PATH", "bin") + assert RawFissionDecompiler._binary() == str(executable.resolve()) + + +@pytest.mark.parametrize("with_plan", [False, True]) +def test_cpp_type_population_rejects_duplicate_producer_entries( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, with_plan: bool +) -> None: + from decbench.models.metrics import MetricValue + + metric, result, oracle, _closed = _evaluation(monkeypatch, tmp_path) + result.add_function( + FunctionDecompilation( + name="alias", address=0x1000, decompiled_code="int alias(){return 1;}" + ) + ) + monkeypatch.setattr(metric, "compute_for_function", lambda *a, **kw: MetricValue(value=1.0)) + kwargs: dict[str, Any] = {"oracle_context": oracle} + if with_plan: + kwargs["requested_functions"] = [("f", 0x1000)] + with pytest.raises(ValueError, match="ambiguous producer records at one requested address"): + metric.compute_for_binary(result, **kwargs) + assert list(result.functions) == ["f", "alias"] + + +def test_cpp_type_population_filters_extras_and_retains_empty_recovery( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + from decbench.models.metrics import MetricValue + + metric, result, oracle, _closed = _evaluation(monkeypatch, tmp_path) + for name in ["extra", "extra_alias"]: + result.add_function( + FunctionDecompilation( + name=name, address=0x2000, decompiled_code="int extra(){return 0;}" + ) + ) + monkeypatch.setattr( + metric, + "compute_for_function", + lambda f, **kw: MetricValue(value=float(bool(f.decompiled_code))), + ) + scored = metric.compute_for_binary( + result, oracle_context=oracle, requested_functions=[("f", 0x1000), ("missing", 0x3000)] + ) + assert set(scored.function_results) == {"f", "missing"} + assert scored.function_results["missing"].value == 0.0 + assert len(result.functions) == 3 + + +@pytest.mark.parametrize("suffix", ["&", "&&"]) +def test_valid_cpp_reference_declarator_selects_error_free_ast(suffix: str) -> None: + from decbench.metrics.variable_features import ( + _CPP_LANGUAGE, + _parse_source, + function_parameters, + variable_occurrence_lines, + ) + + code = f"int f(int{suffix} x) {{\n return x;\n}}\n" + tree = _parse_source(code.encode()) + assert not tree.root_node.has_error + assert tree.language == _CPP_LANGUAGE + assert function_parameters(code, "f") == f"int{suffix} x" + assert variable_occurrence_lines(code, "f", ["x"]) == {"x": (1, 2)} + + +def test_valid_c_ast_keeps_c_grammar() -> None: + from decbench.metrics.variable_features import _C_LANGUAGE, _parse_source + + tree = _parse_source(b"int f(int *restrict p) { return *p; }") + assert not tree.root_node.has_error + assert tree.language == _C_LANGUAGE diff --git a/tests/test_cpp_toolchain_probe.py b/tests/test_cpp_toolchain_probe.py new file mode 100644 index 0000000..0d1c117 --- /dev/null +++ b/tests/test_cpp_toolchain_probe.py @@ -0,0 +1,47 @@ +"""Cross-target fixtures do not confer corpus or original-compiler parity.""" + +import importlib.util +import shutil +from pathlib import Path + +import pytest + + +def test_real_cpp_preprocessing_and_effect_fixture_matrix(tmp_path: Path) -> None: + compiler = shutil.which("clang++") + if compiler is None: + pytest.skip("clang++ required") + script = Path(__file__).parents[1] / "scripts/audit_cpp_toolchains.py" + spec = importlib.util.spec_from_file_location("cpp_toolchain_probe", script) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + report = module.audit( + compiler, Path(__file__).parent / "fixtures/cpp_qualification.cpp", tmp_path + ) + assert report["official_score"] is False + assert report["executed_programs"] is False + assert len(report["cells"]) == 9 + assert {row["object_identity"]["fmt"] for row in report["cells"]} == {"elf", "pe", "macho"} + assert {row["optimization"] for row in report["cells"]} == {"O0", "O2", "O2-noinline"} + assert all( + row["text_sections_identical"] and row["exception_ir_present"] for row in report["cells"] + ) + + +def test_wrong_target_object_fails_fixture_audit( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from decbench.utils.binfmt import BinInfo + + compiler = shutil.which("clang++") + if compiler is None: + pytest.skip("clang++ required") + script = Path(__file__).parents[1] / "scripts/audit_cpp_toolchains.py" + spec = importlib.util.spec_from_file_location("cpp_toolchain_probe_wrong", script) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + monkeypatch.setattr(module, "object_info", lambda path: BinInfo("macho", "aarch64", 64)) + with pytest.raises(ValueError, match="requested target"): + module.audit(compiler, Path(__file__).parent / "fixtures/cpp_qualification.cpp", tmp_path) diff --git a/tests/test_cpp_type_graph.py b/tests/test_cpp_type_graph.py new file mode 100644 index 0000000..2621649 --- /dev/null +++ b/tests/test_cpp_type_graph.py @@ -0,0 +1,662 @@ +"""Object graphs, complete declarations and exact producer correspondence.""" + +from __future__ import annotations + +import copy +import shutil +import struct +import subprocess +from dataclasses import asdict, replace +from pathlib import Path + +import pytest + +from decbench.decompilers.raw.fission_raw import RawFissionDecompiler +from decbench.metrics.base import MetricConfig +from decbench.metrics.cpp_recovered_types import RecoveredTypeContext +from decbench.metrics.cpp_type_graph import ( + TypeEdge, + TypeGraph, + TypeNode, + dwarf_graph, + pdb_graph, + primitive_graph, +) +from decbench.metrics.cpp_type_match import CppTypeMatchMetric +from decbench.metrics.cpp_types import UnsupportedType, parse_type +from decbench.metrics.variable_match import VariableEvidence, extract_decompiler_evidence +from decbench.models.decompilation import FunctionDecompilation, LineMapping, VariableInfo +from decbench.utils import binfmt +from decbench.utils.pdb_info import _Types + + +def _graph(name: str = "demo::Object") -> TypeGraph: + return TypeGraph( + "obj", + { + "obj": TypeNode( + "record", + 16, + name, + edges=( + TypeEdge("field", "i", "value", 0, access=3), + TypeEdge("field", "p", "next", 64, access=3), + ), + ), + "i": TypeNode("signed", 4, rank="int"), + "p": TypeNode("pointer", 8, edges=(TypeEdge("element", "obj"),)), + }, + ) + + +def _payload(graph: TypeGraph) -> dict: + return { + "schema": "decbench-cpp-type-graph-v1", + "root": graph.root, + "nodes": {k: {**asdict(v), "complete": True} for k, v in graph.nodes.items()}, + } + + +def test_ids_do_not_prove_equality_and_recursive_targets_are_compared() -> None: + a = _graph() + payload = _payload(a) + payload["root"] = "new" + payload["nodes"]["new"] = payload["nodes"].pop("obj") + payload["nodes"]["p"]["edges"][0]["target"] = "new" + b = TypeGraph.from_payload(payload) + assert a == b + b.nodes["i"] = TypeNode("unsigned", 4, rank="int") + assert a != b + assert a.project("abi") != b.project("abi") + + +@pytest.mark.parametrize( + "change", ["field_type", "offset", "field_name", "object_name", "size", "access"] +) +def test_source_object_comparison_requires_all_layout_and_identity_facts(change: str) -> None: + a = _graph() + b = copy.deepcopy(a) + if change == "field_type": + b.nodes["i"] = TypeNode("float", 4, rank="float") + else: + node = b.nodes["obj"] + updates = {"object_name": {"name": "other::Object"}, "size": {"size": 24}} + if change in updates: + node = replace(node, **updates[change]) + else: + edge = node.edges[0] + edge = replace( + edge, + **{ + "offset": {"offset_bits": 32}, + "field_name": {"name": "renamed"}, + "access": {"access": 1}, + }[change], + ) + node = replace(node, edges=(edge, node.edges[1])) + b.nodes["obj"] = node + assert a != b + assert (a.project("abi") == b.project("abi")) == ( + change in {"object_name", "field_name", "access"} + ) + + +def test_size_and_nominal_name_without_fields_are_never_perfect() -> None: + a = _graph() + b = TypeGraph("obj", {"obj": TypeNode("record", 16, "demo::Object")}) + assert a != b + assert a.project("abi") != b.project("abi") + + +def test_ordinary_and_virtual_inheritance_are_distinct_and_targets_checked() -> None: + a = _graph("Derived") + a.nodes["base"] = TypeNode( + "record", 4, "Base", edges=(TypeEdge("field", "i", "base_value", 0, access=3),) + ) + a.nodes["obj"] = replace( + a.nodes["obj"], + edges=(TypeEdge("base", "base", offset_bits=0, access=3), a.nodes["obj"].edges[1]), + ) + a.validate() + b = copy.deepcopy(a) + b.nodes["obj"] = replace( + b.nodes["obj"], + edges=( + TypeEdge("base", "base", virtual=True, locator="dwarf:1206381c", access=3), + b.nodes["obj"].edges[1], + ), + ) + b.validate() + assert a != b + assert a.project("abi") != b.project("abi") + c = copy.deepcopy(b) + c.nodes["obj"] = replace( + c.nodes["obj"], + edges=(replace(c.nodes["obj"].edges[0], locator="dwarf:1206381d"), c.nodes["obj"].edges[1]), + ) + assert b != c + + +@pytest.mark.parametrize( + "mutation", + [ + "missing", + "incomplete", + "out_of_extent", + "virtual_offset", + "schema", + "pointer_without_target", + ], +) +def test_incomplete_or_malformed_producer_graphs_abstain(mutation: str) -> None: + p = _payload(_graph()) + if mutation == "missing": + del p["nodes"]["i"] + elif mutation == "incomplete": + p["nodes"]["obj"]["complete"] = False + elif mutation == "out_of_extent": + p["nodes"]["obj"]["edges"][0]["offset_bits"] = 128 + elif mutation == "virtual_offset": + p["nodes"]["obj"]["edges"][0].update(role="base", virtual=True, locator="locator") + elif mutation == "schema": + p["schema"] = "wrong" + else: + p["nodes"]["p"]["edges"] = [] + with pytest.raises(UnsupportedType): + TypeGraph.from_payload(p) + + +def test_ordinary_emitted_definition_and_recursive_pointer_layout() -> None: + code = "namespace demo { struct Object { int value; Object *next; }; }" + recovered = RecoveredTypeContext(code, "LP64").graph("demo::Object") + assert recovered == _graph() + assert recovered.project("abi") == _graph("Different").project("abi") + assert ( + RecoveredTypeContext(code.replace("int value", "unsigned int value"), "LP64").graph( + "demo::Object" + ) + != recovered + ) + + +@pytest.mark.parametrize( + "code,spelling", + [ + ("struct X;", "X"), + ("struct X { int n; }; struct X { int n; };", "X"), + ("struct X { unsigned int n:3; };", "X"), + ("struct X { int n; }; #pragma pack(1)", "X"), + ("struct X { X child; };", "X"), + ("class X { int n; };", "X"), + ("struct Base { int n; }; struct X:Base { int n; };", "X"), + ], +) +def test_recovered_layout_never_guesses_opaque_or_special_abi(code: str, spelling: str) -> None: + with pytest.raises(UnsupportedType): + RecoveredTypeContext(code, "LP64").graph(spelling) + + +def test_arrays_and_typedef_records_use_complete_definitions() -> None: + ctx = RecoveredTypeContext("typedef struct { char c; int v[2]; } X;", "LP64") + graph = ctx.graph("X") + node = graph.nodes[graph.root] + assert node.size == 12 + assert [(e.name, e.offset_bits) for e in node.edges] == [("c", 0), ("v", 32)] + array = graph.nodes[node.edges[1].target] + assert array.kind == "array" and array.count == 2 and array.size == 8 + + +def test_metric_matches_without_using_type_then_compares_object_graph() -> None: + graph = _graph() + fn = FunctionDecompilation( + name="f", + address=0x1000, + decompiled_code="void f() {}", + variables=[VariableInfo(name="v", type="demo::Object", size=16, addresses=[0x1000])], + metadata={"type_graphs": {"demo::Object": _payload(graph)}}, + ) + gt = [{"identity": "source:0", "descriptor": graph}] + evidence = [VariableEvidence("source:0", frozenset({0x1000}))] + metric = CppTypeMatchMetric( + MetricConfig(extra_options={"view": "source", "data_model": "LP64"}) + ) + result = metric.compute_for_function(fn, ground_truth_vars=gt, source_evidence=evidence) + assert result.value == 1 and result.metadata["matched_count"] == 1 + fn.metadata = {} + result = metric.compute_for_function(fn, ground_truth_vars=gt, source_evidence=evidence) + assert result.value == 0 and result.metadata["ground_truth_count"] == 1 + assert result.metadata["decisions"][0]["verdict"] == "unsupported_recovered_type" + + +def test_scoped_fission_lines_never_assign_rhs_or_legacy_lifetime_to_all_occurrences() -> None: + record = { + "code_nir": "int f() {\n int a = b;\n return a;\n}", + "variables": [{"name": "a", "type": "int"}, {"name": "b", "type": "int"}], + "line_mappings_nir": [ + {"line_number": 2, "addresses": [0x1000], "defined_variables": ["a"]} + ], + } + function = RawFissionDecompiler._build_function(record, "f", 0x1000) + assert function is not None + assert function.variables[0].line_numbers == [2] + assert function.variables[1].line_numbers == [] + function.variables[1].line_numbers = [2] + rows = extract_decompiler_evidence(function, backend="test") + assert rows[0].addresses == frozenset({0x1000}) and not rows[1].addresses + function.line_mappings = [LineMapping(line_number=2, addresses=[0x1000])] + assert extract_decompiler_evidence(function, backend="test")[1].addresses == frozenset({0x1000}) + function.variables[0].addresses = [0x1004] + assert extract_decompiler_evidence(function, backend="test")[0].addresses == frozenset({0x1004}) + + +def _field(attrs: int, ty: int, offset: int, name: str) -> bytes: + data = struct.pack(" bytes: + return struct.pack(" None: + types = _Types(b"") + types.records = { + 0x1000: (0x1505, _record(0x1001, 4, "X")), + 0x1001: (0x1203, _field(3, 0x1003, 0, "bits") + struct.pack(" None: + types = _Types(b"") + types.records = { + 0x1000: (0x1505, _record(0x1001, 4, "X")), + 0x1001: (0x1203, struct.pack(" None: + source = tmp_path / "object.cpp" + source.write_text( + "namespace demo { struct Base { int value; }; " + "struct Object: Base { long long other; }; } " + "demo::Object object; int main(){return object.value;}" + ) + binary = tmp_path / "object" + object_file = tmp_path / "object.o" + subprocess.run( + ["clang++", "-g", "-O0", "-c", str(source), "-o", str(object_file)], + check=True, + capture_output=True, + ) + subprocess.run( + ["clang++", str(object_file), "-o", str(binary)], check=True, capture_output=True + ) + if binary.read_bytes()[:4] != b"\x7fELF": + subprocess.run(["dsymutil", str(binary)], check=True, capture_output=True) + dwarf = binfmt.dwarf_info(binary) + assert dwarf is not None + variable = next( + d + for cu in dwarf.iter_CUs() + for d in cu.iter_DIEs() + if d.tag == "DW_TAG_variable" and binfmt.die_str_attr(d, "DW_AT_name") == "object" + ) + graph = dwarf_graph(variable, "LP64") + n = graph.nodes[graph.root] + assert n.kind == "record" and n.name == "demo::Object" and n.size == 16 + assert [(e.role, e.offset_bits) for e in n.edges] == [("base", 0), ("field", 64)] + assert graph.nodes[n.edges[0].target].name == "demo::Base" + assert graph.nodes[graph.nodes[n.edges[0].target].edges[0].target].kind == "signed" + + +@pytest.mark.skipif( + shutil.which("clang-cl") is None or shutil.which("lld-link") is None, + reason="clang-cl/lld-link needed for real CodeView fixture", +) +def test_real_codeview_object_base_and_fields(tmp_path: Path) -> None: + from decbench.utils.pdb_info import read_pdb + + source = tmp_path / "object.cpp" + source.write_text( + "namespace demo { struct Base { int value; }; " + "struct Object: Base { long long other; }; } " + 'demo::Object object; extern "C" int entry(){return object.value;}' + ) + obj, binary = tmp_path / "object.obj", tmp_path / "object.exe" + pdb = tmp_path / "object.pdb" + subprocess.run( + [ + "clang-cl", + "--target=x86_64-pc-windows-msvc", + "/c", + "/Z7", + "/Od", + "/GS-", + str(source), + f"/Fo{obj}", + ], + check=True, + capture_output=True, + ) + subprocess.run( + [ + "lld-link", + str(obj), + "/entry:entry", + "/subsystem:console", + "/nodefaultlib", + "/debug:full", + f"/pdb:{pdb}", + f"/out:{binary}", + ], + check=True, + capture_output=True, + ) + native = read_pdb(binary) + assert native is not None and native.types is not None + candidates = [] + for index, (leaf, payload) in native.types.records.items(): + if ( + leaf in {0x1504, 0x1505} + and not struct.unpack_from(" None: + value = parse_type(spelling, model) + graph = primitive_graph(value) + assert graph.byte_size() == value.byte_size() + assert graph.project("abi") == primitive_graph(value.project("abi")).project("abi") + + +def test_enum_abi_uses_underlying_storage_but_source_retains_values_and_identity() -> None: + graph = TypeGraph( + "enum", + { + "enum": TypeNode( + "enum", + 4, + "Color", + edges=(TypeEdge("element", "u"),), + enumerators=(("red", 1), ("blue", 2)), + ), + "u": TypeNode("unsigned", 4, rank="int"), + }, + ) + graph.validate() + unsigned = primitive_graph(parse_type("unsigned int", "LP64")) + assert graph.project("abi") == unsigned.project("abi") + assert graph != unsigned + changed = copy.deepcopy(graph) + changed.nodes["enum"] = replace(graph.nodes["enum"], enumerators=(("red", 3), ("blue", 2))) + assert changed != graph + + +def test_by_value_cycles_are_rejected_but_pointer_cycles_remain_valid() -> None: + payload = _payload(_graph()) + payload["nodes"]["obj"]["edges"] = [ + {"role": "field", "target": "obj", "offset_bits": 0, "name": "self"} + ] + with pytest.raises(UnsupportedType, match="by-value"): + TypeGraph.from_payload(payload) + TypeGraph.from_payload(_payload(_graph())) + + +def test_source_definition_line_never_borrows_an_overload_or_guesses_same_line() -> None: + from decbench.metrics.variable_features import function_definition_at_line + + code = ( + "namespace a { int same(int x) { return x; } }\n" + "int same(int x){return x+1;}\nint same(double x){return 2;}" + ) + assert "x+1" in function_definition_at_line(code, "same", 2) + assert "return 2" in function_definition_at_line(code, "same", 3) + assert function_definition_at_line(code, "same", 1) is None + assert "return x" in function_definition_at_line(code, "a::same", 1) + assert ( + function_definition_at_line("int f(int x){return x;} int f(double x){return 0;}", "f", 1) + is None + ) + + +def test_codeview_virtual_base_locator_preserves_pointer_type_and_never_offset_zero() -> None: + types = _Types(b"") + types.records = { + 0x1000: (0x1505, _record(0x1001, 4, "Base")), + 0x1001: (0x1203, _field(3, 0x74, 0, "value")), + 0x1002: (0x1505, _record(0x1003, 16, "Derived")), + 0x1003: (0x1203, struct.pack(" None: + from decbench.metrics.cpp_type_match import CppOracleContext + from decbench.models.decompilation import DecompilationResult, DecompilerMetadata + from decbench.utils.function_identity import dwarf_function_identities + + source = tmp_path / "population.cpp" + source.write_text( + 'struct Object { int value; }; extern "C" int take(Object* p){return p->value;} ' + 'extern "C" int mixed(Object* p,int(*f)(int)){return f(p->value);} ' + "int main(){Object o{3};return take(&o);}" + ) + obj, binary = tmp_path / "population.o", tmp_path / "population" + subprocess.run( + ["clang++", "-g", "-O0", "-c", str(source), "-o", str(obj)], check=True, capture_output=True + ) + subprocess.run(["clang++", str(obj), "-o", str(binary)], check=True, capture_output=True) + if binary.read_bytes()[:4] != b"\x7fELF": + subprocess.run(["dsymutil", str(binary)], check=True, capture_output=True) + addresses = {d.name: d.address for d in dwarf_function_identities(binary)} + recovered = DecompilationResult( + binary_path=binary, + binary_name=binary.name, + decompiler=DecompilerMetadata(decompiler_name="fixture"), + ) + recovered.add_function( + FunctionDecompilation( + name="take", + address=addresses["take"], + decompiled_code="struct Object { int value; }; int take(Object* p){return p->value;}", + variables=[VariableInfo(name="p", type="Object*", size=8, kind="arg", arg_index=0)], + ) + ) + missing = DecompilationResult( + binary_path=binary, + binary_name=binary.name, + decompiler=DecompilerMetadata(decompiler_name="missing"), + ) + recovered.add_function( + FunctionDecompilation( + name="extra", address=addresses["mixed"], decompiled_code="int extra(){return 0;}" + ) + ) + oracle = CppOracleContext(binary) + for view in ["source", "abi"]: + metric = CppTypeMatchMetric( + MetricConfig(extra_options={"view": view, "data_model": "LP64"}) + ) + kwargs = {"oracle_context": oracle, "requested_functions": [("take", addresses["take"])]} + good = metric.compute_for_binary(recovered, **kwargs) + lost = metric.compute_for_binary(missing, **kwargs) + assert set(good.function_results) == set(lost.function_results) == {"take"} + assert good.function_results["take"].value == 1 + assert lost.function_results["take"].value == 0 + excluded = metric.compute_for_binary( + missing, oracle_context=oracle, requested_functions=[("mixed", addresses["mixed"])] + ) + assert not excluded.function_results + assert any("DW_TAG_subroutine_type" in e for e in excluded.errors) + assert missing.functions == {} + + +@pytest.mark.parametrize("data_model", ["LP64", "LLP64"]) +@pytest.mark.parametrize( + "suffix,kinds", + [ + ("&&", ["rref", "record"]), + ("&", ["lref", "record"]), + ("*&&", ["rref", "pointer", "record"]), + (" * &", ["lref", "pointer", "record"]), + ], +) +def test_nominal_reference_suffixes_preserve_indirection_tokens( + data_model: str, suffix: str, kinds: list[str] +) -> None: + context = RecoveredTypeContext("namespace demo { struct Object { int value; }; }", data_model) + graph = context.graph("demo::Object" + suffix) + root = graph.root + observed = [] + while True: + node = graph.nodes[root] + observed.append(node.kind) + if node.kind == "record": + break + assert node.size == (4 if data_model == "ILP32" else 8) + root = node.edges[0].target + assert observed == kinds + if suffix == "&&": + pointer = context.graph("demo::Object*") + lref = context.graph("demo::Object&") + assert graph != pointer and graph != lref + assert graph.project("abi") == pointer.project("abi") == lref.project("abi") + + +@pytest.mark.parametrize("suffix", ["&&&", "&*", "&&*", "& &", "&&&&"]) +def test_invalid_nominal_nested_references_abstain(suffix: str) -> None: + with pytest.raises(UnsupportedType): + RecoveredTypeContext("struct Object { int value; };", "LP64").graph("Object" + suffix) + + +def test_ilp32_record_layout_still_requires_explicit_evidence() -> None: + with pytest.raises(UnsupportedType, match="requires explicit graph"): + RecoveredTypeContext("struct Object { int value; };", "ILP32").graph("Object&&") + + +@pytest.mark.skipif(shutil.which("clang++") is None, reason="clang++ needed") +def test_recovered_rvalue_reference_matches_real_dwarf_graph(tmp_path: Path) -> None: + source = tmp_path / "reference.cpp" + code = "namespace demo { struct Object { int value; }; }" + source.write_text( + code + "\nint consume(demo::Object&& x){return x.value;}\n" + "int main(){return consume(demo::Object{1});}\n" + ) + obj, binary = tmp_path / "reference.o", tmp_path / "reference" + subprocess.run( + ["clang++", "-g", "-O0", "-c", str(source), "-o", str(obj)], check=True, capture_output=True + ) + subprocess.run(["clang++", str(obj), "-o", str(binary)], check=True, capture_output=True) + if binary.read_bytes()[:4] != b"\x7fELF": + subprocess.run(["dsymutil", str(binary)], check=True, capture_output=True) + dwarf = binfmt.dwarf_info(binary) + assert dwarf is not None + param = next( + d + for cu in dwarf.iter_CUs() + for d in cu.iter_DIEs() + if d.tag == "DW_TAG_formal_parameter" and binfmt.die_str_attr(d, "DW_AT_name") == "x" + ) + native = dwarf_graph(param, "LP64") + recovered = RecoveredTypeContext(code, "LP64").graph("demo::Object&&") + assert native.nodes[native.root].kind == "rref" + assert recovered == native + assert recovered.project("abi") == native.project("abi") + + +@pytest.mark.skipif( + shutil.which("clang-cl") is None or shutil.which("lld-link") is None, + reason="clang-cl/lld-link needed", +) +def test_recovered_rvalue_reference_matches_real_pdb_graph(tmp_path: Path) -> None: + from decbench.utils.pdb_info import read_pdb + + source = tmp_path / "reference.cpp" + code = "namespace demo { struct Object { int value; }; }" + source.write_text( + code + "\nint consume(demo::Object&& x){return x.value;}\n" + 'extern "C" int entry(){return consume(demo::Object{1});}\n' + ) + obj, binary, pdb_path = ( + tmp_path / "reference.obj", + tmp_path / "reference.exe", + tmp_path / "reference.pdb", + ) + subprocess.run( + [ + "clang-cl", + "--target=x86_64-pc-windows-msvc", + "/c", + "/Z7", + "/Od", + "/GS-", + str(source), + f"/Fo{obj}", + ], + check=True, + capture_output=True, + ) + subprocess.run( + [ + "lld-link", + str(obj), + "/entry:entry", + "/subsystem:console", + "/nodefaultlib", + "/debug:full", + f"/pdb:{pdb_path}", + f"/out:{binary}", + ], + check=True, + capture_output=True, + ) + pdb = read_pdb(binary) + assert pdb is not None and pdb.types is not None + variable = next(v for f in pdb.functions for v in f.variables if v.name == "x") + native = pdb_graph(pdb.types, variable.type_index, "LLP64") + recovered = RecoveredTypeContext(code, "LLP64").graph("demo::Object&&") + assert native.nodes[native.root].kind == "rref" + assert recovered == native + assert recovered.project("abi") == native.project("abi") diff --git a/tests/test_function_identity.py b/tests/test_function_identity.py new file mode 100644 index 0000000..73e9eb7 --- /dev/null +++ b/tests/test_function_identity.py @@ -0,0 +1,294 @@ +"""Regression coverage for collision-safe function identity primitives.""" + +from __future__ import annotations + +import shutil +import subprocess +from pathlib import Path + +import pytest + +from decbench.models.decompilation import ( + DecompilationResult, + DecompilerMetadata, + FunctionDecompilation, + VariableInfo, +) +from decbench.utils.function_identity import ( + dwarf_function_identities, + identities_by_name, + insert_function, +) +from decbench.utils.source_extract import function_source_ex + +CPP_COLLISIONS = r""" +namespace alpha { +__attribute__((noinline)) int collide(int x) { return x + 1; } +} + +namespace beta { +__attribute__((noinline)) int collide(int x) { return x + 2; } +} + +__attribute__((noinline)) int collide(int x) { return x + 3; } +__attribute__((noinline)) int collide(double x) { return static_cast(x) + 4; } + +struct A { __attribute__((noinline)) int Next(int x) const; }; +struct B { __attribute__((noinline)) int Next(int x) const; }; + +int A::Next(int x) const { return x + 5; } +int B::Next(int x) const { return x + 6; } + +int main() { + A a; + B b; + return alpha::collide(1) + beta::collide(1) + collide(1) + collide(1.0) + + a.Next(1) + b.Next(1); +} +""" + + +def _func(name: str, address: int) -> FunctionDecompilation: + return FunctionDecompilation( + name=name, + address=address, + decompiled_code=f"int {name}(void) {{ return 0; }}", + ) + + +def _result(tmp_path: Path) -> DecompilationResult: + return DecompilationResult( + binary_path=tmp_path / "fixture", + binary_name="fixture", + decompiler=DecompilerMetadata(decompiler_name="identity-test"), + ) + + +def _compile_collision_fixture(tmp_path: Path) -> Path: + source = tmp_path / "identity_collision.cpp" + binary = tmp_path / "identity_collision" + source.write_text(CPP_COLLISIONS) + subprocess.run( + [ + "g++", + "-std=c++17", + "-O0", + "-g", + "-fno-inline", + "-fno-builtin", + str(source), + "-o", + str(binary), + ], + check=True, + capture_output=True, + text=True, + ) + return binary + + +def test_insert_function_preserves_legacy_key_until_collision() -> None: + functions: dict[str, FunctionDecompilation] = {} + + assert insert_function(functions, _func("foo", 0x1000)) == "foo" + assert list(functions) == ["foo"] + + assert insert_function(functions, _func("bar", 0x2000)) == "bar" + assert set(functions) == {"foo", "bar"} + + +def test_insert_function_rekeys_every_same_name_collision_by_address() -> None: + functions: dict[str, FunctionDecompilation] = {} + + insert_function(functions, _func("foo", 0x1000)) + insert_function(functions, _func("foo", 0x2000)) + insert_function(functions, _func("foo", 0x3000)) + + assert set(functions) == { + "foo@0x1000", + "foo@0x2000", + "foo@0x3000", + } + assert {function.address for function in functions.values()} == {0x1000, 0x2000, 0x3000} + + +def test_insert_function_replaces_same_identity_in_place() -> None: + functions: dict[str, FunctionDecompilation] = {} + first = _func("foo", 0x1000) + second = FunctionDecompilation( + name="foo", + address=0x1000, + decompiled_code="int foo(void) { return 42; }", + ) + + insert_function(functions, first) + assert insert_function(functions, second) == "foo" + assert len(functions) == 1 + assert functions["foo"].decompiled_code.endswith("return 42; }") + + +def test_decompilation_result_exposes_collision_safe_api(tmp_path: Path) -> None: + result = _result(tmp_path) + + result.add_function(_func("foo", 0x1000)) + result.add_function(_func("foo", 0x2000)) + result.add_function(_func("bar", 0x3000)) + + assert result.function_count == 3 + assert set(result.functions) == {"foo@0x1000", "foo@0x2000", "bar"} + assert {function.address for function in result.functions_named("foo")} == {0x1000, 0x2000} + assert result.functions_named("missing") == [] + + +def test_ghidra_function_lookup_is_address_keyed() -> None: + from decbench.decompilers.raw.ghidra_raw import RawGhidraDecompiler + + class Address: + def __init__(self, offset: int): + self.offset = offset + + def getOffset(self) -> int: + return self.offset + + class Function: + def __init__(self, name: str, offset: int): + self.name = name + self.address = Address(offset) + + def getName(self) -> str: + return self.name + + def getEntryPoint(self) -> Address: + return self.address + + class FunctionManager: + def __init__(self, functions): + self.functions = functions + + def getFunctions(self, _forward: bool): + return list(self.functions) + + class Program: + def __init__(self): + self.base = Address(0x1000) + self.manager = FunctionManager([Function("same", 0x1010), Function("same", 0x1020)]) + + def getImageBase(self) -> Address: + return self.base + + def getFunctionManager(self) -> FunctionManager: + return self.manager + + by_address = RawGhidraDecompiler._functions_by_address(Program(), elf_base=0x400000) + + assert set(by_address) == {0x400010, 0x400020} + assert [by_address[address].getName() for address in sorted(by_address)] == ["same", "same"] + + +def test_type_match_prefers_address_ground_truth_for_same_name(tmp_path: Path) -> None: + from decbench.metrics.type_match import TypeMatchMetric + + result = _result(tmp_path) + first = FunctionDecompilation( + name="same", + address=0x1000, + decompiled_code="int same(int x) { return x; }", + variables=[VariableInfo(name="x", type="int", kind="arg", arg_index=0)], + ) + second = FunctionDecompilation( + name="same", + address=0x2000, + decompiled_code="double same(double x) { return x; }", + variables=[VariableInfo(name="x", type="double", kind="arg", arg_index=0)], + ) + result.add_function(first) + result.add_function(second) + + int_gt = [ + { + "name": "x", + "type": ["int"], + "rbp_offset": [], + "size": 4, + "is_arg": True, + "arg_index": 0, + } + ] + double_gt = [ + { + "name": "x", + "type": ["double"], + "rbp_offset": [], + "size": 8, + "is_arg": True, + "arg_index": 0, + } + ] + + metric = TypeMatchMetric() + cache_key = str(result.binary_path) + metric._ground_truth_cache[cache_key] = {"same": {0x1000: int_gt, 0x2000: double_gt}} + metric._ground_truth_address_cache[cache_key] = { + 0x1000: int_gt, + 0x2000: double_gt, + } + + scored = metric.compute_for_binary(result) + + assert set(scored.function_results) == {"same@0x1000", "same@0x2000"} + assert scored.function_results["same@0x1000"].value == 1.0 + assert scored.function_results["same@0x2000"].value == 1.0 + + +needs_gxx = pytest.mark.skipif(shutil.which("g++") is None, reason="needs g++") + + +@needs_gxx +def test_real_cpp_dwarf_identity_does_not_collapse_overloads(tmp_path: Path) -> None: + binary = _compile_collision_fixture(tmp_path) + + identities = dwarf_function_identities(binary) + interesting = [ + identity for identity in identities if identity.name in {"collide", "Next", "main"} + ] + grouped = identities_by_name(interesting) + + assert len(grouped["collide"]) == 4 + assert len(grouped["Next"]) == 2 + assert len(grouped["main"]) == 1 + assert len({identity.address for identity in interesting}) == 7 + + linkage_names = [identity.linkage_name for identity in interesting if identity.linkage_name] + assert len(linkage_names) == 6 + assert len(set(linkage_names)) == 6 + + result = _result(tmp_path) + for identity in interesting: + result.add_function(_func(identity.name, identity.address)) + + assert result.function_count == 7 + assert {function.address for function in result.functions.values()} == { + identity.address for identity in interesting + } + + +@needs_gxx +def test_source_extraction_uses_address_to_distinguish_overloads(tmp_path: Path) -> None: + binary = _compile_collision_fixture(tmp_path) + identities = [ + identity for identity in dwarf_function_identities(binary) if identity.name == "collide" + ] + + int_global = next(identity for identity in identities if identity.linkage_name == "_Z7collidei") + double_global = next( + identity for identity in identities if identity.linkage_name == "_Z7collided" + ) + + int_source, int_status = function_source_ex(binary, "collide", int_global.address) + double_source, double_status = function_source_ex(binary, "collide", double_global.address) + + assert int_status == "" + assert double_status == "" + assert int_source is not None and "collide(int x)" in int_source + assert double_source is not None and "collide(double x)" in double_source + assert int_source != double_source diff --git a/tests/test_materialized.py b/tests/test_materialized.py index ac162ad..65a0de5 100644 --- a/tests/test_materialized.py +++ b/tests/test_materialized.py @@ -108,6 +108,57 @@ def test_load_source_cfgs_rebuilds_ged_ready_graphs(tmp_path: Path) -> None: assert load_source_cfgs(tmp_path, "O2", "demo") is None +COLLISION_C = """\ +// Function: same@0x1000 @ 0x1000 +int same(int x) { return x + 1; } + +// Function: same@0x2000 @ 0x2000 +int same(int x) { return x + 2; } + +// Function: operator new @ 0x3000 +void *operator_new_stub(unsigned long n) { return 0; } +""" + + +def test_load_decompilation_keeps_storage_keys_and_semantic_names(tmp_path: Path) -> None: + """A collision-qualified artifact must round-trip without leaking its key. + + The dictionary key is the storage key, so two overloads stay distinct. The + ``FunctionDecompilation.name`` field stays a real source name, because + ByteMatch hands it to ``function_bytes`` and to the compile fixup as a C + identifier -- ``same@0x1000`` arriving there breaks the metric rather than + merely looking odd. + """ + artifact = tmp_path / "ghidra_demo.c" + artifact.write_text(COLLISION_C) + + dr = load_decompilation(artifact, "ghidra", tmp_path / "demo") + + assert set(dr.functions) == {"same@0x1000", "same@0x2000", "operator new"} + assert dr.functions["same@0x1000"].name == "same" + assert dr.functions["same@0x2000"].name == "same" + assert dr.functions["same@0x1000"].address == 0x1000 + assert dr.functions["same@0x2000"].address == 0x2000 + assert "x + 1" in dr.functions["same@0x1000"].decompiled_code + assert "x + 2" in dr.functions["same@0x2000"].decompiled_code + + +def test_load_decompilation_reads_a_dwarf_name_containing_a_space(tmp_path: Path) -> None: + """``operator new`` has a space, which the old ``(\\S+)`` marker could not see. + + The marker was invisible to that regex, so the function was dropped from the + artifact entirely rather than mis-keyed. + """ + artifact = tmp_path / "ghidra_demo.c" + artifact.write_text(COLLISION_C) + + dr = load_decompilation(artifact, "ghidra", tmp_path / "demo") + + assert "operator new" in dr.functions + assert dr.functions["operator new"].name == "operator new" + assert dr.functions["operator new"].address == 0x3000 + + def test_discover_tree_projects(tmp_path: Path) -> None: _write_tree(tmp_path) projects, opts = discover_tree_projects(tmp_path) diff --git a/tests/test_metric_function_identity.py b/tests/test_metric_function_identity.py new file mode 100644 index 0000000..cddc28c --- /dev/null +++ b/tests/test_metric_function_identity.py @@ -0,0 +1,100 @@ +"""Metric regressions for collision-qualified function storage keys.""" + +from pathlib import Path + +import networkx as nx + +from decbench.metrics.base import Metric +from decbench.models.decompilation import ( + DecompilationResult, + DecompilerMetadata, + FunctionDecompilation, +) +from decbench.models.metrics import MetricValue + + +class _CfgMetric(Metric): + name = "cfg_identity_test" + requires_source_cfg = True + requires_decompiled_cfg = True + + def compute_for_function( + self, + decompiled: FunctionDecompilation, + source_cfg=None, + decompiled_cfg=None, + **kwargs, + ) -> MetricValue: + assert source_cfg is not None + assert decompiled_cfg is not None + return MetricValue(value=1.0) + + +def _function(name: str, address: int) -> FunctionDecompilation: + return FunctionDecompilation( + name=name, + address=address, + decompiled_code=f"int {name}(void) {{ return 0; }}", + ) + + +def _result(tmp_path: Path) -> DecompilationResult: + result = DecompilationResult( + binary_path=tmp_path / "fixture", + binary_name="fixture", + decompiler=DecompilerMetadata(decompiler_name="identity-test"), + ) + result.add_function(_function("same", 0x1000)) + result.add_function(_function("same", 0x2000)) + return result + + +def _graph() -> nx.DiGraph: + graph = nx.DiGraph() + graph.add_node(0) + return graph + + +def test_cfg_metric_abstains_when_only_ambiguous_plain_name_exists(tmp_path: Path) -> None: + result = _result(tmp_path) + metric = _CfgMetric() + + scored = metric.compute_for_binary( + result, + source_cfgs={"same": _graph()}, + decompiled_cfgs={"same": _graph()}, + ) + + assert scored.function_results == {} + + +def test_cfg_metric_uses_collision_qualified_keys_when_available(tmp_path: Path) -> None: + result = _result(tmp_path) + metric = _CfgMetric() + cfgs = { + "same@0x1000": _graph(), + "same@0x2000": _graph(), + } + + scored = metric.compute_for_binary(result, source_cfgs=cfgs, decompiled_cfgs=cfgs) + + assert set(scored.function_results) == {"same@0x1000", "same@0x2000"} + assert all(value.value == 1.0 for value in scored.function_results.values()) + + +def test_cfg_metric_keeps_legacy_name_lookup_for_unique_function(tmp_path: Path) -> None: + result = DecompilationResult( + binary_path=tmp_path / "fixture", + binary_name="fixture", + decompiler=DecompilerMetadata(decompiler_name="identity-test"), + ) + result.add_function(_function("unique", 0x3000)) + metric = _CfgMetric() + + scored = metric.compute_for_binary( + result, + source_cfgs={"unique": _graph()}, + decompiled_cfgs={"unique": _graph()}, + ) + + assert set(scored.function_results) == {"unique"} diff --git a/tests/test_producer_validation.py b/tests/test_producer_validation.py new file mode 100644 index 0000000..a2b05cf --- /dev/null +++ b/tests/test_producer_validation.py @@ -0,0 +1,196 @@ +"""Bounded producer defects, explicit unknowns, and independent oracle populations.""" + +from __future__ import annotations + +import copy +from pathlib import Path +from types import SimpleNamespace +from typing import Any + +import pytest +from pydantic import ValidationError + +import decbench.metrics.cpp_type_match as cpp_module +import decbench.utils.cfg_identity as cfg_module +import decbench.utils.producer_validation as validation_module +from decbench.metrics.base import MetricConfig +from decbench.metrics.cpp_type_graph import primitive_graph +from decbench.metrics.cpp_type_match import CppTypeMatchMetric +from decbench.metrics.cpp_types import UnsupportedType, parse_type +from decbench.metrics.variable_match import VariableEvidence +from decbench.models.decompilation import ( + DecompilationResult, + DecompilerMetadata, + FunctionDecompilation, + ProducerLanguageContract, + VariableInfo, +) +from decbench.utils.producer_validation import inspect_producer + + +@pytest.mark.parametrize( + "code,kind", + [ + ("int f(){return ::0x1234::call();}", "numeric_qualified_identifier"), + ("int f(){return :: 0x1234 :: call();}", "numeric_qualified_identifier"), + ( + "int f(){int D; return [D] unsupported_Iop_ZeroHI64ofV128();}", + "unsupported_ir_placeholder", + ), + ("int f(){int x; long x; return 0;}", "duplicate_local_binding"), + ("int f(int x){long x;return 0;}", "duplicate_local_binding"), + ("int f(int x,int x){return x;}", "duplicate_local_binding"), + ("int f(){int a,b;char b[8];return 0;}", "duplicate_local_binding"), + ("int f(){struct S *p;void *p;return 0;}", "duplicate_local_binding"), + ], +) +def test_unambiguous_defects(code: str, kind: str) -> None: + inspection = inspect_producer(code) + assert inspection.invalid + assert kind in {finding.kind for finding in inspection.findings} + assert inspection.evidence()["validity"] == "invalid_source_output" + + +@pytest.mark.parametrize( + "code", + [ + "int f(int x){{int x;}return x;}", + "int f(){ {int x;} {int x;} return 0;}", + "int f(){int g(int);int g(int);return 0;}", + "int f(){extern int x;extern int x;return 0;}", + "int f(){typedef int T;typedef int T;return 0;}", + "struct S {int x;int f(int x){return x;}};", + "int f(){int a,b; a*b; a*b; return 0;}", + "int f(){return 0;} int g(){int x;return 0;}", + 'const char *f(){return "::0x1234::call() [D] unsupported_Iop_bad()";}', + 'const char *f(){return R"tag(::0x1234::call() [D] unsupported_Iop_bad())tag";}', + "int f(){/* ::0x1234::bad() */ return 0;}", + "#define EXAMPLE ::0x1234::bad()\nint f(){return 0;}", + "int f(){Foo *p;Foo *p;return 0;}", + "int f(){for(int x=0;x<1;++x){int y;}return 0;}", + ], +) +def test_clean_bounded_inspection_does_not_claim_validity(code: str) -> None: + inspection = inspect_producer(code) + assert not inspection.invalid, inspection.findings + assert inspection.evidence()["validity"] == "not_established" + + +def test_byte_coordinates_and_immutable_contract() -> None: + code = "int f(){ /* 한글 */ int x;\nlong x; return 0;}" + metadata = { + "output_contract": ProducerLanguageContract( + surface="c-like-pseudocode", declarations="partial", dialect="fixture" + ).model_dump() + } + before = copy.deepcopy(metadata) + result = inspect_producer(code, metadata) + finding = result.findings[0] + assert (finding.line, finding.column, finding.previous_line) == (2, 6, 1) + assert metadata == before + assert result.contract.dialect == "fixture" + assert inspect_producer("int f(){return 0;}").contract.surface == "unknown" + + +def test_contract_complete_is_an_assertion_not_a_validity_certificate() -> None: + result = inspect_producer( + "int f(){return 0;}", {"output_contract": {"surface": "cpp", "declarations": "complete"}} + ) + assert result.evidence()["validity"] == "not_established" + + +@pytest.mark.parametrize( + "payload", [{"surface": "invented"}, {"version": "wrong"}, {"extra": True}] +) +def test_invalid_contracts_are_not_silently_recovered(payload: dict[str, Any]) -> None: + with pytest.raises(ValidationError): + inspect_producer("int f(){return 0;}", {"output_contract": payload}) + + +def _result(code: str) -> DecompilationResult: + result = DecompilationResult( + binary_path=Path("unused"), + binary_name="input", + decompiler=DecompilerMetadata(decompiler_name="fixture"), + ) + result.add_function(FunctionDecompilation(name="f", address=16, decompiled_code=code)) + return result + + +def test_invalid_producer_never_reaches_cfg_parser(monkeypatch: pytest.MonkeyPatch) -> None: + result = _result("int f(){int x;long x;return 0;}") + before = result.model_dump() + + def forbidden(*args: Any, **kwargs: Any) -> Any: + raise AssertionError("invalid producer must not reach CFG construction") + + monkeypatch.setattr(cfg_module, "parse_definitions", forbidden) + joined = cfg_module.decompiled_cfgs(result) + assert not joined.cfgs + assert joined.evidence["f"]["status"] == "invalid_producer_output" + assert result.model_dump() == before + + +def test_frontend_rejection_is_not_proof_of_invalid_cpp(monkeypatch: pytest.MonkeyPatch) -> None: + def fail(*args: Any, **kwargs: Any) -> Any: + raise cfg_module.CfgParseError("unsupported known syntax") + + monkeypatch.setattr(cfg_module, "parse_definitions", fail) + result = cfg_module.decompiled_cfgs(_result("int f(int x){return x;}")) + assert result.evidence["f"]["failure_class"] == "frontend_rejected_recognized_syntax" + assert result.evidence["f"]["validation"]["validity"] == "not_established" + + +def test_inspection_fault_is_not_a_producer_or_oracle_exclusion( + monkeypatch: pytest.MonkeyPatch, +) -> None: + def fail(*args: Any, **kwargs: Any) -> Any: + raise ValueError("inspection infrastructure fault") + + monkeypatch.setattr(validation_module, "_parse_source", fail) + with pytest.raises(ValueError, match="infrastructure"): + cfg_module.decompiled_cfgs(_result("int f(){return 0;}")) + + +def test_invalid_recovery_scores_zero_without_shrinking_oracle_population( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + metric = CppTypeMatchMetric( + MetricConfig(use_cache=False, extra_options={"view": "source", "data_model": "LP64"}) + ) + result = _result("int f(int x){long x;return 0;}") + result.binary_path = tmp_path / "unused" + result.functions["f"].variables = [VariableInfo(name="x", type="int", kind="arg", arg_index=0)] + gt = [{"identity": "source:0", "descriptor": primitive_graph(parse_type("int", "LP64"))}] + oracle = SimpleNamespace( + validate=lambda path: None, + native=None, + info=SimpleNamespace(arch="x86_64"), + dwarf=object(), + pdb=None, + index={}, + ) + monkeypatch.setattr(cpp_module, "sanitize_native_provenance", lambda *a, **kw: {}) + monkeypatch.setattr(metric, "_dwarf_rows", lambda *a: gt) + monkeypatch.setattr( + cpp_module, + "build_source_evidence", + lambda *a: SimpleNamespace(variables=[VariableEvidence("source:0", arg_index=0)]), + ) + scored = metric.compute_for_binary( + result, oracle_context=oracle, requested_functions=[("f", 16)] + ) + assert len(scored.function_results) == 1 + assert not scored.errors + value = scored.function_results["f"] + assert value.value == 0.0 + assert value.metadata["ground_truth_count"] == 1 + assert value.metadata["producer_failure"] == "invalid_producer_output" + + def unsupported(*args: Any) -> Any: + raise UnsupportedType("unqualified oracle") + + monkeypatch.setattr(metric, "_dwarf_rows", unsupported) + excluded = metric.compute_for_binary(result, oracle_context=oracle) + assert not excluded.function_results + assert excluded.errors == ["f: unqualified oracle"] diff --git a/tests/test_recompiler_arch.py b/tests/test_recompiler_arch.py index c5df8b7..83aa542 100644 --- a/tests/test_recompiler_arch.py +++ b/tests/test_recompiler_arch.py @@ -3,6 +3,7 @@ from __future__ import annotations import platform +from pathlib import Path from unittest import mock import pytest @@ -14,8 +15,11 @@ ("arch", "expected"), [("x86-64", "gcc"), ("x86", "gcc"), ("aarch64", "aarch64-linux-gnu-gcc")], ) -def test_an_x86_host_is_unchanged(arch: str, expected: str) -> None: - with mock.patch.object(platform, "machine", return_value="x86_64"): +def test_an_x86_linux_host_is_unchanged(arch: str, expected: str) -> None: + with ( + mock.patch.object(platform, "system", return_value="Linux"), + mock.patch.object(platform, "machine", return_value="x86_64"), + ): assert recompiler_for(BinInfo("elf", arch, 64)) == expected @@ -25,35 +29,242 @@ def test_a_non_x86_host_does_not_recompile_x86_with_its_own_gcc() -> None: That fails for every function, scoring 0.0 rather than abstaining, so a retargeted corpus reads as "decompiled fine, bytes never matched". """ - with mock.patch.object(platform, "machine", return_value="aarch64"): + with ( + mock.patch.object(platform, "system", return_value="Linux"), + mock.patch.object(platform, "machine", return_value="aarch64"), + ): assert recompiler_for(BinInfo("elf", "x86-64", 64)) == "x86_64-linux-gnu-gcc" assert recompiler_for(BinInfo("elf", "aarch64", 64)) == "gcc" @pytest.mark.parametrize("host", ["x86_64", "AMD64", "i686", "i386"]) def test_every_intel_host_spelling_builds_x86_natively(host: str) -> None: - """`platform.machine()` says `AMD64` on Windows and `i686` on a 32-bit host. + """Preserve accepted machine spellings on Linux hosts. Missing a spelling costs a real recompile: the host's own `gcc` is there and works, but `byte_match` abstains as if no toolchain existed. """ - with mock.patch.object(platform, "machine", return_value=host): + with ( + mock.patch.object(platform, "system", return_value="Linux"), + mock.patch.object(platform, "machine", return_value=host), + ): assert recompiler_for(BinInfo("elf", "x86", 32)) == "gcc" @pytest.mark.parametrize("host", ["ARM64", "arm64", "aarch64"]) def test_arm_host_spellings_are_native(host: str) -> None: - with mock.patch.object(platform, "machine", return_value=host): + with ( + mock.patch.object(platform, "system", return_value="Linux"), + mock.patch.object(platform, "machine", return_value=host), + ): assert recompiler_for(BinInfo("elf", "aarch64", 64)) == "gcc" def test_a_32_bit_host_cannot_build_the_64_bit_target() -> None: - with mock.patch.object(platform, "machine", return_value="i686"): + with ( + mock.patch.object(platform, "system", return_value="Linux"), + mock.patch.object(platform, "machine", return_value="i686"), + ): assert recompiler_for(BinInfo("elf", "x86-64", 64)) == "x86_64-linux-gnu-gcc" def test_pe_and_bare_metal_arm_are_host_independent() -> None: for host in ("x86_64", "aarch64"): - with mock.patch.object(platform, "machine", return_value=host): + with ( + mock.patch.object(platform, "system", return_value="Linux"), + mock.patch.object(platform, "machine", return_value=host), + ): assert recompiler_for(BinInfo("pe", "x86-64", 64)) == "x86_64-w64-mingw32-gcc" assert recompiler_for(BinInfo("elf", "arm", 32)) == "arm-none-eabi-gcc" + + +@pytest.mark.parametrize("system", ["Darwin", "Windows", "FreeBSD"]) +@pytest.mark.parametrize("machine,arch", [("arm64", "aarch64"), ("AMD64", "x86-64")]) +def test_same_architecture_on_other_os_requires_cross_elf_compiler( + system: str, machine: str, arch: str +) -> None: + with ( + mock.patch.object(platform, "system", return_value=system), + mock.patch.object(platform, "machine", return_value=machine), + ): + assert ( + recompiler_for(BinInfo("elf", arch, 64)) + == {"aarch64": "aarch64-linux-gnu-gcc", "x86-64": "x86_64-linux-gnu-gcc"}[arch] + ) + + +def test_coff_object_identity_requires_a_complete_header(tmp_path: Path) -> None: + import struct + + from decbench.utils.binfmt import object_info + + obj = tmp_path / "unit.obj" + obj.write_bytes(struct.pack(" None: + import shutil + import subprocess + + from decbench.utils.binfmt import object_info + + compiler = shutil.which("clang") + if compiler is None: + pytest.skip("clang required") + source, obj = tmp_path / "unit.c", tmp_path / "unit.o" + source.write_text("int f(int x){return x+1;}\n") + command = [compiler] + if target: + command += ["-target", target] + subprocess.run([*command, "-c", str(source), "-o", str(obj)], check=True, capture_output=True) + actual = object_info(obj) + assert actual is not None + if expected: + assert actual == expected + else: + assert actual.fmt == ( + "macho" + if platform.system() == "Darwin" + else "elf" if platform.system() == "Linux" else "pe" + ) + + +def test_wrong_compiler_object_is_abstention_not_a_byte_score( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from types import SimpleNamespace + + import decbench.metrics.fixup as fixup + from decbench.metrics.byte_match import ByteMatchMetric + from decbench.models.decompilation import FunctionDecompilation + from decbench.utils import binfmt + + obj_dir = tmp_path / "build" + obj_dir.mkdir() + obj = obj_dir / "unit.o" + obj.write_bytes(b"unproven object") + monkeypatch.setattr( + fixup, + "compile_with_fixup", + lambda *a, **kw: SimpleNamespace(obj_path=obj, iterations=0, injected=[]), + ) + monkeypatch.setattr(binfmt, "object_info", lambda path: BinInfo("macho", "aarch64", 64)) + + def forbidden(*args: object) -> None: + raise AssertionError("incompatible object must not reach byte extraction") + + monkeypatch.setattr(binfmt, "object_text_bytes", forbidden) + result = ByteMatchMetric()._compute_uncached( + FunctionDecompilation(name="f", address=16, decompiled_code="int f(){return 0;}"), + b"\x90", + "gcc", + [], + None, + expected_info=BinInfo("elf", "aarch64", 64), + ) + assert result.metadata["skipped"] is True + assert result.metadata["emitted_target"]["fmt"] == "macho" + assert result.metadata["expected_target"]["fmt"] == "elf" + assert not obj_dir.exists() + + +def test_incompatible_target_is_excluded_from_binary_aggregate( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from decbench.metrics.byte_match import ByteMatchMetric + from decbench.models.decompilation import ( + DecompilationResult, + DecompilerMetadata, + FunctionDecompilation, + ) + from decbench.models.metrics import MetricValue + from decbench.utils import binfmt + + monkeypatch.setattr(binfmt, "detect", lambda path: BinInfo("elf", "x86-64", 64)) + monkeypatch.setattr(binfmt, "recompiler_for", lambda *args: "gcc") + monkeypatch.setattr(binfmt, "tool_available", lambda compiler: True) + result = DecompilationResult( + binary_path=tmp_path / "original", + binary_name="original", + decompiler=DecompilerMetadata(decompiler_name="fixture"), + ) + result.add_function( + FunctionDecompilation(name="f", address=16, decompiled_code="int f(){return 0;}") + ) + metric = ByteMatchMetric() + monkeypatch.setattr( + metric, + "compute_for_function", + lambda *a, **kw: MetricValue( + value=0.0, metadata={"skipped": True, "reason": "incompatible compiler target"} + ), + ) + scored = metric.compute_for_binary(result) + assert not scored.function_results + assert scored.errors == ["f: abstained: incompatible compiler target"] + + +def test_compatible_compiler_object_preserves_byte_comparison( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from types import SimpleNamespace + + import decbench.metrics.fixup as fixup + from decbench.metrics.byte_match import ByteMatchMetric + from decbench.models.decompilation import FunctionDecompilation + from decbench.utils import binfmt + + obj_dir = tmp_path / "build" + obj_dir.mkdir() + obj = obj_dir / "unit.o" + obj.write_bytes(b"verified by stub") + info = BinInfo("elf", "x86-64", 64) + monkeypatch.setattr( + fixup, + "compile_with_fixup", + lambda *a, **kw: SimpleNamespace(obj_path=obj, iterations=0, injected=[]), + ) + monkeypatch.setattr(binfmt, "object_info", lambda path: info) + monkeypatch.setattr(binfmt, "object_text_bytes", lambda *a: b"\x90") + result = ByteMatchMetric()._compute_uncached( + FunctionDecompilation(name="f", address=16, decompiled_code="int f(){return 0;}"), + b"\x90", + "gcc", + [], + None, + expected_info=info, + ) + assert result.value == 1.0 + assert result.metadata["exact_match"] is True + assert not obj_dir.exists() + + +def test_elf_object_bitness_comes_from_file_class(tmp_path: Path) -> None: + import struct + + from decbench.utils.binfmt import object_info + + obj = tmp_path / "x32.o" + header = bytearray(64) + header[:6] = b"\x7fELF\x01\x01" + struct.pack_into(" FunctionDecompilation: + return FunctionDecompilation( + name=name, + address=address, + decompiled_code=body or f"int {name}(void) {{ return 1; }}", + ) + + +def _result(*functions: FunctionDecompilation, failed: list[str] | None = None): + return DecompilationResult( + binary_path=Path("stripped.bin"), + binary_name="fixture", + decompiler=DecompilerMetadata( + decompiler_name="fixture", + failed_functions=list(failed or []), + ), + functions={f"raw-{index}": function for index, function in enumerate(functions)}, + ) + + +def test_relabel_uses_full_target_set_for_ambiguous_storage_keys(monkeypatch): + driver = _load_driver() + from decbench.decompilers.raw import common + + monkeypatch.setattr(common, "elf_min_vaddr", lambda _path: 0) + targets = {0x1000: "same", 0x2000: "same", 0x3000: "unique"} + + complete = _result(_fd("sub_1000", 0x1000), _fd("sub_2000", 0x2000)) + partial = _result(_fd("FUN_1000", 0x1000), _fd("FUN_3000", 0x3000)) + + driver._relabel_to_dwarf(complete, targets, Path("unstripped.bin")) + driver._relabel_to_dwarf(partial, targets, Path("unstripped.bin")) + + assert set(complete.functions) == {"same@0x1000", "same@0x2000"} + assert set(partial.functions) == {"same@0x1000", "unique"} + assert partial.functions["same@0x1000"].name == "same" + assert partial.functions["same@0x1000"].address == 0x1000 + assert partial.decompiler.failed_functions == ["same@0x2000"] + assert partial.decompiler.extra["function_identity"] == "dwarf-low-pc" + + +def test_relabel_normalizes_historical_pe_rva_to_dwarf_low_pc(monkeypatch): + driver = _load_driver() + from decbench.decompilers.raw import common + + monkeypatch.setattr(common, "elf_min_vaddr", lambda _path: 0x400000) + result = _result(_fd("sub_1000", 0x1000)) + + driver._relabel_to_dwarf(result, {0x401000: "target"}, Path("unstripped.exe")) + + assert set(result.functions) == {"target"} + assert result.functions["target"].address == 0x401000 + assert result.functions["target"].name == "target" + assert "target" in result.functions["target"].decompiled_code + assert result.decompiler.failed_functions == [] + + +def test_relabel_preserves_compact_all_failure_marker(monkeypatch): + driver = _load_driver() + from decbench.decompilers.raw import common + + monkeypatch.setattr(common, "elf_min_vaddr", lambda _path: 0) + result = _result(failed=["all"]) + + driver._relabel_to_dwarf( + result, + {0x1000: "same", 0x2000: "same"}, + Path("unstripped.bin"), + ) + + assert result.functions == {} + assert result.decompiler.failed_functions == ["all"] + + +@pytest.mark.skipif(shutil.which("g++") is None, reason="g++ required") +def test_source_extract_accepts_canonical_storage_key(tmp_path: Path): + source = tmp_path / "collision.cpp" + source.write_text( + "int collide(int x) { return x + 11; }\n" + "double collide(double x) { return x + 22.0; }\n" + "int main() { return collide(1); }\n" + ) + binary = tmp_path / "collision" + subprocess.run( + ["g++", "-O0", "-g", str(source), "-o", str(binary)], + check=True, + capture_output=True, + text=True, + ) + + identities = dwarf_function_identities(binary) + int_overload = next(i for i in identities if i.linkage_name == "_Z7collidei") + storage_key = f"collide@0x{int_overload.address:x}" + + snippet, status = function_source_ex(binary, storage_key) + + assert status == "" + assert snippet is not None + assert "return x + 11" in snippet + assert "return x + 22.0" not in snippet diff --git a/tests/test_type_match_correspondence.py b/tests/test_type_match_correspondence.py index 4aac9cf..ff58220 100644 --- a/tests/test_type_match_correspondence.py +++ b/tests/test_type_match_correspondence.py @@ -294,4 +294,4 @@ def test_direct_call_defaults_to_generic_fallback_but_can_request_address_mode() def test_address_matcher_uses_new_cache_generation() -> None: - assert TypeMatchMetric.cache_version == "17" + assert TypeMatchMetric.cache_version == "25"