"""Local Gradio demo. Run inside the Linux x86-64 Docker environment.""" import argparse import json from contextlib import closing from dataclasses import asdict from hashlib import sha256 from decimal import Decimal import re import shutil from pathlib import Path import verifier import shlex import tempfile import gradio as gr from demo import examples as example_catalog from demo.examples import EXAMPLE_LABELS, ROOT, prepare_examples, reference_sources, example_groups from demo.presentation import banner, format_bit_ranges, json_detail, mismatch_summary, status from demo.session import (UISession, begin_ui, cancel_session, controls, edit_inputs, idle_controls, reset_session, stream_ui) import verifier as api from demo.execution import verification_reports from verifier import (Task, ExecutionConfig, SamplingConfig, GuidanceConfig, ConcolicConfig, InputRecipe, integer_text, parse_bindings, parse_function, yaml_mapping) HEADERS = ["Execution", "Source", "Verdict", "Location", "Differing bits", "Original", "Candidate", "Reason"] def integer(value, label): if type(value) is int: return value if isinstance(value, float) and value.is_integer(): return int(value) if isinstance(value, str): try: if value.strip().lower().startswith("0x"): return int(value.strip(), 16) if re.fullmatch(r"[+-]?[0-9]+", value.strip()): return int(Decimal(value.strip())) except ValueError: pass raise ValueError(f"{label} must be an integer") def execution_source(report): if report.task.get("scheduler") == "fixed_input_replay": return "Replay" testcases = getattr(report, "testcases", ()) origin = testcases[-1].origin if testcases else "random" if origin == "generated": return "Concolic" if origin == "replay": return "Replay" if report.task.get("execution", {}).get("mode") == "comparison_guided": return "Comparison guided" return "Random" def result_row(result, execution_index=None, source="Random"): diff = result.difference bits = format_bit_ranges((span.first, span.last) for span in getattr(diff, "bit_ranges", ())) original, candidate = getattr(diff, "original", ""), getattr(diff, "candidate", "") if isinstance(original, int): original, candidate = hex(original), hex(candidate) return [integer_text(result.seed if execution_index is None else execution_index), source, result.verdict, getattr(diff, "location", ""), bits, original, candidate, result.reason or result.original.reason or result.candidate.reason or ""] def task_from_inputs(binary, function, candidate, bindings, candidate_symbol, execution_count, seed_start, instruction_budget, compiler, flags, interfaces="", execution_mode="comparison_guided", null_probability=0, repeated_branch_threshold=8, max_learned_probability=0.75, replay_recipe=None, max_new_testcases_per_run=8, solver_timeout_ms=1000, random_input_probability=0.25): if not binary: raise ValueError("upload a binary or select an example") options = yaml_mapping(interfaces or "") allowed = {"original_prototype", "original_cc", "candidate_cc", "variadic_types", "callee_interfaces", "memory_ranges", "original_base"} if set(options) - allowed: raise ValueError("unknown interface settings: " + ", ".join(sorted(set(options) - allowed))) sampling = (SamplingConfig(null_probability, null_probability, null_probability) if execution_mode == "random" else SamplingConfig()) guidance = (GuidanceConfig(integer(repeated_branch_threshold, "repetition threshold"), max_learned_probability) if execution_mode == "comparison_guided" else GuidanceConfig()) concolic = (ConcolicConfig(integer(max_new_testcases_per_run, "max new testcases per run"), integer(solver_timeout_ms, "solver timeout"), random_input_probability) if execution_mode == "concolic" else ConcolicConfig()) return Task(binary=Path(binary), function=parse_function(function), candidate=candidate, bindings=parse_bindings(bindings), candidate_symbol=candidate_symbol.strip(), execution_count=integer(execution_count, "execution count"), seed_start=integer(seed_start, "seed start"), instruction_budget=integer(instruction_budget, "instruction budget"), compiler=compiler, flags=tuple(shlex.split(flags)), execution=ExecutionConfig(execution_mode, sampling, guidance, concolic), **options) def load_recipe(path): data = json.loads(Path(path).read_text(), parse_int=lambda token: int(Decimal(token))) if isinstance(data, dict) and "recipes" in data: recipes = data["recipes"] if not isinstance(recipes, list) or not recipes: raise ValueError("the report has no saved input recipes") data = recipes[0] return InputRecipe.from_mapping(data) def clear_outputs(): return "", [], None, None, "", "" def show_assembly(binary, function, candidate, bindings, candidate_symbol, execution_count, seed_start, instruction_budget, compiler, flags, interfaces="", execution_mode="comparison_guided", null_probability=0, repeated_branch_threshold=8, max_learned_probability=0.75, replay_recipe=None, max_new_testcases_per_run=8, solver_timeout_ms=1000, random_input_probability=0.25): try: report = api.inspect(task_from_inputs(binary, function, candidate, bindings, candidate_symbol, execution_count, seed_start, instruction_budget, compiler, flags, interfaces, execution_mode, null_probability, repeated_branch_threshold, max_learned_probability, replay_recipe, max_new_testcases_per_run, solver_timeout_ms, random_input_probability)) yield banner(report, preview=True), [], None, None, report.original_assembly, report.candidate_assembly except (ValueError, OSError) as error: raise gr.Error(str(error)) from error def run_verification(binary, function, candidate, bindings, candidate_symbol, execution_count, seed_start, instruction_budget, compiler, flags, interfaces="", execution_mode="comparison_guided", null_probability=0, repeated_branch_threshold=8, max_learned_probability=0.75, replay_recipe=None, max_new_testcases_per_run=8, solver_timeout_ms=1000, random_input_probability=0.25, *, report_directory=None, cancelled=None): """Translate UI inputs, stream engine results, then supply the JSON download.""" try: task = task_from_inputs(binary, function, candidate, bindings, candidate_symbol, execution_count, seed_start, instruction_budget, compiler, flags, interfaces, execution_mode, null_probability, repeated_branch_threshold, max_learned_probability, replay_recipe, max_new_testcases_per_run, solver_timeout_ms, random_input_probability) recipe = load_recipe(replay_recipe) if replay_recipe else None rows, detail, report = [], None, None with closing(verification_reports(task, cancelled=cancelled, recipe=recipe)) as iterator: for report in iterator: rows.append(result_row(report.results[-1], len(rows) + 1, execution_source(report))) if detail is None and report.first_mismatch is not None: detail = asdict(report.first_mismatch) yield banner(report), list(rows), detail, None, report.original_assembly, report.candidate_assembly if cancelled is not None and cancelled.is_set(): return output = report.to_json() directory = Path(report_directory) if report_directory else Path(tempfile.gettempdir()) / "decompile-verify-reports" directory.mkdir(parents=True, exist_ok=True) path = directory / (sha256(output.encode()).hexdigest() + ".json") # Atomic writes allow concurrent requests for the same deterministic report. with tempfile.NamedTemporaryFile(mode="w", dir=directory, delete=False) as stream: stream.write(output) temporary = Path(stream.name) temporary.replace(path) yield banner(report, complete=True), rows, detail, str(path), report.original_assembly, report.candidate_assembly except (ValueError, OSError) as error: raise gr.Error(str(error)) from error class Workbench(gr.Blocks): """Apply the workbench theme even when a caller launches create_app() directly.""" def launch(self, **kwargs): kwargs.setdefault("theme", gr.themes.Base( primary_hue="teal", secondary_hue="teal", neutral_hue="slate", font=["system-ui", "sans-serif"], font_mono=["ui-monospace", "monospace"], )) kwargs.setdefault("css_paths", Path(__file__).with_name("workbench.css")) kwargs.setdefault("js", Path(__file__).with_name("workbench.js").read_text()) blocked = list(kwargs.pop("blocked_paths", ()) or ()) blocked.extend([str(Path(verifier.__file__).resolve().parent), str(ROOT / "core"), str(ROOT / ".git")]) helper = shutil.which("decompile-verify-declarations") if helper: blocked.append(str(Path(helper).resolve())) kwargs["blocked_paths"] = blocked return super().launch(**kwargs) def create_app(examples_directory=None): examples = prepare_examples(examples_directory or ROOT / "build" / "demo-examples") references = reference_sources() with Workbench(title="Decompiler verifier", analytics_enabled=False, fill_width=True, delete_cache=(3600, 86400)) as app: session = gr.State(UISession()) ticket = gr.State(None) reference_function = gr.State(None) gr.Markdown("""# Decompiler verifier This tool takes an **executable file**, a **function in that executable**, and a **candidate C implementation**, and attempts to determine whether the candidate and the original function are equivalent. It compiles the candidate C, runs both functions on the same seeded random inputs, and compares their behavior. A mismatch shows where they differ; agreement means they matched on the tested inputs within the comparison scope described below. **Quick start:** Pick an **example** below and click **Verify**. """, elem_id="intro") with gr.Accordion("About bindings, run settings, and your own inputs", open=False): gr.Markdown("""Open **Bindings** to map candidate `extern` names to original binary addresses: `globals` for shared data and `callees` for called functions. Bound callees receive deterministic synthetic returns. In the same panel, `original_prototype` is the **trusted prototype**: a C declaration describing the original function's argument and return types, such as `unsigned original(unsigned x);`. Supply it from your knowledge of the binary, independently of the candidate's guessed types; it controls input generation and return interpretation. The examples supply it. You can also specify callee interfaces and memory ranges there. Open **Run settings** to adjust seeds, instruction budget, compiler, or flags, then click **Verify**. Inspect the assembly panes, per-seed results, and first mismatch, or download the JSON report. Agreement covers the tested inputs and comparison scope described below. **Try changing the Candidate C**—an operator, constant, or type—and verify again with the same seeds to see how the assembly and results change. For your own input, upload an **x86-64 ELF binary**, enter the **function start address**, and paste complete **Candidate C**. Set **Candidate function name** to the C function to test. The examples include matching implementations, deliberate bugs, real `factor` binary functions, paper and artifact cases under **Related work**, and saved **Ghidra exports** in expandable sections. Some Ghidra exports agree; others reveal mismatches or compile/type errors. **Reference C**, when shown, is a reading aid; verification runs the binary. """) # Render inputs later so the example picker can sit above setup. binary = gr.File(label="Binary", type="filepath", file_count="single", render=False, elem_id="binary") function = gr.Textbox(label="Function start address", placeholder="0x401000", info="angr recovers the function's blocks using available symbols and control flow.", render=False) candidate_symbol = gr.Textbox(label="Candidate function name", value="target", render=False) candidate = gr.Code(label="Candidate C", language="c", lines=20, wrap_lines=False, buttons=["copy"], render=False, elem_id="candidate-source") bindings = gr.Code(label="Bindings (YAML)", language="yaml", lines=4, value="globals: {}\ncallees: {}\n", render=False) execution_count = gr.Number(label="Number of executions", value=100, precision=0, minimum=1, render=False) seed_start = gr.Textbox(label="Initial seed", value="0", info="Nonnegative decimal or hex integer", render=False) instruction_budget = gr.Number(label="Instruction budget per run", value=1000, precision=0, minimum=1, render=False) compiler = gr.Dropdown(label="Candidate compiler", choices=["gcc", "clang"], value="gcc", render=False) flags = gr.Textbox(label="Candidate flags", value="-O2", info="Optimization/debug flags and selected C options; see docs/demo.md.", render=False) interfaces = gr.Code(label="Function interfaces and explicit memory ranges (YAML)", language="yaml", value="", lines=5, render=False) example_inputs = [binary, function, candidate, bindings, candidate_symbol, execution_count, seed_start, instruction_budget, compiler, flags, interfaces] execution_mode = gr.Dropdown(label="Execution mode", choices=["random", "comparison_guided", "concolic"], value="comparison_guided", render=False, info="random samples inputs with a configurable NULL probability. " "comparison_guided learns preferred values from branch comparisons. " "concolic solves branch conditions to generate testcases.") null_probability = gr.Slider(label="NULL probability", minimum=0, maximum=1, value=0, step=0.01, visible=False, render=False, info="Chance of choosing NULL (zero) for each new argument value, lazy memory word, " "or call response. Otherwise, sample randomly. Uses zero for non-pointer values.") repeated_branch_threshold = gr.Number(label="Repeated branch threshold", value=8, precision=0, minimum=1, render=False, info="Consecutive identical outcomes at the same branch before learning " "preferred values. Lower values start learning sooner.") max_learned_probability = gr.Slider(label="Learned-value probability", minimum=0, maximum=1, value=0.75, step=0.01, render=False, info="Once relevant values are learned, chance of choosing one of them. " "Otherwise, sample randomly.") replay_recipe = gr.File(label="Replay recipe or report (JSON)", type="filepath", file_types=[".json"], render=False) max_new_testcases_per_run = gr.Number(label="Max new testcases per run", value=8, precision=0, minimum=1, render=False) solver_timeout_ms = gr.Number(label="Solver timeout per query (ms)", value=1000, precision=0, minimum=1, render=False) random_input_probability = gr.Slider(label="Fresh random input probability", minimum=0, maximum=1, value=0.25, step=0.01, render=False) mode_inputs = [execution_mode, null_probability, repeated_branch_threshold, max_learned_probability, replay_recipe, max_new_testcases_per_run, solver_timeout_ms, random_input_probability] inputs = [*example_inputs, *mode_inputs] gr.Markdown("### Examples\nOpen a section and select an example.", elem_id="examples") gr.HTML('