diff --git a/AGENTS.md b/AGENTS.md index 4e7f9c2..2c79a7a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,178 +1,243 @@ # Agent Guidelines for r2sleigh -> LLM-focused working notes for contributors and coding agents. +> LLM-focused working rules for contributors and coding agents. -## Project Summary +## North Star -`r2sleigh` is the Sleigh-backed analysis and decompiler pipeline for radare2. +`r2sleigh`, `r2ssa`, `r2sym`, `r2types`, `r2dec`, `r2plugin`, and +`../radare2` are one subsystem. -```text -.sla (Ghidra) --> libsla --> P-code --> r2il --> ESIL - | - +--> SSA (r2ssa) - +--> Type inference (r2types) - +--> Symbolic / taint (r2sym) - +--> Decompiler (r2dec) - +--> Plugin / CLI export surfaces -``` +The goal is not "more commands" or "more crates doing similar work." The goal +is a gold-standard radare2 analysis engine where: -The repository is no longer just "P-code to ESIL". A lot of current work lands in SSA, symbolic execution, type inference, decompilation, and radare2 integration layers. +- one canonical fact has one canonical owner +- facts flow through typed contracts, not JSON reparsing +- decompiler, types, symbolic execution, and radare2 core views agree +- expensive work is summarized, cached, and reused +- output is deterministic +- architecture and API seams may be rewritten whenever the rewrite is cleaner -Treat this workspace and `../radare2` as one analysis system. `r2sleigh` is part of the radare2 analysis/decompiler stack, not a separate sidecar that should paper over missing library seams at the end of the pipeline. +The plugin should feel like radare2 itself got smarter, not like radare2 grew a +second shell. -The opening sections are the operational rules. Later sections are reference material. +## Non-Negotiables -## Fast Path Rules +1. One fact, one owner. +2. Treat this repo and `../radare2` as one component boundary. +3. `r2plugin` is orchestration/FFI glue only. +4. Do not reconstruct missing semantics downstream. +5. Prefer typed contracts over JSON blobs and stringly maps. +6. `r2types::FunctionFacts` is the canonical combined type+semantic contract. +7. `r2types::FunctionTypeFacts` is the canonical type/layout/signature payload. +8. `r2sym::SemanticArtifact` is the canonical semantic artifact. +9. `r2sym` owns semantic policy and evidence; consumers interpret it. +10. Deterministic ordering beats cleverness. +11. Rewrite bad seams instead of patching around them. +12. Validation is part of the change, not optional cleanup. -1. Find the canonical owner of a fact before editing code. Cross-crate cooperation is expected; duplicated policy is not. -2. Treat this workspace and `../radare2` as one component boundary. If the right fix belongs in radare2, implement it there instead of patching around it in Rust. -3. `r2plugin` is orchestration/FFI glue only. Do not fix missing semantics by reparsing command output or merging policy there. -4. For decompiler/type context, do not add new plugin-side parsing of `afcfj`, `afvj`, or `tsj`. If the seam is wrong, fix it in `../radare2` and keep the plugin on the typed collector path. -5. `r2types::FunctionTypeFacts` is the only canonical type/layout/signature contract on the decompiler path. -6. Default to `tests/r2r` for new plugin regressions and command-output checks. Do not add new snapshot-style plugin tests to `tests/e2e/integration_tests.rs` unless `r2r` genuinely cannot express the case. -7. Build and run commands in this repo have drifted over time. Prefer the commands in this file over older examples. -8. File paths below use the current `src/` layout. Older references like `r2plugin/lib.rs` are stale. -9. Architecture feature support differs by crate. Check the relevant `Cargo.toml` before documenting or wiring a new arch. -10. When the current architecture blocks a clean design, rewrite the seam or crate API instead of adding end-stage hacks. Large refactors, FFI changes, and cross-repo redesigns are acceptable when they reduce long-term complexity. +## Optimization Doctrine -## Task-Start Protocol +Low-quality implementations tend to optimize the wrong thing. Do not do that. -For any non-trivial change, follow this order: +The target is not fantasy "`O(1)` symex." The target is: -1. Identify the user-visible behavior or broken invariant. -2. Decide which layer should own the fix: `../radare2`, lifting, SSA, symex, types, decompiler, export, CLI, or plugin glue. -3. Look for an existing typed contract to extend before adding new JSON blobs, stringly maps, or parallel wrapper types. -4. Push facts upstream to the canonical owner instead of reconstructing them downstream. -5. Add the smallest deterministic test at the layer users actually exercise. -6. If the seam crosses into `../radare2`, validate both repos before claiming the fix is complete. - -## Change Placement Guide - -Use this as the default "where should this logic live?" map: - -- `../radare2`: core analysis facts, typed collectors, and library seams that should exist for radare2 consumers generally. -- `crates/r2sleigh-lift`: Sleigh/P-code lifting, disassembly, register naming, text formatting, and ESIL formatting. -- `crates/r2ssa`: SSA construction, phi handling, dominators, def-use, determinism, and SSA-local transforms. -- `crates/r2sym`: symbolic execution, taint propagation, summaries, path exploration, and solver-facing symbolic policy. -- `crates/r2types`: signature parsing, type inference, layout inference, external context normalization, and canonical merged type facts. -- `crates/r2dec`: lowering, semantic interpretation, structuring, folding, and rendering. -- `crates/r2sleigh-export` and `crates/r2sleigh-cli`: shared export plumbing and CLI surface behavior. -- `r2plugin`: command dispatch, FFI, serialization, and radare2 integration glue only. - -## Workspace Layout - -```text -crates/ -├── r2il/ # Core IL types and serialization -├── r2sleigh-lift/ # Sleigh/P-code lifting, disassembly, ESIL formatting -├── r2sleigh-export/ # Unified export pipeline for lift/ssa/defuse/dec -├── r2sleigh-cli/ # Standalone CLI -├── r2ssa/ # SSA form, dominators, def-use, optimization -├── r2sym/ # Symbolic execution, taint, summaries, solving -├── r2types/ # Type inference and signatures -└── r2dec/ # Decompiler AST, folding, lowering, codegen -r2plugin/ # Rust cdylib + C radare2 wrapper -tests/ -├── r2r/ # Preferred snapshot and command regression suite -└── e2e/ # Rust semantic/FFI/benchmark suite and fixture binaries -``` +- `O(1)` or `O(log n)` lookup for metadata, indexes, summaries, and caches +- `O(n)` passes over blocks, SSA ops, or facts whenever possible +- bounded search when search is unavoidable +- incremental recomputation instead of whole-function replay +- summary reuse across query, typing, and decompilation +- explicit budgets for solver work, symbolic exploration, and structuring -## Whole-System Architecture Stance +Use this mental model: -`r2il`, `r2sleigh-lift`, `r2ssa`, `r2sym`, `r2types`, `r2dec`, `r2sleigh-export`, `r2plugin`, and `../radare2` should be treated as one radare2 analysis/decompiler subsystem with explicit internal ownership. +- repeated whole-function scans are a smell +- repeated solver queries for the same fact are a smell +- recomputing downstream what already exists upstream is a smell +- parallel representations of the same fact are a bug +- hash-order-dependent output is a bug -- Optimize for clean typed seams and correct ownership, not for preserving historical plugin boundaries. -- `r2plugin` is an integration surface, not the place to recover missing semantics from downstream command output. -- If a clean solution requires moving logic across crates or across the `../radare2` boundary, do it at the owning layer. -- Prefer principled rewrites over incremental "fix it later in the plugin/decompiler/export path" patches. -- Cross-component cooperation is required; cross-component policy duplication is not. +If you cannot explain the asymptotic and practical cost of a new analysis path, +you do not understand it well enough to land it. -## Ownership Boundaries +### Efficiency Rules -These boundaries are the main architectural guardrail. The crates should work together as one pipeline, but each fact still needs one canonical owner. Most recent churn happened when multiple crates tried to "help" each other by owning the same policy. +1. Prefer canonical summaries over re-analysis. +2. Prefer incremental updates over full rebuilds. +3. Prefer typed caches over ad hoc memoization. +4. Prefer `BTreeMap` / `BTreeSet` when ordering affects output or tests. +5. Prefer one richer pass over many weak overlapping passes. +6. Prefer explicit budgets and refusal modes over silent blowups. +7. Prefer stronger upstream facts over downstream heuristics. -- `r2ssa` owns SSA construction, decompile-safe SSA preparation, determinism, and SSA-local transforms. -- `r2sym` owns symbolic state modeling, taint propagation, summaries, path exploration, and solver-facing symbolic policy. -- `r2types` owns signature parsing/normalization, type inference, layout inference, external context parsing, field lookup policy, and canonical merged type facts. -- `r2dec` owns decompiler semantic facts, lowering, structuring, and rendering only. -- `r2plugin` owns orchestration, JSON/FFI, command dispatch, and radare2 integration glue only. -- `../radare2` owns core analysis facts, typed collectors, and library seams that should exist for radare2 consumers generally, not just this workspace. +## Architectural Stance -Do not put the same policy in two crates "temporarily". That temporary state lasted a long time and created most of the recent regressions. +This system should move toward a gold-standard analysis architecture even when +that requires invasive refactors. -When multiple layers need the same fact, push that fact toward its canonical owner and expose it through a typed contract instead of rebuilding it independently in each crate. +- It is acceptable to redesign contracts, move logic across crates, or change + FFI and `../radare2` seams when the result is cleaner. +- Do not preserve a bad abstraction because it already exists. +- Do not add end-stage hacks in `r2plugin` or `r2dec` to hide missing upstream + semantics. +- If a crate is carrying policy it should not own, move that policy. -## Typed `radare2` Context Seam +The right question is not "what is the smallest diff?" It is "what is the +cleanest owner and the cheapest long-term design?" -The plugin used to pull decompiler/type context by shelling out to radare2 commands and re-parsing JSON from `afcfj`, `afvj`, and `tsj`. That was convenient, but it created duplicate ownership and brittle parsing policy in the plugin. +## Task-Start Protocol -Current rule: +For any non-trivial change, follow this order: + +1. Identify the user-visible behavior or broken invariant. +2. Identify the canonical owner: `../radare2`, lift, SSA, symex, types, + decompiler, export, CLI, or plugin. +3. Identify the existing typed contract to extend before creating a new one. +4. State the complexity target: lookup, traversal, search, cache, summary. +5. Push facts upstream to the owner instead of reconstructing them downstream. +6. Add the smallest deterministic test at the layer users actually exercise. +7. If the seam crosses into `../radare2`, validate both repos before claiming + the work is complete. -- For decompiler/type analysis, use the typed function/base-type collector API in `../radare2`. -- Keep user-visible commands like `afcfj`, `afvj`, and `tsj`, but do not use them as an internal plugin data source. -- If the plugin lacks a typed field from radare2, add it to the `r_anal` API instead of layering more command parsing into `r2plugin`. -- Prefer one consolidated external-context payload across the C/Rust FFI boundary rather than multiple partially overlapping JSON blobs. -- Treat `../radare2` as available for coordinated changes. If the right fix needs new FFI, collector APIs, or analysis metadata, add them there and wire them through cleanly. +## Ownership Boundaries -This seam exists to keep `r2plugin` orchestration-only and to keep type/signature/layout policy out of the plugin. +Use this map by default: + +- `../radare2` + - core analysis facts + - typed collectors + - native analysis metadata and persistence + - library seams that should exist for radare2 consumers generally +- `crates/r2il` + - canonical IL data model and serialization +- `crates/r2sleigh-lift` + - Sleigh/P-code lifting + - register naming + - disassembly formatting + - ESIL formatting +- `crates/r2ssa` + - SSA construction + - phi handling + - dominators / def-use + - prepared function facts + - determinism and SSA-local transforms +- `crates/r2sym` + - symbolic state + - semantic artifacts + - evidence algebra + - query planning + - summaries and replay + - solver-facing semantic policy +- `crates/r2types` + - signature parsing and normalization + - type inference + - layout inference + - external type context normalization + - canonical `FunctionTypeFacts` + - canonical combined `FunctionFacts` +- `crates/r2dec` + - lowering + - semantic interpretation of canonical facts + - structuring + - rendering +- `crates/r2sleigh-export` / `crates/r2sleigh-cli` + - shared export/CLI plumbing +- `r2plugin` + - command dispatch + - JSON shaping + - FFI + - radare2 integration glue + +Do not let the same policy exist in two crates "for now." + +## Canonical Contracts + +These are the preferred subsystem seams: + +- `r2ssa::SsaArtifact` and `PreparedFunctionFacts` + - canonical SSA/dataflow preparation +- `r2sym::SemanticArtifact` + - canonical semantic artifact +- `r2sym::SemanticEvidence` + - canonical evidence carrier +- `r2sym::{ArtifactBuildPlan, QueryPlan, TargetQueryRoutePlan, TypePlan, DecompilePlan}` + - canonical plan surfaces +- `r2types::FunctionTypeFacts` + - canonical type/layout/signature payload +- `r2types::FunctionFacts` + - canonical combined type+semantic payload +- `r2dec::SemanticRoutePlan` + - renderer route selected from canonical upstream capabilities + +If a caller needs more information, extend these contracts instead of creating +parallel wrappers. + +## Typed `radare2` Seam + +The plugin must not parse `afcfj`, `afvj`, `tsj`, or similar command output as +an internal data source. + +Rules: + +- use the typed function/base-type collector APIs in `../radare2` +- keep user-visible commands, but do not use them as plugin internals +- if a typed field is missing, add it in `../radare2` +- prefer one consolidated typed context payload over multiple overlapping JSON + blobs + +If the right fix belongs in `../radare2`, implement it there. + +## Plugin Philosophy + +The public product should be workflow-oriented, not command-oriented. + +The plugin should: + +- improve `aa`, `af`, `pdfj`, `pdd`, type views, and existing radare2 analysis + surfaces +- keep a small public command surface +- treat engine-inspection commands as debug/maintainer tools +- move knobs to config (`e anal.sleigh.*`) where that is cleaner than inventing + verbs + +If you are about to add a new command, stop and ask: + +1. should this be automatic? +2. should this enrich an existing radare2 view instead? +3. should this be config rather than a verb? +4. is this just a debug surface? -## Canonical Type Contract +## Rewrite Bias -`r2types::FunctionTypeFacts` is the only canonical type/layout/signature artifact for the decompiler path. +When current architecture blocks correctness, composability, or efficiency: -- `r2types` may consume local inference artifacts, radare2 external context, and decompiler-emitted semantic field-access facts. -- `r2dec` should consume `FunctionTypeFacts` only. Do not add back public `TypeInference`, duplicate `FunctionType`, or decompiler-side external-signature / stack-var setters. -- `r2plugin` may gather inputs and serialize outputs, but it must not own signature merge policy, external/local struct reconciliation, or layout query policy. +- rewrite the seam +- move the owner +- shrink duplicated policy +- reshape FFI if needed +- change module layout if needed -If a caller needs more type information, extend `FunctionTypeFacts` or the `r2types` query contract instead of creating a second type wrapper layer elsewhere. +Avoid: -## Rewrite Bias +- plugin-side reparsing +- decompiler-side type policy +- consumer-local semantic policy that should live in `r2sym` +- compatibility shims that silently become permanent -When the current architecture blocks correctness, composability, or maintainability: - -- Prefer rewriting the seam, contract, or owning crate over adding compensating logic at the end of the pipeline. -- It is acceptable to redesign APIs, move logic across crates, reshape FFI, or perform cross-repo refactors involving `../radare2`. -- Do not preserve a bad abstraction just because it already exists. -- The goal is a better end-to-end radare2 component, not the smallest possible diff. -- Avoid plugin-side or decompiler-side "final fixups" that only exist to hide missing upstream semantics. - -## Rust Typing Rules For Rewrites - -Recent work went better once ownership seams stopped relying on stringly maps and implicit conventions. - -- Prefer enums, newtypes, named structs, and small typed input structs over raw `HashMap` plus comments. -- Use traits at crate seams where the contract matters, for example layout queries or semantic fact access. -- Use `BTreeMap` / `BTreeSet` whenever iteration order can affect decompiler output, local naming, snapshots, or SSA determinism. -- Use `From` / `TryFrom` for typed boundary conversions instead of ad hoc parsing spread across crates. -- Keep JSON-only types at the JSON boundary. Internal analysis code should use typed Rust models first and serialize late. -- Avoid boolean mode flags when there are more than two semantic states; use enums instead. - -## Core Types and Entry Points - -| Type / Function | Location | Purpose | -|-----------------|----------|---------| -| `Varnode` | `crates/r2il/src/varnode.rs` | Sized data location: reg/mem/const/unique | -| `SpaceId` | `crates/r2il/src/space.rs` | Address-space enum | -| `R2ILOp` | `crates/r2il/src/opcode.rs` | Semantic IL op enum | -| `R2ILBlock` | `crates/r2il/src/opcode.rs` | One-instruction IL block | -| `ArchSpec` | `crates/r2il/src/serialize.rs` | Architecture metadata | -| `Disassembler` | `crates/r2sleigh-lift/src/disasm.rs` | libsla wrapper and P-code lifting | -| `format_op()` / `op_to_esil()` | `crates/r2sleigh-lift/src/esil.rs` | Text and ESIL formatting | -| `run_action_output()` | `crates/r2sleigh-cli/src/main.rs` | CLI action/format dispatcher | -| export helpers | `crates/r2sleigh-export/src/lib.rs` | Shared export pipeline used by CLI/plugin | -| `SSAVar` | `crates/r2ssa/src/var.rs` | Versioned SSA variable | -| `SSAOp` | `crates/r2ssa/src/op.rs` | SSA operation enum | -| `to_ssa()` | `crates/r2ssa/src/block.rs` | R2IL block -> SSA block | -| `DefUseInfo` | `crates/r2ssa/src/defuse.rs` | Def-use analysis result | -| `FunctionSSABlock` / `SSAFunction` | `crates/r2ssa/src/function.rs` | Function-level SSA with phi nodes | -| `AnalysisResult` and type passes | `crates/r2types/src/` | Type inference payloads | -| `CExpr` / `CStmt` | `crates/r2dec/src/ast.rs` | Decompiler AST | -| `FoldingContext` | `crates/r2dec/src/fold/` | Expression folding and simplification | -| `LowerCtx` | `crates/r2dec/src/analysis/lower.rs` | SSA-to-expression lowering | -| plugin Rust surface | `r2plugin/src/lib.rs` | JSON commands, analysis helpers, FFI | -| plugin C wrapper | `r2plugin/r_anal_sleigh.c` | radare2 callbacks and command dispatch | - -## Build and Run +## Optimization Checklist + +Before landing any non-trivial change, check these explicitly: + +1. Did I add a second owner for an existing fact? +2. Did I add a repeated full-function walk? +3. Did I add repeated solver work that should be cached or summarized? +4. Did I add a new JSON-shaped internal type where a Rust type should exist? +5. Did I add ordering nondeterminism? +6. Did I move policy downstream instead of upstream? +7. Did I preserve a bad seam instead of rewriting it? + +If any answer is "yes", the design is probably wrong. + +## Build And Run Use these commands from the workspace root unless noted otherwise. @@ -202,34 +267,11 @@ cargo e2e-test Notes: -- `cargo run --features x86 -- ...` is stale at the workspace root; use `-p r2sleigh-cli --bin r2sleigh`. -- `cargo install-plugin` is defined in `.cargo/config.toml` and wraps `r2plugin/src/bin/r2sleigh-plugin-install.rs`. -- x86/x86-64 disassembly still needs at least 16 bytes of input; pad with zeros. -- If you touch the typed radare2 seam in `r2plugin/r_anal_sleigh.c`, plan to build and test `../radare2` too. Fix the library seam there instead of adding more plugin-side command parsing. - -## Architecture Support - -Feature matrices are not identical across crates. - -- `r2plugin` currently exposes `x86`, `arm`, `riscv`, and `all-archs`. -- `r2sleigh-cli` currently exposes `x86`, `arm`, `mips`, `riscv`, and `all-archs`. -- There is still some compatibility code for `mips` in shared/plugin code, but the plugin crate itself is currently feature-gated around `x86`, `arm`, and `riscv`. -- If you change architecture wiring, inspect both `r2plugin/Cargo.toml` and `crates/r2sleigh-cli/Cargo.toml`. - -For radare2 auto-selection, the plugin currently maps common values like: - -- `anal.arch=x86`, `anal.bits=64` -> `x86-64` -- `anal.arch=x86`, `anal.bits=32` -> `x86` -- `anal.arch=arm`, `anal.bits=32` -> `arm` -- `anal.arch=arm`, `anal.bits=64` or `anal.arch=arm64` / `aarch64` -> `aarch64` -- `anal.arch=riscv`, `anal.bits=32` -> `riscv32` -- `anal.arch=riscv`, `anal.bits=64` -> `riscv64` - -Manual override stays: - -```bash -r2 -qc 'a:sla.arch x86-64; a:sla.arch' /bin/ls -``` +- `cargo run --features x86 -- ...` at the workspace root is stale; use + `-p r2sleigh-cli --bin r2sleigh` +- `cargo install-plugin` is defined in `.cargo/config.toml` +- x86/x86-64 lifting still expects 16 bytes minimum +- if you touch the typed `../radare2` seam, build and test `../radare2` too ## Testing Policy @@ -238,42 +280,30 @@ r2 -qc 'a:sla.arch x86-64; a:sla.arch' /bin/ls Use `tests/r2r` for new regressions involving: - plugin commands such as `a:sla.*`, `a:sym.*`, `pdd`, `pdD` -- stable JSON/text/ESIL outputs that are worth exact normalized snapshots -- CFG/SSA/def-use/type payload shape when structural assertions are the better fit -- command UX, help text, error text, and normalized decompiler output -- radare2 integration behavior that is best expressed as command snapshots +- stable JSON/text/ESIL output +- CFG / SSA / def-use / type payload shape +- command UX and error text +- radare2 integration behavior Why: - faster feedback -- better snapshot-style diffs -- already normalized around radare2 command execution -- consistent with how users exercise the plugin +- better diffs +- already normalized around real radare2 command execution ### Use `tests/e2e` only when `r2r` is the wrong tool Keep Rust E2E tests for: - FFI / ABI checks -- CLI `run` export semantics -- analysis-quality thresholds or benchmark-style assertions -- cases that need direct Rust-side orchestration rather than command snapshots +- CLI export semantics +- benchmark-style assertions +- direct Rust orchestration cases that `r2r` cannot express cleanly -`tests/e2e/integration_tests.rs` still exists, but it is not the default place for new plugin regression coverage. +## Required Validation Bar -## Adding New Tests - -### Preferred workflow for new features - -1. Implement the feature. -2. If the user-facing behavior is visible through radare2 commands, add or update an `r2r` case. -3. If the feature needs a specific binary pattern, add or update a fixture source under `tests/e2e/`. -4. Run `make -C tests/r2r run`. -5. If the change also affects CLI semantics, FFI, or benchmark-style behavior, run `cargo e2e-test` or a focused `tests/e2e` module. - -### Required validation for ownership / seam changes - -If you touch `r2ssa`, `r2sym`, `r2types`, `r2dec`, `r2plugin`, or the typed `../radare2` context seam, the minimum validation bar is: +If you touch `r2ssa`, `r2sym`, `r2types`, `r2dec`, `r2plugin`, or the typed +`../radare2` seam, the minimum validation bar is: ```bash cargo test -p r2ssa @@ -290,8 +320,6 @@ make -C r2plugin RUST_FEATURES=all-archs install make -C tests/r2r run ``` -During local iteration, a focused subset is fine. Do not mark an ownership or seam change complete until the full relevant validation bar is green. - If you also changed `../radare2`, add: ```bash @@ -301,158 +329,60 @@ cd ../radare2/test && r2r -L -o results.json db/cmd/cmd_af db/json/json1 Do not claim the seam is fixed without both sides being green. -### Where to put new `r2r` cases +## `r2r` Placement Guide `tests/r2r/db/extras/r2sleigh_core` -- very small, deterministic instruction-level checks -- good for `a:sla.json`, `a:sla.regs`, `a:sla.mem`, `a:sla.vars` +- small deterministic instruction-level checks `tests/r2r/db/extras/r2sleigh_integration_fast` -- function-level plugin behavior that should stay quick -- good for `a:sla.ssa.func`, `a:sla.cfg.json`, `a:sla.dom`, `a:sla.types`, `a:sla.opvals` +- function-level behavior that should stay quick `tests/r2r/db/extras/r2sleigh_integration_extended` -- slower or heavier coverage -- symbolic execution, taint, complex decompilation, larger binaries - -### `r2r` test authoring tips - -- Prefer exact full-output snapshots for stable user-facing surfaces after normalization. -- Use `tests/r2r/normalize_snapshot.py` or `jq -S -c` to canonicalize stable output before snapshotting. -- Prefer structural assertions only when ordering, naming, or formatting is expected to evolve, especially for SSA, symex, taint, and large CFG/DOM payloads. -- Keep these args unless you have a reason not to: - -```text --e scr.color=false -e log.level=0 -e bin.relocs.apply=true -``` - -- `tests/r2r/Makefile` builds the fixture binaries from `tests/e2e/` and symlinks them into `tests/r2r/bins/`. -- If you add a brand-new fixture binary, update `tests/r2r/Makefile` so the harness links it. - -Minimal `r2r` example: - -```text -NAME=instruction_regs_snapshot -FILE=bins/vuln_test_x86 -ARGS=-e scr.color=false -e bin.relocs.apply=true -EXPECT=</dev/null -a:sla.regs | python3 normalize_snapshot.py regs -EOF_CMDS -RUN -``` - -### Fixture guidance - -Use the smallest fixture that exercises the behavior: - -- `tests/e2e/vuln_test.c` for focused plugin features and common analysis cases -- `tests/e2e/stress_test.c` for larger decompiler/symbolic/type cases -- `tests/e2e/test_func.c` for small structured helper functions -- `tests/e2e/sym_test.c` for symbolic-execution-specific patterns - -When you add a fixture function: - -1. Add the function with a short comment explaining what it exercises. -2. Wire it into the fixture's `main()` or other entry path if the tests need runtime access. -3. Add or update the corresponding `r2r` snapshot. +- heavier symbolic, taint, decompilation, and larger-CFG coverage ## Common Change Workflows -### Add a new R2IL opcode - -1. Add the variant to `crates/r2il/src/opcode.rs`. -2. Teach the lifter to emit it in `crates/r2sleigh-lift/src/disasm.rs`. -3. Add text and ESIL formatting in `crates/r2sleigh-lift/src/esil.rs`. -4. Check any export path that formats or serializes the new op through `crates/r2sleigh-export/src/lib.rs` or CLI output. -5. Add tests. Prefer an `r2r` snapshot when the opcode is visible through plugin output. - -### Add SSA support for a new op - -1. Add the SSA variant to `crates/r2ssa/src/op.rs`. -2. Convert it in `crates/r2ssa/src/block.rs`. -3. Update `dst()` and `sources()` in `crates/r2ssa/src/op.rs`. -4. Add function-level or instruction-level coverage, usually via `a:sla.ssa`, `a:sla.ssa.func`, or `a:sla.defuse`. - -### Add decompiler support for a new SSA op +### Change the type / decompiler seam -1. Add lowering in `crates/r2dec/src/analysis/lower.rs` if needed. -2. Add fold/codegen support under `crates/r2dec/src/fold/`. -3. Test through `a:sla.dec` snapshots and add direct Rust tests when local folding behavior is easier to assert there. +1. Choose the owner. If the answer is "more than one", the design is wrong. +2. Extend `r2types` first for signatures, layouts, and type facts. +3. Keep `r2dec` on semantic interpretation and rendering only. +4. If the plugin needs more context, extend `../radare2` instead of parsing + more command JSON. +5. If the seam is wrong, redesign it across repos instead of layering adapters. +6. Add or update `r2r` before broad snapshot churn. -### Change the type / decompiler seam +### Change symbolic / query behavior -1. Start by deciding which crate should own the policy. If the answer is "more than one", the design is wrong. -2. Extend `r2types` contracts first when the change affects signatures, layouts, or external context. -3. Keep `r2dec` on semantic facts and rendering. If decompiler code needs to rediscover type/layout policy from rendered expressions, stop and move that logic upstream. -4. If the plugin needs more context from radare2, extend the typed `r_anal` seam in `../radare2` instead of parsing more command JSON. -5. If the existing seam is fundamentally wrong, redesign it across both repos instead of layering more adapters on top. -6. Add or update `r2r` coverage before broad snapshot churn. The right fix is usually a stronger semantic assertion, not a bigger snapshot. +1. Put semantic policy in `r2sym`. +2. Put evidence and ambiguity in canonical artifact/evidence types. +3. Put routing in canonical plans. +4. Let `r2types` / `r2dec` consume those plans; do not reinvent them. +5. Add solver-budget and determinism coverage where applicable. ### Add or change a plugin command -1. Rust-side command data shaping usually lives in `r2plugin/src/lib.rs`. -2. radare2 command dispatch and help text live in `r2plugin/r_anal_sleigh.c`. -3. Add or update `r2r` coverage for help text, happy path, and error path. +1. Decide whether the feature should really be automatic or radare2-native. +2. Rust-side data shaping usually lives in `r2plugin/src/lib.rs`. +3. C dispatch/help lives in `r2plugin/r_anal_sleigh.c`. +4. Add `r2r` coverage for help, happy path, and failure path. -### Add a new architecture +## Plugin Command Surface -1. Update the relevant crate feature flags. -2. Wire spec/disassembler creation in the CLI, plugin, and export surfaces that need it. -3. Add at least one focused test path for the new arch. -4. Prefer documenting only architectures that are actually wired and tested in the crate you changed. +Treat this as two tiers: -## Plugin Command Surface +- public / user-facing + - `a:sla` + - `a:sla.dec` + - `pdd`, `pdD` + - `a:sym.explore` + - `a:sym.solve` + - `a:sym.state` +- debug / engine inspection + - low-level IL / SSA / facts / plan / replay / path listing commands -Common instruction-level commands: - -| Command | Purpose | -|---------|---------| -| `a:sla` | status / help | -| `a:sla.info` | current architecture info | -| `a:sla.arch [name]` | get or set Sleigh arch override | -| `a:sla.json` | raw r2il for current instruction | -| `a:sla.regs` | read/write registers | -| `a:sla.opvals` | analysis src/dst register view | -| `a:sla.mem` | memory accesses | -| `a:sla.vars` | varnodes | -| `a:sla.ssa` | instruction SSA | -| `a:sla.defuse` | instruction def-use | - -Function-level commands: - -| Command | Purpose | -|---------|---------| -| `a:sla.ssa.func` | function SSA with phi nodes | -| `a:sla.ssa.func.opt` | optimized function SSA | -| `a:sla.defuse.func` | function-wide def-use | -| `a:sla.dom` | dominator tree | -| `a:sla.slice ` | backward slice | -| `a:sla.types` | type-inference payload | -| `a:sla.taint` | taint analysis | -| `a:sla.sym` | symbolic summary | -| `a:sla.sym.paths` | explored symbolic paths | -| `a:sla.sym.merge [on|off]` | symbolic merge toggle | -| `a:sla.dec [name|addr]` | decompile | -| `pdd`, `pdD` | aliases for `a:sla.dec` | -| `a:sla.cfg` | ASCII CFG | -| `a:sla.cfg.json` | CFG JSON | - -Targeted symbolic commands: - -| Command | Purpose | -|---------|---------| -| `a:sym.explore ` | explore paths reaching target | -| `a:sym.solve ` | solve concrete input for target | -| `a:sym.state` | show cached symbolic state | - -Important: - -- Use `a:sym.solve`, not the old `a:sla.sym.solve` spelling. -- Use `a:sla.cfg.json` when you want stable structured assertions. +Do not expand the public surface casually. Prefer deeper integration over more +verbs. ## Two SSA Block Types @@ -465,8 +395,6 @@ There are two different block types in `r2ssa`: `r2dec` works with `FunctionSSABlock`. -When writing direct decompiler tests, build `FunctionSSABlock` values directly rather than assuming a convenience constructor exists. - ## File Quick Reference | File | Edit this when... | @@ -474,26 +402,14 @@ When writing direct decompiler tests, build `FunctionSSABlock` values directly r | `crates/r2il/src/opcode.rs` | adding or changing IL ops | | `crates/r2sleigh-lift/src/disasm.rs` | changing P-code lifting or register naming | | `crates/r2sleigh-lift/src/esil.rs` | changing text or ESIL rendering | -| `crates/r2sleigh-export/src/lib.rs` | changing shared export formatting or action plumbing | -| `crates/r2sleigh-cli/src/main.rs` | changing CLI commands or action/format routing | -| `crates/r2ssa/src/op.rs` | changing SSA operations | -| `crates/r2ssa/src/block.rs` | changing SSA conversion | -| `crates/r2ssa/src/function.rs` | function SSA / phi handling | -| `crates/r2ssa/src/defuse.rs` | changing def-use analysis | -| `crates/r2sym/src/` | changing symbolic execution or taint internals | -| `crates/r2types/src/` | changing type inference | -| `crates/r2types/src/context.rs` | changing typed external context imported from radare2 | -| `crates/r2dec/src/fold/` | changing decompiler folding and lowering | -| `crates/r2dec/src/codegen.rs` | changing C output formatting | +| `crates/r2ssa/src/` | changing SSA construction, def-use, prepared facts | +| `crates/r2sym/src/` | changing semantic artifacts, query, summaries, replay, solver policy | +| `crates/r2types/src/` | changing type inference, layouts, canonical function facts | +| `crates/r2dec/src/` | changing lowering, structuring, rendering | | `r2plugin/src/lib.rs` | changing plugin-side Rust logic and JSON payloads | -| `r2plugin/r_anal_sleigh.c` | changing radare2 callbacks, command help, dispatch | -| `../radare2/libr/include/r_anal.h` | changing the typed function/base-type collector API used by the plugin | -| `../radare2/libr/anal/fcn.c` | implementing typed function-context collection | -| `../radare2/libr/anal/type.c` | implementing typed base-type collection / parity with `tsj` | -| `tests/r2r/Makefile` | changing r2r harness setup or fixture linking | -| `tests/r2r/db/extras/` | adding or updating snapshot regressions | -| `tests/e2e/README.md` | checking when to use Rust E2E vs `r2r` | -| `tests/e2e/integration_tests.rs` | legacy semantic/FFI coverage, not the default for new snapshots | +| `r2plugin/r_anal_sleigh.c` | changing command dispatch/help or C-side integration | +| `../radare2/libr/include/r_anal.h` | changing typed collector APIs used by the plugin | +| `tests/r2r/db/extras/` | adding or updating regression snapshots | ## Gotchas @@ -501,20 +417,20 @@ When writing direct decompiler tests, build `FunctionSSABlock` values directly r 2. ESIL subtraction must use ASCII `-`, not Unicode minus. 3. `Const` means literal; `Unique` means temporary SSA-like storage, not memory. 4. Width mismatches usually need explicit sign/zero extension. -5. Register aliasing needs deterministic policy in output and recovery. +5. Register aliasing must stay deterministic. 6. `#[no_mangle]` is now `#[unsafe(no_mangle)]` under Rust 2024. -7. Plugin, CLI, and export crate feature matrices are not identical. -8. Prefer `a:sla.cfg.json`, `a:sla.types`, and `jq`-normalized checks over raw pretty-printed output in snapshots. -9. Use `r2dec/address.rs::parse_address_from_var_name()` for consistent `const:` / `ram:` parsing. -10. Taint summaries intentionally filter noisy stack/frame-pointer labels. -11. If you add a new fixture binary, remember both the build step and the `tests/r2r/bins/` symlink step. -12. If output stability matters, assume hash-order nondeterminism is a bug. Use deterministic ordering in SSA facts, decompiler facts, and local naming. -13. On the decompiler/type path, tests may now see `r2types::CTypeLike` rather than a local `r2dec` type wrapper. Do not reintroduce `r2dec` type ownership just to make an old test compile. +7. Plugin, CLI, and export feature matrices are not identical. +8. If output stability matters, hash-order nondeterminism is a bug. +9. Use `r2dec/address.rs::parse_address_from_var_name()` for consistent + `const:` / `ram:` parsing. +10. On the decompiler/type path, do not reintroduce `r2dec` type ownership just + to make an old test compile. ## Useful References - `README.md` for current build and testing quick-start -- `tests/e2e/README.md` for the split between `r2r` and Rust E2E +- `ROADMAP.md` for current system direction and priority order +- `tests/e2e/README.md` for the split between Rust E2E and `r2r` - `doc/` for IL, SSA, ESIL, decompiler, taint, symex, and type-system notes - radare2 ESIL docs: - Ghidra P-code reference: diff --git a/Cargo.lock b/Cargo.lock index 19d0209..386fbea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "0.6.21" @@ -106,7 +112,7 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", "regex", @@ -115,6 +121,21 @@ dependencies = [ "syn", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "2.10.0" @@ -139,6 +160,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.52" @@ -170,6 +197,33 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -247,12 +301,79 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + [[package]] name = "critical-section" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "cxx" version = "1.0.192" @@ -350,6 +471,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "find-msvc-tools" version = "0.1.7" @@ -362,6 +499,12 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -466,6 +609,17 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hash32" version = "0.2.1" @@ -510,6 +664,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -745,12 +905,32 @@ dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -821,6 +1001,12 @@ dependencies = [ "cc", ] +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" version = "0.8.1" @@ -966,6 +1152,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1002,6 +1194,34 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "postcard" version = "1.1.3" @@ -1042,6 +1262,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quinn" version = "0.11.9" @@ -1118,6 +1363,7 @@ version = "0.1.0" dependencies = [ "r2il", "r2ssa", + "r2sym", "r2types", "serde", "serde_json", @@ -1221,6 +1467,8 @@ dependencies = [ name = "r2sym" version = "0.1.0" dependencies = [ + "criterion", + "proptest", "r2il", "r2pipe", "r2ssa", @@ -1237,6 +1485,7 @@ version = "0.1.0" dependencies = [ "r2il", "r2ssa", + "r2sym", "serde", "serde_json", "thiserror 2.0.17", @@ -1271,6 +1520,35 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "regex" version = "1.12.2" @@ -1369,6 +1647,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.36" @@ -1410,12 +1701,33 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1597,6 +1909,19 @@ dependencies = [ "syn", ] +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -1656,6 +1981,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.10.0" @@ -1765,6 +2100,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -1807,6 +2148,25 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" diff --git a/README.md b/README.md index 513f6a3..18fdc52 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ Documentation | [doc/taint.md](doc/taint.md) | Taint analysis | | [doc/symex.md](doc/symex.md) | Symbolic execution | | [doc/plugin.md](doc/plugin.md) | radare2 plugin and commands | +| [doc/plugin-rfe-2026.md](doc/plugin-rfe-2026.md) | plugin feature and integration RFE | | [doc/types.md](doc/types.md) | Type inference | | [doc/testing.md](doc/testing.md) | Testing strategy | diff --git a/ROADMAP.md b/ROADMAP.md index 7c0f4cf..16202b9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,277 +1,389 @@ r2sleigh Roadmap ================ -> Vision: r2sleigh is not a separate plugin the user needs to know about. -> It is the **analysis brain** of radare2 — transparently lifting, typing, -> decompiling, tainting, and solving every function the user touches. -> The user runs `aaa`, `a:sla.dec`, or just browses code, and the best -> analysis in the industry happens automatically behind the scenes. - -Current State (Feb 2026) -------------------------- - -~200 tests passing across 8 crates. 20+ plugin commands working in radare2. - -Working features: -- 60+ R2IL opcodes from Ghidra Sleigh specifications -- Full SSA pipeline: CFG, dominator tree, phi nodes, optimization (SCCP, DCE, CSE, copy-prop, inst-combine) -- Z3-backed symbolic execution with path exploration -- SSA-based taint analysis with automatic radare2 integration during aaaa -- Decompiler producing C code with expression folding, predicate simplification, for-loops, switches, string literals, and symbol resolution -- Constraint-based type inference with struct/signature support -- Backward slicing -- 20+ radare2 plugin commands with automatic analysis hooks -- Deep integration callbacks: `analyze_fcn`, `recover_vars`, `get_data_refs`, `post_analysis` - -Supported architectures: x86, x86-64, ARM, MIPS. - -Planned Features ----------------- +> Vision: `r2sleigh` is not a sidecar plugin with many commands. +> It is the analysis brain of radare2: one typed subsystem that lifts, +> structures, solves, types, summarizes, and renders functions coherently. + +North Star +---------- + +The system we are aiming for has these properties: -### 2026 Priority Reset (Maintainer Feedback, Execution Order) - -This section defines the actual implementation order for upcoming work. -Phase numbering below remains as thematic grouping, but delivery priority is: -`P0 (Now) -> P1 (Next) -> P2 (Later)`. - -| Priority | Theme | Why now | Concrete deliverables | -|---|---|---|---| -| P0 | **R2IL Foundation Hardening** | Avoid design debt before adding major features | Keep core IL minimal, add optional metadata (`storage_class`, pointer/type hints, memory attributes, richer endianness model), document semantics and compatibility rules | -| P0 | **Unified Output + One-Liner UX** | Fast iteration for developers and easier maintainer review | Single export pipeline for C-like / r2-command-like / JSON outputs, plus CLI one-liners to run lift/SSA/defuse/dec actions directly | -| P0 | **RISC-V Support** | Higher ecosystem value than MIPS for new users | Add riscv feature flag, disassembler wiring, register profile/calling convention integration, plugin + e2e coverage | -| P1 | **Memory Semantics Extensions (VEX-inspired, scoped)** | Improves analysis fidelity without overhauling IL | Add `MemoryOrdering` + atomic/fence/guarded ops where liftable; compare behavior with VEX and document differences | -| P1 | **Float/Vector + Encoding Path** | Needed for correctness on modern binaries | Integrate r2 float encoding model, add float/vector metadata and staged SIMD support | -| P1 | **Hardware/Memory Topology Modeling** | Needed for firmware and embedded workflows | MMIO/IO port classification, const/permission/range attributes, segmented/banked memory policy and tests | -| P2 | **VM Architecture Perspective** | Valuable long-term but high scope | Prototype Dalvik/VM support as separate lifter module, validate whether core IL needs extension | -| P2 | **Advanced Execution Models** | Niche/high-complexity | VLIW parallel group representation and deeper scheduling semantics | - -#### Maintainer Points -> Priority Mapping - -| Maintainer point | Priority | Decision | -|---|---|---| -| Multiple address spaces + TLS | P0 | Keep `SpaceId` stable; represent TLS via optional storage metadata first | -| RISC-V over MIPS | P0 | Promote RISC-V to immediate architecture milestone | -| VM archs (Dalvik/JVM) | P2 | Explore in separate lifter crate, avoid premature core IL changes | -| FPU/vector + any float encoding | P1 | Stage through metadata + encoding integration, then expand op support | -| Atomic ops / guards / VEX comparison | P1 | Add scoped memory-ordering features with explicit doc comparison | -| MMIO + IO ports | P1 | Add memory-class metadata and explicit IO semantics where available | -| Complex endianness modes | P0 | Replace binary endianness assumptions with richer enum/override model | -| VLIW parallel execution | P2 | Model as block-level parallel groups first | -| Pointer as attribute, not base type | P0 | Treat pointer-ness as semantic metadata/type hint | -| Const attrs (permissions, valid ranges) | P1 | Add optional memory attribute model | -| Memory banks / segmented memory | P1 | Keep `SegmentOp`; add policy/docs/tests for banks/segments | -| Switch as operation | P2 | Keep `switch_info` for now; revisit after SSA/CFG simplification pass | - -#### Near-Term Milestones (Next 3) - -1. **Milestone A (P0)**: R2IL metadata foundation + docs + compatibility tests. -2. **Milestone B (P0)**: Unified export formats + CLI one-liners for lift/ssa/defuse/dec. -3. **Milestone C (P0)**: End-to-end RISC-V support with plugin and e2e coverage. - -### Phase 1 — Seamless r2 Integration (make the seams invisible) - -The user should never feel they are using a separate tool. r2sleigh must -consume everything radare2 already knows and push results back into r2's -native data structures so every existing r2 command benefits. - -| # | Feature | Description | Effort | Impact | -|---|---------|-------------|--------|--------| -| 1.1 | **DWARF signature pipeline** | Verify DWARF-imported function signatures (via r2 `sdb_types` / `afcfj`) flow end-to-end into r2dec's `VariableRecovery` and `TypeInference`. DWARF variable names, parameter types, and return types must appear in decompiled output automatically. | Low | High | -| 1.2 | **DWARF struct/enum type feeding** | In `sleigh_cmd` (`a:sla.dec`), query r2's `tsj` for DWARF-imported structs/unions/enums and feed into `ExternalTypeDb`. The type solver already has `FieldAccess` constraints and `lookup_field_name`; wire them to real DWARF data. | Medium | High | -| 1.3 | **DWARF-assisted struct field recovery** | When type inference resolves `*(ptr+offset)` and the `ExternalTypeDb` has a matching struct with a field at that offset, emit `ptr->field` in decompiled C. Combines DWARF data + existing `detect_addr_pattern` + `lookup_field_name`. | Medium | High | -| 1.4 | **Transparent `pdd` alias** | Register `pdd` (or `pdD`) as an r2 command alias that calls `a:sla.dec` for the current function. Users get decompilation from the standard r2 command vocabulary without knowing r2sleigh exists. | Low | High | -| 1.5 | **Write-back inferred types to r2** | After decompilation or `aaaa`, push inferred struct shapes, function signatures, and variable types back into `sdb_types` (via `r_anal_save_parsed_type`/`r_anal_import_c_decls`). This means `t` commands, `afvt`, and future analysis passes all benefit. | Medium | High | -| 1.6 | **Global variable recognition** | Use SSA data-flow analysis to detect accesses to fixed RAM addresses, cross-reference with r2's `r_anal_global_get`/flags, and emit named globals in decompiled output instead of raw hex constants. | Low | Medium | -| 1.7 | **Autoname functions from decompiler** | After decompilation, heuristically derive function names from string arguments to known calls (e.g., a function whose first call is `printf("usage: ...")` → `print_usage`). Feed names back via `r_anal_function_rename`. Integrate with `aan`. | Medium | Medium | -| 1.8 | **Calling convention auto-detection** | During `analyze_fcn`, determine calling convention (cdecl/stdcall/fastcall/sysv/win64/arm-aapcs) from SSA parameter-register usage patterns. Write back to r2's `afcc` so all downstream commands agree. Currently hardcoded to SysV x86-64. | Medium | Medium | - -### Phase 2 — Decompiler Quality (match Ghidra, exceed it) - -| # | Feature | Description | Effort | Impact | -|---|---------|-------------|--------|--------| -| 2.1 | **Phi node elimination** | Convert `phi(x1,x2)` to proper variable assignments at predecessor edges. Removes the last SSA artifacts from decompiled output. | Medium | High | -| 2.2 | **Register coalescing** | Merge `RAX_1`, `RAX_2`, ... into a single C variable when the live ranges don't interfere. Dramatically reduces variable clutter. | Medium | High | -| 2.3 | **Short-circuit operators** | Detect `if(a) { if(b) { X } }` → `if(a && b) { X }` and the OR variant. | Low | Medium | -| 2.4 | **Condition inversion / early return** | Prefer `if(!x) return;` over `if(x) { ...long body... }`. Reduces nesting. | Low | Medium | -| 2.5 | **No More Gotos** | Handle irreducible CFGs with region-based restructuring instead of gotos. The `structure.rs` already has region analysis; extend with node splitting or controlled duplication. | High | High | -| 2.6 | **Pointer type propagation** | Track pointer types through Load/Store chains. When `p = malloc(sizeof(Foo))`, propagate `Foo*` to all uses of `p`. | Medium | High | -| 2.7 | **Array access patterns** | Detect `base + i*stride` as `arr[i]`. The type solver already has `stride` detection in `detect_addr_pattern`; surface it in codegen. | Medium | Medium | -| 2.8 | **Enum constant folding** | When a comparison operand matches an enum variant from `ExternalTypeDb`, emit the enum name instead of the raw integer. | Low | Medium | -| 2.9 | **String constant propagation** | When a local variable is assigned a string address and only used in one call, inline the string literal at the call site. | Low | Medium | -| 2.10 | **sizeof() recovery** | Detect `malloc(N)` where N matches `sizeof(struct X)` from the type DB. Emit `malloc(sizeof(X))`. | Low | Low | - -### Phase 3 — Vulnerability Intelligence (the killer feature) - -No other open-source tool provides automatic, per-function vulnerability -assessment integrated directly into the reversing workflow. - -| # | Feature | Description | Effort | Impact | -|---|---------|-------------|--------|--------| -| 3.1 | **Vulnerability pattern library** | Detect buffer overflow, format string, UAF, double-free, integer overflow at IL/SSA level. Each pattern is a taint policy + SSA matcher. Ship as data files, not code. | Medium | Critical | -| 3.2 | **Risk scoring engine** | Assign per-function risk scores based on: sink severity × input reachability × sanitizer presence. Rank all functions by exploitability during `aaaa`. Write `sla.risk` flag + comment. | Medium | Critical | -| 3.3 | **Guided vuln discovery** | `a:sym.vuln ` — use symbolic execution to find concrete input reaching a dangerous sink (e.g., `gets()` or unchecked `memcpy`). Output includes input constraints in SMT-LIB2 and concrete model. | Medium | High | -| 3.4 | **Crypto detection** | Detect crypto algorithms by IL patterns: S-box constants (AES), round constants (SHA), Feistel structure. Flag functions as `sla.crypto.aes`, etc. | Medium | Medium | -| 3.5 | **Integer overflow detection** | Flag arithmetic operations on user-controlled values that lack bounds checks before use as array indices or allocation sizes. | Medium | High | -| 3.6 | **Path predicate export** | Export path constraints as SMT-LIB2 for external solvers or integration with fuzzing harnesses. | Low | Medium | - -### Phase 4 — Inter-Procedural Analysis (the hard problems) - -| # | Feature | Description | Effort | Impact | -|---|---------|-------------|--------|--------| -| 4.1 | **Function summaries** | Cache per-function symbolic summaries: which inputs affect which outputs, what gets tainted, what's returned. Enables inter-procedural without full inlining. | High | Critical | -| 4.2 | **Inter-procedural taint** | Taint analysis spanning function boundaries using summaries. `input:argv` reaching `strcpy` in a callee three levels deep. | High | Critical | -| 4.3 | **Call graph with data flow** | Build a call graph where edges carry data-flow information (which args of caller flow to which params of callee). | High | High | -| 4.4 | **Whole-program type inference** | Unify types across function boundaries: if `foo()` returns a `struct stat*` and `bar()` receives it, propagate the struct type into `bar`'s parameter. | High | High | -| 4.5 | **Context-sensitive decompilation** | When decompiling `foo(x)`, look at callers to determine likely type/range of `x`. Annotate decompiled output with "called from: ..." context. | High | Medium | - -### Phase 5 — Symbolic Execution & Concolic (the smart engine) - -| # | Feature | Description | Effort | Impact | -|---|---------|-------------|--------|--------| -| 5.1 | **Interactive symbolic execution** | `a:sym.explore` and `a:sym.solve` commands with user-specified targets, constraints, and hooks. | Medium | High | -| 5.2 | **Memory in solutions** | Include concrete memory layout (heap, stack, globals) in symbolic path output, not just register values. | Low | Medium | -| 5.3 | **Concolic execution** | Concrete + symbolic hybrid guided by ESIL traces from r2's debugger. Run the binary, record a trace, symbolically explore alternatives. | High | High | -| 5.4 | **Symbolic call stubs** | Auto-generate symbolic stubs for common libc functions (`strlen` returns symbolic length, `malloc` returns fresh symbolic pointer). | Medium | High | -| 5.5 | **Constraint caching** | Cache Z3 queries per function so repeated solves (e.g., during fuzzing integration) don't redundantly re-solve. | Medium | Medium | - -### Phase 6 — Platform & Architecture Expansion - -| # | Feature | Description | Effort | Impact | -|---|---------|-------------|--------|--------| -| 6.1 | **ABI/calling-convention model** | Abstract architecture-specific assumptions (arg registers, stack direction, alignment) into a data model. Currently hardcoded for SysV x86-64 in `variable.rs`, `types.rs`, `taint.rs`. | Medium | High | -| 6.2 | **RISC-V support** | Add RISC-V Sleigh spec + register profile + calling convention. **Execution priority: P0 (Milestone C).** | Medium | Medium | -| 6.3 | **AArch64 / ARM64 support** | Full ARM64 support with AAPCS64 calling convention. | Medium | Medium | -| 6.4 | **PPC / AVR / SPARC** | Additional architecture support with per-arch test fixtures. | Medium | Low | -| 6.5 | **Register naming policy** | Normalize register names, resolve overlapping aliases (RAX vs EAX vs AX vs AL). Use canonical names in decompiled output. | Low | Medium | -| 6.6 | **Floating-point type inference** | Properly distinguish float/double from integer types using SSA float opcodes. | Low | Low | - -### Phase 7 — Advanced Analysis & Research - -| # | Feature | Description | Effort | Impact | -|---|---------|-------------|--------|--------| -| 7.1 | **Memory/value-set analysis** | Alias-aware abstract interpretation tracking value ranges and pointer targets. Enables more precise taint, slicing, and decompilation. | High | High | -| 7.2 | **R2IL VM + event tracing** | Make R2IL executable with concrete values. Record execution traces with events (mem read/write, branch taken). Compare static vs dynamic analysis. | High | Medium | -| 7.3 | **Semantic diff** | Compare two functions (or two versions of a binary) for semantic differences at the SSA level. Highlight what changed in the decompiled output. | High | Medium | -| 7.4 | **Pattern matching DSL** | User-defined IL patterns for custom detection. "Find all functions that read from `[user_input + *]` and pass it to `exec*`." | Medium | Medium | -| 7.5 | **Incremental analysis** | When the user annotates a type or renames a variable, incrementally update SSA/taint/decompilation without re-lifting the whole function. | High | Medium | -| 7.6 | **Decompiler output diffing** | When types/signatures change, show a diff of the decompiled C output. Useful for iterative reverse engineering. | Low | Low | - -Integration Architecture +- one canonical IL substrate +- one canonical SSA/dataflow layer +- one canonical semantic artifact with explicit evidence +- one canonical combined `FunctionFacts` contract +- one planner surface for query, types, and decompilation +- one replay/trace validation loop +- one small public plugin surface that feels native to radare2 + +The metric is not command count. The metric is whether radare2 feels like it +gained one coherent, mathematically disciplined analysis engine. + +Current State (Apr 2026) ------------------------ -The key insight: r2sleigh hooks into radare2's analysis pipeline at every -stage, consuming r2's metadata and pushing results back. The user never -invokes r2sleigh directly — it's just "r2 but smarter." - -``` -radare2 analysis pipeline r2sleigh hooks -───────────────────────── ────────────── -aa (basic analysis) - └─ af (find functions) ──→ analyze_fcn: SSA + annotations - └─ afva (find vars) ──→ recover_vars: SSA-derived stack vars + reg args - └─ aar (find refs) ──→ get_data_refs: SSA-derived data/code/string refs - -aaa (deeper analysis) - └─ aan (autoname) ──→ [NEW] autoname from decompiler heuristics - └─ DWARF integration ──→ [NEW] DWARF types → ExternalTypeDb → decompiler - └─ afcfj (signatures) ──→ already consumed by a:sla.dec - └─ tsj (type structs) ──→ already consumed by a:sla.dec - -aaaa (experimental) - └─ post_analysis ──→ taint analysis + risk scoring + xrefs - └─ [NEW] ──→ write-back inferred types to sdb_types - └─ [NEW] ──→ write-back function signatures to afcc - └─ [NEW] ──→ flag risky functions with sla.risk.* - -User commands (transparent) - └─ pdd / pdD ──→ [NEW] alias to a:sla.dec - └─ a:sla.dec ──→ decompile with full context from r2 - └─ a:sla.taint ──→ taint current function - └─ a:sym.solve ──→ solve for reachability -``` - -### Data Flow: r2 → r2sleigh → r2 - -``` - ┌──────────────┐ - │ radare2 │ - │ │ - ┌─────────────────┤ sdb_types │◄──────── DWARF / PDB / user annotations - │ │ flags │ - │ │ xrefs │ - │ │ afcfj │ - │ │ afvj │ - │ │ tsj │ - │ │ aflj │ - │ └──────┬───────┘ - │ │ JSON - │ ▼ - │ ┌──────────────┐ - │ │ r2sleigh │ - │ │ │ - │ │ R2IL lift │ - │ │ SSA build │ - │ │ Type infer │ - │ │ Decompile │ - │ │ Taint │ - │ │ SymExec │ - │ └──────┬───────┘ - │ │ JSON + C strings - │ ▼ - │ ┌──────────────┐ - │ write-back ────►│ radare2 │ - │ types │ │ - │ signatures │ sdb_types ← inferred struct shapes - │ variables │ afcc ← detected calling convention - │ names │ flags ← taint/risk/crypto flags - │ xrefs │ xrefs ← taint-flow + data refs - │ comments │ comments ← taint summaries, risk scores - └─────────────────┤ afn ← auto-named functions - └──────────────┘ -``` - -Comparison ----------- +The core architecture reset is done. + +Completed high-value work: + +- canonical `r2sym::SemanticArtifact` ownership +- canonical evidence and plan surfaces in `r2sym` +- `FunctionFacts` as the combined type+semantic contract +- removal of legacy mirrored symbolic/type ownership +- planner-gated symbolic query routing +- target-local narrowing with explicit ambiguity handling +- decompiler planner/consumer split +- canonical VM summary routing in decompiler/plugin +- explicit semantic schema/cache versioning +- end-to-end plugin transport on canonical facts/plans +- strong `r2r` coverage and full validation bar + +The system is no longer missing foundations. The next work is about using those +foundations better across the whole stack. + +Strategic Principles +-------------------- + +1. One subsystem, not many tools. + - `r2il`, `r2ssa`, `r2sym`, `r2types`, `r2dec`, `r2plugin`, and + `../radare2` should behave like one analysis engine. + +2. Optimize for typed ownership, not command growth. + - The best improvements are deeper integration and stronger facts, not + additional verbs. + +3. Optimize for practical asymptotics. + - aim for `O(1)` / `O(log n)` lookups + - `O(n)` passes where possible + - bounded search where unavoidable + - summaries and incremental reuse everywhere else + +4. Prefer principled rewrites over downstream patchwork. + +5. Determinism beats cleverness. + +What Is Done +------------ + +### 1. Canonical Semantic Rewrite + +Done: + +- `r2sym` is the semantic owner +- evidence and ambiguity are first-class +- query routing is planner-gated +- target-local narrowing is authoritative and source-consistent +- symbolic artifact transport is canonical across plugin/export surfaces + +### 2. Consumer Hardening + +Done: + +- `r2types` consumes canonical semantics through `FunctionFacts` +- `r2dec` routes through planner + consumer modules +- VM decompile path is honest summary mode instead of pretending to be native +- plugin JSON/reporting is on canonical facts and plan fields + +### 3. Decompiler/Plugin Integration + +Done: + +- canonical semantic route planning +- VM summary rendering route +- combined facts/plan reporting +- strong `r2r` coverage for the new paths + +What Is Not Done +---------------- + +The biggest remaining gains are not foundational rewrites. They are whole-stack +intelligence improvements: + +- shared assumption model +- deeper reuse of interprocedural summaries across all consumers +- trace/replay as a first-class validation loop +- richer semantic type algebra +- stronger VM semantic rendering +- narrower and more native plugin surface + +Priority Order +-------------- + +The real implementation order from here is: + +`P0 assumptions -> P1 summary reuse -> P2 replay loop -> P3 semantic typing -> +P4 VM semantics -> P5 command-surface rationalization -> P6 incremental/perf` + +### P0 — Shared Assumption Model + +Goal: + +Turn the subsystem into an interactive reasoning engine instead of a static +batch analyzer. + +Deliverables: + +- canonical typed assumption model across `r2ssa`, `r2sym`, and `r2types` +- explicit persistence or transport seam through plugin and, if needed, + `../radare2` +- incremental recomputation of affected analyses +- assumptions influence: + - query narrowing + - branch feasibility + - type/layout recovery + - decompiler simplification + +Why this is highest ROI: + +- users often know one critical fact the engine does not +- this amplifies every existing subsystem instead of adding a silo + +Success criteria: + +- one assumption set changes query, types, and decompiler coherently +- assumptions are explicit, typed, serializable, and test-covered + +### P1 — Promote Interprocedural Summaries To Whole-Stack Inputs + +Goal: + +The summaries already present in `r2sym` should stop being mostly query power +and start being a shared strength across the subsystem. + +Deliverables: + +- summary-driven return/value-shape hints for `r2types` +- summary-driven helper-call simplification for `r2dec` +- summary-backed applicability/evidence surfaced in function facts +- stricter reuse of library and derived summaries instead of rediscovery + +Why: + +- interproc summary work already exists in `r2sym` +- downstream consumers currently leave too much value on the table + +Success criteria: + +- better out-param inference +- better return-shape inference +- better helper-call rendering +- fewer downstream local heuristics + +### P2 — Replay And Trace As First-Class Validation + +Goal: + +Make debugger state and replay checkpoints part of the normal semantic loop. + +Deliverables: + +- replay seeds as canonical engine input, not just expert-only path control +- typed import of debugger/trace state +- witness validation against replayed state +- static-vs-observed semantic mismatch reporting + +Why: + +- this is the cleanest bridge between static and dynamic reasoning +- replay infrastructure already exists and needs promotion, not reinvention -### vs radare2 (stock) +Success criteria: -r2sleigh adds: SSA form, phi nodes, def-use chains, dominator tree, -symbolic execution, taint analysis, path exploration, Z3 solving, typed -decompilation, constraint-based type inference, vulnerability detection, -risk scoring, automatic function naming from decompiler heuristics. +- replay and witness validation share canonical semantic state +- observed state can refine confidence without becoming semantic owner -### vs angr +### P3 — Semantic Type Algebra V2 -r2sleigh provides: zero-friction radare2 integration (no Python, no -separate process), CLI-first workflow, Sleigh specs (vs VEX), JSON -output for scripting, per-function taint during `aaaa`, decompiler -output, no Python overhead. angr has: mature inter-procedural analysis, -larger community, more memory models. +Goal: -### vs Ghidra +Make `r2types` consume more of the semantic artifact than memory terms alone. -r2sleigh provides: exposed SSA for scripting, integrated symbolic -execution, automatic taint analysis with risk scoring, vulnerability -pattern detection, native CLI operation, no JVM dependency, incremental -results during analysis. Ghidra has: more mature decompiler, -inter-procedural type propagation (which we're building in Phase 4), -larger architecture coverage. +Deliverables: -### vs Binary Ninja +- use `pre`, `post`, `control`, and `targets` alongside memory facts +- use diagnostics and residual reasons to refuse unsafe projections +- infer: + - out-params + - return shape + - field applicability + - layout confidence -r2sleigh provides: fully open source, no license cost, Sleigh specs -(broadest architecture coverage), integrated symbolic execution and -taint analysis, CLI-native workflow, r2pipe scriptability. Binary Ninja -has: polished GUI, MLIL/HLIL abstraction layers, commercial support. +Why: -**The goal**: combine the best of all four — Ghidra's Sleigh specs, -angr's symbolic execution, Binary Ninja's type inference quality, and -radare2's CLI-first hackability — into a single transparent analysis -engine that just works when you type `aaa`. +- current type recovery is useful but still too memory-led +- this is the biggest non-query value gap -References +Success criteria: + +- fewer unsafe struct candidates +- stronger return/out-param recovery +- cleaner agreement between types and decompiler output + +### P4 — VM Semantic Rendering V2 + +Goal: + +Move VM analysis from summary comments toward structured semantic rendering. + +Deliverables: + +- selector recovery +- handler graph summaries +- guarded transfer summaries +- switch-like pseudo-C from canonical VM semantics + +Non-goal: + +- do not start with a fake "full VM decompiler" + +Why: + +- current VM path is honest but leaves value on the table + +Success criteria: + +- VM functions render as structured semantic summaries, not just comments +- VM routes remain explicitly marked as summary-driven where appropriate + +### P5 — Public Surface Rationalization + +Goal: + +Make the plugin feel native to radare2 instead of exposing every internal stage +as a first-class user command. + +Deliverables: + +- define public vs debug command tiers +- move config-like behaviors to `e anal.sleigh.*` +- enrich existing radare2 views instead of growing more plugin verbs +- keep expert inspection surfaces available, but demoted + +Why: + +- command count is not a quality metric +- intelligent integration should reduce, not expand, user-visible surface area + +Success criteria: + +- fewer public commands +- stronger existing radare2 workflows +- less need for users to think in crate boundaries + +### P6 — Incremental And Performance Discipline + +Goal: + +Turn the subsystem into a cheaper engine to run repeatedly. + +Deliverables: + +- stronger summary caches +- fewer repeated full-function passes +- explicit invalidation and reuse boundaries +- budget-aware scheduling +- deterministic cache keys and cost-aware planners + +Why: + +- the architecture is now clean enough that the next gains come from reuse + +Success criteria: + +- repeated analysis gets cheaper +- large-CFG behavior stays bounded +- downstream consumers reuse upstream work instead of re-deriving it + +Per-Crate Direction +------------------- + +### `r2il` / `r2sleigh-lift` + +Keep the IL small, typed, and architecture-faithful. Extend only when new +semantics are truly canonical across the stack. + +### `r2ssa` + +Focus on: + +- prepared facts +- deterministic SSA +- incremental recomputation hooks +- assumption-aware control/dataflow preparation + +### `r2sym` + +Focus on: + +- semantic artifact authority +- evidence algebra +- query planning +- summaries +- replay +- witness generation +- summary composition across consumers + +### `r2types` + +Focus on: + +- semantic type algebra +- stronger `FunctionFacts` +- candidate ranking and refusal logic +- struct/signature/layout confidence + +### `r2dec` + +Focus on: + +- rendering from canonical plans/facts +- less local planning +- stronger helper/summary interpretation +- VM structured summary rendering + +### `r2plugin` + +Focus on: + +- orchestration +- JSON shaping +- radare2-native integration +- shrinking the public surface over time + +### `../radare2` + +Focus on: + +- typed collectors +- persistence of shared analysis metadata +- debugger/trace seams that should exist for all consumers + +Anti-Goals ---------- -- radare2 ESIL: https://book.rada.re/disassembling/esil.html -- Ghidra Sleigh: https://ghidra.re/courses/languages/html/sleigh.html -- P-code reference: https://ghidra.re/courses/languages/html/pcoderef.html +Do not spend roadmap energy on: + +- command count inflation +- plugin-side reparsing of existing commands +- parallel type or semantic owners +- decompiler-local policy that should live upstream +- pretending hard analyses are "`O(1)`" + +If a change improves the subsystem by deleting a command, moving an owner, or +rewriting a seam, that is progress. + +Acceptance Standard +------------------- + +The target plugin system should eventually have these properties: + +- a user can rely on normal radare2 workflows and quietly get better analysis +- symex, types, decompiler, and replay agree on the same facts +- assumptions update the whole subsystem coherently +- summaries are reused across crates instead of being rediscovered +- evidence and fallback reasons are visible and honest +- the public command surface is smaller and smarter, not larger + +That is the gold standard we should optimize toward. diff --git a/crates/r2dec/Cargo.toml b/crates/r2dec/Cargo.toml index 636b6b2..333e862 100644 --- a/crates/r2dec/Cargo.toml +++ b/crates/r2dec/Cargo.toml @@ -8,6 +8,7 @@ description = "Native decompiler for r2sleigh" [dependencies] r2il = { path = "../r2il" } r2ssa = { path = "../r2ssa" } +r2sym = { path = "../r2sym" } r2types = { path = "../r2types" } serde.workspace = true thiserror.workspace = true diff --git a/crates/r2dec/src/consumer_fallback.rs b/crates/r2dec/src/consumer_fallback.rs new file mode 100644 index 0000000..c510082 --- /dev/null +++ b/crates/r2dec/src/consumer_fallback.rs @@ -0,0 +1,167 @@ +use crate::ast::CStmt; + +pub(crate) struct EmptyStructuringFallback { + pub(crate) body_stmt: CStmt, + pub(crate) use_conservative_locals: bool, + pub(crate) is_linear_fallback: bool, +} + +pub(crate) fn recover_empty_structuring<'o, F>( + func: &r2ssa::SSAFunction, + fold_ctx: &'o crate::fold::FoldingContext<'o>, + folded_reason: String, + semantic_worker_linear_reason: Option<&str>, + mut linearize: F, +) -> EmptyStructuringFallback +where + F: FnMut() -> Vec, +{ + let mut unfolded = crate::ControlFlowStructurer::new_unfolded(func, fold_ctx); + let unfolded_stmt = unfolded.structure(); + + if crate::Decompiler::stmt_has_content(&unfolded_stmt) { + return EmptyStructuringFallback { + body_stmt: crate::Decompiler::prepend_comment( + unfolded_stmt, + format!("r2dec fallback: {}", folded_reason), + ), + use_conservative_locals: true, + is_linear_fallback: false, + }; + } + + let unfolded_reason = unfolded + .safety_reason() + .map(str::to_string) + .unwrap_or_else(|| "unfolded structuring produced empty output".to_string()); + let fallback_reason = format!("{}; {}", folded_reason, unfolded_reason); + let mut linear_stmts = linearize(); + + if let Some(reason) = semantic_worker_linear_reason { + return EmptyStructuringFallback { + body_stmt: crate::consumer_structured::semantic_worker_linear_body( + reason, + linear_stmts, + ), + use_conservative_locals: true, + is_linear_fallback: true, + }; + } + + let body_stmt = if linear_stmts.is_empty() { + CStmt::Block(vec![CStmt::comment(format!( + "r2dec fallback: {} -> no statements recovered", + fallback_reason + ))]) + } else { + linear_stmts.insert( + 0, + CStmt::comment(format!( + "r2dec fallback: {} -> linear block emission", + fallback_reason + )), + ); + CStmt::Block(linear_stmts) + }; + + EmptyStructuringFallback { + body_stmt, + use_conservative_locals: true, + is_linear_fallback: true, + } +} + +pub(crate) fn semantic_fallback_comment( + func_name: &str, + function_facts: &r2types::FunctionFacts, +) -> Option { + let semantic_artifact = function_facts.semantic_artifact()?; + if let Some(comment) = + crate::consumer_vm::render_vm_semantic_fallback_comment(func_name, semantic_artifact) + { + return Some(comment); + } + let slice_class = semantic_artifact.slice_class()?; + let mut reason = format!( + "semantic fallback: {} slice in {} mode", + crate::semantic_slice_class_label(slice_class), + crate::semantic_mode_label(semantic_artifact) + ); + if !semantic_artifact.diagnostics.residual_reasons.is_empty() { + reason.push_str(" ("); + reason.push_str( + &semantic_artifact + .diagnostics + .residual_reasons + .iter() + .map(|reason| crate::semantic_residual_reason_label(*reason)) + .collect::>() + .join(", "), + ); + reason.push(')'); + } + if !semantic_artifact.ambiguous_targets().is_empty() { + reason.push_str("; ambiguous_targets=["); + reason.push_str( + &semantic_artifact + .ambiguous_targets() + .into_iter() + .map(|target| format!("0x{target:x}")) + .collect::>() + .join(", "), + ); + reason.push(']'); + } + if let Some(native) = semantic_artifact.native_body() + && !native.regions.is_empty() + { + reason.push_str(&format!( + "; regions={}, actionable_conditions={}, exact_conditions={}", + native.regions.len(), + native.actionable_control_count(), + native.exact_control_count(), + )); + } + let actionable_preview = semantic_artifact + .actionable_regions() + .into_iter() + .filter_map(|region| { + region + .actionable_compiled_condition() + .map(|condition| format!("0x{:x}: {}", region.anchor, condition.simplified)) + }) + .take(3) + .collect::>(); + if !actionable_preview.is_empty() { + reason.push_str("; actionable_preview=["); + reason.push_str(&actionable_preview.join(" | ")); + reason.push(']'); + } + if function_facts.has_assumption_conflicts() { + reason.push_str(&format!( + "; assumption_conflicts={}", + function_facts.assumption_usage.conflicts.len() + )); + } + if let Some(rollup) = function_facts.summary_rollup() { + if let Some(return_relation) = rollup.root_return_relation.as_ref() { + reason.push_str(&format!("; summary_return={return_relation:?}")); + } + if !rollup.out_param_indices.is_empty() { + reason.push_str("; out_params=["); + reason.push_str( + &rollup + .out_param_indices + .iter() + .map(|idx| idx.to_string()) + .collect::>() + .join(", "), + ); + reason.push(']'); + } + } + Some(format!( + "/* r2dec fallback: skipped decompilation for {} ({}) */", + func_name, reason + )) +} diff --git a/crates/r2dec/src/consumer_linear.rs b/crates/r2dec/src/consumer_linear.rs new file mode 100644 index 0000000..f79ec8b --- /dev/null +++ b/crates/r2dec/src/consumer_linear.rs @@ -0,0 +1,65 @@ +use std::fmt::Write as _; + +pub(crate) fn render_semantic_worker_linearization( + plan: &r2types::TypeWritebackPlan, + semantic_artifact: Option<&r2sym::SemanticArtifact>, + reason: &str, +) -> String { + let mut out = String::new(); + for decl in &plan.struct_decls { + let _ = writeln!(&mut out, "{}", decl.decl); + } + if !plan.struct_decls.is_empty() { + out.push('\n'); + } + let _ = writeln!(&mut out, "{} {{", plan.signature.signature); + let _ = writeln!( + &mut out, + " /* r2dec semantic worker linearization: {} */", + reason + ); + for warning in plan.diagnostics.warnings.iter().take(2) { + let _ = writeln!(&mut out, " /* {} */", warning); + } + + let mut emitted_any = false; + for region in semantic_artifact + .map(r2sym::SemanticArtifact::actionable_regions) + .unwrap_or_default() + .into_iter() + .take(6) + { + if let (Some(target), Some(condition)) = ( + region.actionable_reachable_target(), + region.actionable_compiled_condition(), + ) { + let _ = writeln!( + &mut out, + " /* 0x{:x}: if ({}) => 0x{:x} [{:?}] */", + region.anchor, + condition.simplified, + target, + condition.evidence().tier + ); + emitted_any = true; + } + for term in region.actionable_memory_terms().into_iter().take(2) { + let _ = writeln!( + &mut out, + " /* 0x{:x}: {} [{:?}] */", + region.anchor, + term.expr, + term.evidence().tier + ); + emitted_any = true; + } + } + if !emitted_any { + let _ = writeln!( + &mut out, + " /* no actionable worker-island statements recovered */" + ); + } + let _ = writeln!(&mut out, "}}"); + out +} diff --git a/crates/r2dec/src/consumer_structured.rs b/crates/r2dec/src/consumer_structured.rs new file mode 100644 index 0000000..858f750 --- /dev/null +++ b/crates/r2dec/src/consumer_structured.rs @@ -0,0 +1,69 @@ +use crate::ast::CStmt; + +pub(crate) struct RoutedBody { + pub(crate) body_stmt: CStmt, + pub(crate) use_conservative_locals: bool, + pub(crate) is_linear_fallback: bool, +} + +pub(crate) fn primary_body_for_semantic_route<'a, 'o, F>( + route: &crate::SemanticRoutePlan, + structurer: &mut crate::ControlFlowStructurer<'a, 'o>, + mut linearize: F, +) -> RoutedBody +where + F: FnMut() -> Vec, +{ + match route { + crate::SemanticRoutePlan::StructuredWorker { reason } => { + match structurer.structure_semantic_worker_islands(6) { + Some(structured) => RoutedBody { + body_stmt: semantic_worker_structured_body(reason, structured), + use_conservative_locals: true, + is_linear_fallback: false, + }, + None => RoutedBody { + body_stmt: semantic_worker_linear_body(reason, linearize()), + use_conservative_locals: true, + is_linear_fallback: true, + }, + } + } + crate::SemanticRoutePlan::LinearWorker { reason } => RoutedBody { + body_stmt: semantic_worker_linear_body(reason, linearize()), + use_conservative_locals: true, + is_linear_fallback: true, + }, + crate::SemanticRoutePlan::VmSummary { .. } + | crate::SemanticRoutePlan::FallbackComment { .. } + | crate::SemanticRoutePlan::Standard => RoutedBody { + body_stmt: structurer.structure(), + use_conservative_locals: false, + is_linear_fallback: false, + }, + } +} + +pub(crate) fn semantic_worker_structured_body(reason: &str, structured: CStmt) -> CStmt { + CStmt::Block(vec![ + CStmt::comment(format!("r2dec semantic worker structuring for {}", reason)), + structured, + ]) +} + +pub(crate) fn semantic_worker_linear_body(reason: &str, mut linear_stmts: Vec) -> CStmt { + if linear_stmts.is_empty() { + return CStmt::Block(vec![CStmt::comment(format!( + "r2dec fallback: semantic worker linearization for {} -> no statements recovered", + reason + ))]); + } + linear_stmts.insert( + 0, + CStmt::comment(format!( + "r2dec fallback: semantic worker linearization for {}", + reason + )), + ); + CStmt::Block(linear_stmts) +} diff --git a/crates/r2dec/src/consumer_vm.rs b/crates/r2dec/src/consumer_vm.rs new file mode 100644 index 0000000..c303584 --- /dev/null +++ b/crates/r2dec/src/consumer_vm.rs @@ -0,0 +1,365 @@ +use std::fmt::Write as _; + +use r2types::FunctionTypeFacts; + +fn vm_summary_stats_comment(func_name: &str, vm_step: &r2sym::VmStepSummary) -> String { + let kind = crate::format_vm_summary_kind(vm_step.kind); + let selector = vm_step.selector.as_deref().unwrap_or("unknown"); + let inputs = if vm_step.state_inputs.is_empty() { + "none".to_string() + } else { + vm_step.state_inputs.join(", ") + }; + let outputs = if vm_step.state_outputs.is_empty() { + "none".to_string() + } else { + vm_step.state_outputs.join(", ") + }; + let exact_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.exact) + .count(); + let likely_transfers = vm_step + .transfers + .iter() + .filter(|transfer| matches!(transfer.confidence(), r2sym::SemanticConfidence::Likely)) + .count(); + let heuristic_transfers = vm_step + .transfers + .iter() + .filter(|transfer| matches!(transfer.confidence(), r2sym::SemanticConfidence::Heuristic)) + .count(); + let redispatch_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.redispatch) + .count(); + let returning_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.may_return) + .count(); + let selector_updates = vm_step + .transfers + .iter() + .filter(|transfer| transfer.selector_update.is_some()) + .count(); + let exit_guards = vm_step + .transfers + .iter() + .map(|transfer| transfer.exit_guards.len()) + .sum::(); + let residual_guards = vm_step + .transfers + .iter() + .filter(|transfer| transfer.residual_guards) + .count(); + let residual_memory = vm_step + .transfers + .iter() + .filter(|transfer| transfer.residual_memory_effects) + .count(); + let read_effects = vm_step + .handler_memory_read_effects + .values() + .map(Vec::len) + .sum::(); + let write_effects = vm_step + .handler_memory_write_effects + .values() + .map(Vec::len) + .sum::(); + let total_reads: usize = vm_step.handler_memory_reads.values().copied().sum(); + let total_writes: usize = vm_step.handler_memory_writes.values().copied().sum(); + let handler_preview = vm_step + .dispatch_targets + .iter() + .take(3) + .map(|target| { + let values = vm_step + .case_values_by_target + .get(target) + .map(|values| { + values + .iter() + .map(|value| format!("0x{value:x}")) + .collect::>() + .join("|") + }) + .unwrap_or_else(|| "default".to_string()); + let updates = vm_step + .handler_state_updates + .get(target) + .map(|updates| { + updates + .iter() + .take(3) + .map(|update| format!("{}={}", update.output, update.expr)) + .collect::>() + .join(", ") + }) + .filter(|text| !text.is_empty()) + .unwrap_or_else(|| "no_state_updates".to_string()); + format!("0x{target:x}[{values}] => {updates}") + }) + .collect::>() + .join("; "); + format!( + "r2dec semantic summary: vm_summary for {} ({} @ 0x{:x}, loop_header=0x{:x}, selector={}, targets={}, redispatch={}, exact_transfers={}, likely_transfers={}, heuristic_transfers={}, redispatch_transfers={}, returning_transfers={}, selector_updates={}, exact_exit_guards={}, residual_guards={}, residual_memory={}, total_reads={}, total_writes={}, read_effects={}, write_effects={}, state_inputs=[{}], state_outputs=[{}], handlers={})", + func_name, + kind, + vm_step.dispatch_header, + vm_step.loop_header, + selector, + vm_step.dispatch_targets.len(), + vm_step.redispatch_handlers.len(), + exact_transfers, + likely_transfers, + heuristic_transfers, + redispatch_transfers, + returning_transfers, + selector_updates, + exit_guards, + residual_guards, + residual_memory, + total_reads, + total_writes, + read_effects, + write_effects, + inputs, + outputs, + handler_preview, + ) +} + +fn render_vm_signature(type_facts: &FunctionTypeFacts, func_name: &str) -> String { + let Some(signature) = type_facts.merged_signature.as_ref() else { + return format!("void {func_name}(void)"); + }; + let ret_ty = signature + .ret_type + .as_ref() + .map(crate::type_like_to_ctype) + .unwrap_or(crate::CType::Void); + let params = if signature.params.is_empty() { + "void".to_string() + } else { + signature + .params + .iter() + .enumerate() + .map(|(idx, param)| { + let ty = param + .ty + .as_ref() + .map(crate::type_like_to_ctype) + .unwrap_or(crate::CType::Unknown); + let name = if param.name.trim().is_empty() { + format!("arg{}", idx + 1) + } else { + param.name.clone() + }; + format!("{ty} {name}") + }) + .collect::>() + .join(", ") + }; + format!("{ret_ty} {func_name}({params})") +} + +fn render_vm_memory_condition(effect: &r2sym::VmMemoryCondition) -> String { + let binding = effect.binding.as_deref().unwrap_or("value"); + let range = if effect.offset_lo == effect.offset_hi { + format!("{:+#x}", effect.offset_lo) + } else { + format!("{:+#x}..{:+#x}", effect.offset_lo, effect.offset_hi) + }; + let value = effect + .value_expr + .as_deref() + .or(Some(effect.expr.as_str())) + .unwrap_or(binding); + format!( + "{}[{:?} {}] size={} {}", + effect.region.name, effect.region.kind, range, effect.size, value + ) +} + +fn render_vm_case_block( + out: &mut String, + vm_step: &r2sym::VmStepSummary, + target: u64, + default_emitted: &mut bool, +) { + let case_values = vm_step + .case_values_by_target + .get(&target) + .cloned() + .unwrap_or_default(); + if case_values.is_empty() && vm_step.default_target == Some(target) { + let _ = writeln!(out, " default:"); + *default_emitted = true; + } else if case_values.is_empty() { + if !*default_emitted { + let _ = writeln!(out, " default:"); + *default_emitted = true; + } else { + let _ = writeln!( + out, + " /* unlabeled handler 0x{:x} omitted from switch surface */", + target + ); + return; + } + } else { + for value in case_values { + let _ = writeln!(out, " case 0x{value:x}:"); + } + } + + let region_blocks = vm_step + .handler_regions + .get(&target) + .map(|blocks| crate::format_vm_target_list(blocks)) + .unwrap_or_else(|| "[]".to_string()); + let _ = writeln!( + out, + " /* handler 0x{:x} blocks={} */", + target, region_blocks + ); + + let mut emitted_body = false; + for transfer in vm_step + .transfers + .iter() + .filter(|transfer| transfer.handler_target == target) + { + for update in transfer.state_updates.iter().take(4) { + let _ = writeln!(out, " {} = {};", update.output, update.expr); + emitted_body = true; + } + if let Some(selector_update) = transfer.selector_update.as_ref() { + let _ = writeln!( + out, + " {} = {};", + selector_update.output, selector_update.expr + ); + emitted_body = true; + } + for guard in transfer.exit_guards.iter().take(3) { + let _ = writeln!( + out, + " /* if ({}) -> 0x{:x} [{:?}] */", + guard.guard.expr, + guard.target, + guard.guard.confidence() + ); + emitted_body = true; + } + for effect in transfer.memory_reads.iter().take(2) { + let _ = writeln!( + out, + " /* read {} [{:?}] */", + render_vm_memory_condition(effect), + effect.confidence() + ); + emitted_body = true; + } + for effect in transfer.memory_writes.iter().take(2) { + let _ = writeln!( + out, + " /* write {} [{:?}] */", + render_vm_memory_condition(effect), + effect.confidence() + ); + emitted_body = true; + } + if transfer.redispatch { + let _ = writeln!(out, " /* redispatch */"); + emitted_body = true; + } + if transfer.may_return { + let _ = writeln!(out, " /* may return */"); + emitted_body = true; + } + if transfer.truncated { + let _ = writeln!(out, " /* truncated handler summary */"); + emitted_body = true; + } + } + + if !emitted_body { + let _ = writeln!(out, " /* no exact handler body recovered */"); + } + let _ = writeln!(out, " break;"); +} + +pub(crate) fn render_vm_semantic_summary( + func_name: &str, + type_facts: &FunctionTypeFacts, + semantic_artifact: &r2sym::SemanticArtifact, +) -> Option { + let vm_body = semantic_artifact.vm_body()?; + let vm_step = vm_body + .step_summary + .as_ref() + .or(vm_body.transfer_summary.as_ref())?; + let selector = vm_step.selector.as_deref().unwrap_or("dispatch_selector"); + let mut out = String::new(); + + let _ = writeln!(out, "{} {{", render_vm_signature(type_facts, func_name)); + let _ = writeln!( + out, + " /* {} */", + vm_summary_stats_comment(func_name, vm_step) + ); + if !vm_step.state_inputs.is_empty() || !vm_step.state_outputs.is_empty() { + let _ = writeln!( + out, + " /* state_inputs=[{}] state_outputs=[{}] */", + if vm_step.state_inputs.is_empty() { + "none".to_string() + } else { + vm_step.state_inputs.join(", ") + }, + if vm_step.state_outputs.is_empty() { + "none".to_string() + } else { + vm_step.state_outputs.join(", ") + }, + ); + } + let _ = writeln!(out, " switch ({selector}) {{"); + let mut default_emitted = false; + for target in &vm_step.dispatch_targets { + render_vm_case_block(&mut out, vm_step, *target, &mut default_emitted); + } + if !default_emitted { + let _ = writeln!(out, " default:"); + if let Some(default_target) = vm_step.default_target { + let _ = writeln!(out, " /* default_target=0x{:x} */", default_target); + } else { + let _ = writeln!(out, " /* no default target recovered */"); + } + let _ = writeln!(out, " break;"); + } + let _ = writeln!(out, " }}"); + let _ = writeln!(out, "}}"); + Some(out) +} + +pub(crate) fn render_vm_semantic_fallback_comment( + func_name: &str, + semantic_artifact: &r2sym::SemanticArtifact, +) -> Option { + let vm_body = semantic_artifact.vm_body()?; + let vm_step = vm_body + .step_summary + .as_ref() + .or(vm_body.transfer_summary.as_ref())?; + Some(format!( + "/* {} */", + vm_summary_stats_comment(func_name, vm_step) + )) +} diff --git a/crates/r2dec/src/fold/context.rs b/crates/r2dec/src/fold/context.rs index a3d054a..c6ba275 100644 --- a/crates/r2dec/src/fold/context.rs +++ b/crates/r2dec/src/fold/context.rs @@ -11,8 +11,8 @@ use r2ssa::{ #[cfg(test)] use r2types::ExternalStackVarSpec; use r2types::{ - CalleeFact, ExternalStackSlotSpec, ExternalTypeDb, FunctionType, SignatureRegistry, - StackSlotKey, TypeOracle, VisibleBinding, + CalleeFact, ExternalStackSlotSpec, ExternalTypeDb, FunctionType, InterprocSummaryView, + SignatureRegistry, StackSlotKey, TypeOracle, VisibleBinding, }; pub(crate) type SSABlock = FunctionSSABlock; @@ -65,12 +65,14 @@ pub(crate) struct FoldInputs<'a> { pub(crate) external_stack_vars: &'a HashMap, pub(crate) visible_bindings: &'a [VisibleBinding], pub(crate) external_type_db: &'a ExternalTypeDb, + pub(crate) semantic_artifact: Option<&'a r2sym::SemanticArtifact>, pub(crate) param_register_aliases: &'a HashMap, pub(crate) type_hints: &'a HashMap, pub(crate) type_oracle: Option<&'a dyn TypeOracle>, pub(crate) function_return_type: Option<&'a CType>, pub(crate) prepared_ssa: Option<&'a SsaArtifact>, pub(crate) interproc_summary_set: Option<&'a InterprocSummarySet>, + pub(crate) summary_view: Option<&'a InterprocSummaryView>, pub(crate) prepared_semantic_view: Option<&'a analysis::PreparedSemanticView>, pub(crate) prepared_objects: Option<&'a ObjectModel>, #[allow(dead_code)] @@ -232,12 +234,14 @@ impl<'a> FoldingContext<'a> { external_stack_vars: EMPTY_I64_STACK.get_or_init(HashMap::new), visible_bindings: EMPTY_VISIBLE_BINDINGS.get_or_init(Vec::new), external_type_db: EMPTY_TYPE_DB.get_or_init(ExternalTypeDb::default), + semantic_artifact: None, param_register_aliases: EMPTY_STRING_STRING.get_or_init(HashMap::new), type_hints: EMPTY_STRING_CTYPE.get_or_init(HashMap::new), type_oracle: None, function_return_type: None, prepared_ssa: None, interproc_summary_set: None, + summary_view: None, prepared_semantic_view: None, prepared_objects: None, prepared_memory: None, diff --git a/crates/r2dec/src/fold/flags.rs b/crates/r2dec/src/fold/flags.rs index eb385b4..2420f87 100644 --- a/crates/r2dec/src/fold/flags.rs +++ b/crates/r2dec/src/fold/flags.rs @@ -5,16 +5,15 @@ use r2ssa::{ CompareKind as PreparedCompareKind, CompareProvenance, FunctionSSABlock, SSAOp, SSAVar, }; -use crate::analysis; -use crate::analysis::{FlagCompareKind, FlagCompareProvenance, utils}; -use crate::ast::{BinaryOp, CExpr, CType, UnaryOp}; - use super::context::FoldingContext; use super::op_lower::parse_const_value; use super::{ MAX_COND_STACK_ALIAS_DEPTH, MAX_PREDICATE_OPERAND_DEPTH, MAX_PREDICATE_SIMPLIFY_DEPTH, MAX_SF_SURROGATE_DEPTH, MAX_SUB_LIKE_DEPTH, }; +use crate::analysis; +use crate::analysis::{FlagCompareKind, FlagCompareProvenance, utils}; +use crate::ast::{BinaryOp, CExpr, CType, UnaryOp}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) enum CompareContext { @@ -30,6 +29,347 @@ pub(super) struct CompareTuple { context: CompareContext, } +#[derive(Debug, Clone, PartialEq, Eq)] +enum SymbolicConditionToken { + Atom(String), + LParen, + RParen, + Not, + BitNot, + Star, + Slash, + Percent, + Plus, + Minus, + Shl, + Shr, + Lt, + Le, + Gt, + Ge, + Eq, + Ne, + BitAnd, + BitXor, + BitOr, + And, + Or, +} + +struct SymbolicConditionExprParser<'ctx, 'a> { + ctx: &'ctx FoldingContext<'a>, + tokens: Vec, + pos: usize, +} + +impl<'ctx, 'a> SymbolicConditionExprParser<'ctx, 'a> { + fn new(ctx: &'ctx FoldingContext<'a>, text: &str) -> Option { + Some(Self { + ctx, + tokens: tokenize_symbolic_condition(text)?, + pos: 0, + }) + } + + fn parse(mut self) -> Option { + let expr = self.parse_logical_or()?; + (self.pos == self.tokens.len()).then_some(expr) + } + + fn peek(&self) -> Option<&SymbolicConditionToken> { + self.tokens.get(self.pos) + } + + fn next(&mut self) -> Option { + let token = self.tokens.get(self.pos)?.clone(); + self.pos += 1; + Some(token) + } + + fn parse_logical_or(&mut self) -> Option { + let mut expr = self.parse_logical_and()?; + while matches!(self.peek(), Some(SymbolicConditionToken::Or)) { + self.next(); + let rhs = self.parse_logical_and()?; + expr = CExpr::binary(BinaryOp::Or, expr, rhs); + } + Some(expr) + } + + fn parse_logical_and(&mut self) -> Option { + let mut expr = self.parse_bit_or()?; + while matches!(self.peek(), Some(SymbolicConditionToken::And)) { + self.next(); + let rhs = self.parse_bit_or()?; + expr = CExpr::binary(BinaryOp::And, expr, rhs); + } + Some(expr) + } + + fn parse_bit_or(&mut self) -> Option { + let mut expr = self.parse_bit_xor()?; + while matches!(self.peek(), Some(SymbolicConditionToken::BitOr)) { + self.next(); + let rhs = self.parse_bit_xor()?; + expr = CExpr::binary(BinaryOp::BitOr, expr, rhs); + } + Some(expr) + } + + fn parse_bit_xor(&mut self) -> Option { + let mut expr = self.parse_bit_and()?; + while matches!(self.peek(), Some(SymbolicConditionToken::BitXor)) { + self.next(); + let rhs = self.parse_bit_and()?; + expr = CExpr::binary(BinaryOp::BitXor, expr, rhs); + } + Some(expr) + } + + fn parse_bit_and(&mut self) -> Option { + let mut expr = self.parse_equality()?; + while matches!(self.peek(), Some(SymbolicConditionToken::BitAnd)) { + self.next(); + let rhs = self.parse_equality()?; + expr = CExpr::binary(BinaryOp::BitAnd, expr, rhs); + } + Some(expr) + } + + fn parse_equality(&mut self) -> Option { + let mut expr = self.parse_relational()?; + loop { + let op = match self.peek() { + Some(SymbolicConditionToken::Eq) => BinaryOp::Eq, + Some(SymbolicConditionToken::Ne) => BinaryOp::Ne, + _ => break, + }; + self.next(); + let rhs = self.parse_relational()?; + expr = CExpr::binary(op, expr, rhs); + } + Some(expr) + } + + fn parse_relational(&mut self) -> Option { + let mut expr = self.parse_shift()?; + loop { + let op = match self.peek() { + Some(SymbolicConditionToken::Lt) => BinaryOp::Lt, + Some(SymbolicConditionToken::Le) => BinaryOp::Le, + Some(SymbolicConditionToken::Gt) => BinaryOp::Gt, + Some(SymbolicConditionToken::Ge) => BinaryOp::Ge, + _ => break, + }; + self.next(); + let rhs = self.parse_shift()?; + expr = CExpr::binary(op, expr, rhs); + } + Some(expr) + } + + fn parse_shift(&mut self) -> Option { + let mut expr = self.parse_additive()?; + loop { + let op = match self.peek() { + Some(SymbolicConditionToken::Shl) => BinaryOp::Shl, + Some(SymbolicConditionToken::Shr) => BinaryOp::Shr, + _ => break, + }; + self.next(); + let rhs = self.parse_additive()?; + expr = CExpr::binary(op, expr, rhs); + } + Some(expr) + } + + fn parse_additive(&mut self) -> Option { + let mut expr = self.parse_multiplicative()?; + loop { + let op = match self.peek() { + Some(SymbolicConditionToken::Plus) => BinaryOp::Add, + Some(SymbolicConditionToken::Minus) => BinaryOp::Sub, + _ => break, + }; + self.next(); + let rhs = self.parse_multiplicative()?; + expr = CExpr::binary(op, expr, rhs); + } + Some(expr) + } + + fn parse_multiplicative(&mut self) -> Option { + let mut expr = self.parse_unary()?; + loop { + let op = match self.peek() { + Some(SymbolicConditionToken::Star) => BinaryOp::Mul, + Some(SymbolicConditionToken::Slash) => BinaryOp::Div, + Some(SymbolicConditionToken::Percent) => BinaryOp::Mod, + _ => break, + }; + self.next(); + let rhs = self.parse_unary()?; + expr = CExpr::binary(op, expr, rhs); + } + Some(expr) + } + + fn parse_unary(&mut self) -> Option { + match self.peek() { + Some(SymbolicConditionToken::Not) => { + self.next(); + Some(CExpr::unary(UnaryOp::Not, self.parse_unary()?)) + } + Some(SymbolicConditionToken::BitNot) => { + self.next(); + Some(CExpr::unary(UnaryOp::BitNot, self.parse_unary()?)) + } + Some(SymbolicConditionToken::Minus) => { + self.next(); + Some(CExpr::unary(UnaryOp::Neg, self.parse_unary()?)) + } + Some(SymbolicConditionToken::Star) => { + self.next(); + Some(CExpr::deref(self.parse_unary()?)) + } + _ => self.parse_primary(), + } + } + + fn parse_primary(&mut self) -> Option { + match self.next()? { + SymbolicConditionToken::LParen => { + let expr = self.parse_logical_or()?; + matches!(self.next(), Some(SymbolicConditionToken::RParen)).then_some(expr) + } + SymbolicConditionToken::Atom(atom) => { + let lower = atom.to_ascii_lowercase(); + if lower == "true" { + Some(CExpr::IntLit(1)) + } else if lower == "false" { + Some(CExpr::IntLit(0)) + } else { + Some( + self.ctx + .parse_expr_from_name(&atom) + .unwrap_or(CExpr::Var(atom)), + ) + } + } + _ => None, + } + } +} + +fn tokenize_symbolic_condition(text: &str) -> Option> { + let mut tokens = Vec::new(); + let chars = text.chars().collect::>(); + let mut i = 0usize; + while i < chars.len() { + let ch = chars[i]; + if ch.is_whitespace() { + i += 1; + continue; + } + let rest = &chars[i..]; + let push_two = |token: SymbolicConditionToken, i: &mut usize, tokens: &mut Vec<_>| { + tokens.push(token); + *i += 2; + }; + if rest.len() >= 2 { + match (rest[0], rest[1]) { + ('|', '|') => { + push_two(SymbolicConditionToken::Or, &mut i, &mut tokens); + continue; + } + ('&', '&') => { + push_two(SymbolicConditionToken::And, &mut i, &mut tokens); + continue; + } + ('=', '=') => { + push_two(SymbolicConditionToken::Eq, &mut i, &mut tokens); + continue; + } + ('!', '=') => { + push_two(SymbolicConditionToken::Ne, &mut i, &mut tokens); + continue; + } + ('<', '=') => { + push_two(SymbolicConditionToken::Le, &mut i, &mut tokens); + continue; + } + ('>', '=') => { + push_two(SymbolicConditionToken::Ge, &mut i, &mut tokens); + continue; + } + ('<', '<') => { + push_two(SymbolicConditionToken::Shl, &mut i, &mut tokens); + continue; + } + ('>', '>') => { + push_two(SymbolicConditionToken::Shr, &mut i, &mut tokens); + continue; + } + _ => {} + } + } + match ch { + '(' => tokens.push(SymbolicConditionToken::LParen), + ')' => tokens.push(SymbolicConditionToken::RParen), + '!' => tokens.push(SymbolicConditionToken::Not), + '~' => tokens.push(SymbolicConditionToken::BitNot), + '*' => tokens.push(SymbolicConditionToken::Star), + '/' => tokens.push(SymbolicConditionToken::Slash), + '%' => tokens.push(SymbolicConditionToken::Percent), + '+' => tokens.push(SymbolicConditionToken::Plus), + '-' => tokens.push(SymbolicConditionToken::Minus), + '<' => tokens.push(SymbolicConditionToken::Lt), + '>' => tokens.push(SymbolicConditionToken::Gt), + '&' => tokens.push(SymbolicConditionToken::BitAnd), + '^' => tokens.push(SymbolicConditionToken::BitXor), + '|' => tokens.push(SymbolicConditionToken::BitOr), + '=' => return None, + _ => { + let start = i; + while i < chars.len() { + let ch = chars[i]; + if ch.is_whitespace() + || matches!( + ch, + '(' | ')' + | '!' + | '~' + | '*' + | '/' + | '%' + | '+' + | '-' + | '<' + | '>' + | '&' + | '^' + | '|' + | '=' + ) + { + break; + } + i += 1; + } + if start == i { + return None; + } + tokens.push(SymbolicConditionToken::Atom( + chars[start..i].iter().collect(), + )); + continue; + } + } + i += 1; + } + Some(tokens) +} + impl<'a> FoldingContext<'a> { fn finalize_condition_expr(&self, expr: CExpr) -> CExpr { let expr = self.normalize_local_branch_expr(expr); @@ -87,6 +427,68 @@ impl<'a> FoldingContext<'a> { .and_then(|view| view.branch_expr_for_block(block_addr).cloned()) } + fn symbolic_branch_condition_expr(&self, block_addr: u64) -> Option { + match self + .inputs + .semantic_artifact? + .exact_branch_truth_for_block(block_addr) + { + Some(true) => Some(CExpr::IntLit(1)), + Some(false) => Some(CExpr::IntLit(0)), + None => None, + } + } + + fn symbolic_actionable_compiled_condition( + &self, + block_addr: u64, + ) -> Option<&r2sym::BackwardConditionSummary> { + self.inputs + .semantic_artifact? + .actionable_compiled_condition_for_block(block_addr) + } + + fn symbolic_actionable_compiled_condition_expr(&self, block_addr: u64) -> Option { + let compiled = self.symbolic_actionable_compiled_condition(block_addr)?; + SymbolicConditionExprParser::new(self, compiled.simplified.trim()) + .and_then(SymbolicConditionExprParser::parse) + .map(|expr| self.finalize_condition_expr(expr)) + } + + fn symbolic_actionable_memory_condition_expr(&self, block_addr: u64) -> Option { + fn memory_term_rank(term: &r2sym::BackwardMemoryCondition) -> (u8, bool, bool, i64, i64) { + let evidence_rank = match term.evidence().tier { + r2sym::SemanticConfidence::Exact => 3, + r2sym::SemanticConfidence::Likely => 2, + r2sym::SemanticConfidence::Heuristic => 1, + r2sym::SemanticConfidence::Residual => 0, + }; + ( + evidence_rank, + term.exact_value, + term.exact_offset, + -(term.offset_hi - term.offset_lo), + -term.offset_lo.abs(), + ) + } + + let term = self + .inputs + .semantic_artifact? + .actionable_memory_terms_for_block(block_addr) + .into_iter() + .filter(|term| { + term.value_expr + .as_ref() + .is_some_and(|value| value != &term.expr) + }) + .max_by_key(|term| memory_term_rank(term))?; + let condition = format!("({} == {})", term.expr.trim(), term.value_expr.as_deref()?); + SymbolicConditionExprParser::new(self, &condition) + .and_then(SymbolicConditionExprParser::parse) + .map(|expr| self.finalize_condition_expr(expr)) + } + fn prepared_predicate_view(&self) -> Option> { self.prepared_semantic_view().map(Cow::Borrowed) } @@ -292,6 +694,18 @@ impl<'a> FoldingContext<'a> { } pub fn extract_condition_from_block(&self, block: &FunctionSSABlock) -> Option { + if let Some(cond) = self.symbolic_actionable_compiled_condition_expr(block.addr) { + return Some(cond); + } + + if let Some(cond) = self.symbolic_actionable_memory_condition_expr(block.addr) { + return Some(cond); + } + + if let Some(cond) = self.symbolic_branch_condition_expr(block.addr) { + return Some(cond); + } + let (branch_idx, cond) = block .ops @@ -306,14 +720,16 @@ impl<'a> FoldingContext<'a> { let prepared_block_candidate = self.prepared_predicate_candidate_for_branch_block(block.addr, cond); let prepared_var_candidate = self.prepared_predicate_candidate_for_var(cond); - let allow_legacy_flag_provenance = ![ - prepared_branch_candidate.as_ref(), - prepared_block_candidate.as_ref(), - prepared_var_candidate.as_ref(), - ] - .into_iter() - .flatten() - .any(|expr| !self.prepared_candidate_needs_legacy_compare_help(expr)); + let exact_compiled = self.symbolic_actionable_compiled_condition(block.addr); + let allow_legacy_flag_provenance = exact_compiled.is_none() + && ![ + prepared_branch_candidate.as_ref(), + prepared_block_candidate.as_ref(), + prepared_var_candidate.as_ref(), + ] + .into_iter() + .flatten() + .any(|expr| !self.prepared_candidate_needs_legacy_compare_help(expr)); let prev_block_addr = self.current_block_addr.replace(Some(block.addr)); let prev_op_idx = self.current_op_idx.replace(Some(branch_idx)); @@ -630,6 +1046,15 @@ impl<'a> FoldingContext<'a> { self.prepared_compare_provenance_expr(compare) } + #[cfg(test)] + pub(super) fn prepared_predicate_candidate_for_branch_block_for_test( + &self, + block_addr: u64, + var: &SSAVar, + ) -> Option { + self.prepared_predicate_candidate_for_branch_block(block_addr, var) + } + fn prepared_compare_provenance_expr(&self, prov: &CompareProvenance) -> Option { let lhs_var = self.prepared_var_for_value_id(prov.lhs)?; let rhs_var = self.prepared_var_for_value_id(prov.rhs)?; diff --git a/crates/r2dec/src/fold/op_lower/calls.rs b/crates/r2dec/src/fold/op_lower/calls.rs index 7cd4cf1..8b7aacf 100644 --- a/crates/r2dec/src/fold/op_lower/calls.rs +++ b/crates/r2dec/src/fold/op_lower/calls.rs @@ -158,7 +158,7 @@ impl<'a> FoldingContext<'a> { .lookup_known_signature(name) .and_then(|sig| (!sig.variadic).then_some(sig.params.len())); let summary_arity = self - .interproc_summary_for_name(name) + .summary_helper_view_for_name(name) .and_then(|summary| summary.arg_count_hint); let normalized = normalize_callee_name(name); @@ -202,7 +202,7 @@ impl<'a> FoldingContext<'a> { }; let normalized = normalize_callee_name(name); - if self.interproc_summary_for_name(name).is_some() { + if self.summary_helper_view_for_name(name).is_some() { return true; } diff --git a/crates/r2dec/src/fold/op_lower/mod.rs b/crates/r2dec/src/fold/op_lower/mod.rs index 9712e52..4f8f406 100644 --- a/crates/r2dec/src/fold/op_lower/mod.rs +++ b/crates/r2dec/src/fold/op_lower/mod.rs @@ -23,9 +23,9 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use r2ssa::{ - CallSiteFact, CallSiteFacts, DecompilePrepFacts, FunctionSemanticSummary, InterprocSummarySet, - MemoryDefFact, MemoryLocation, MemoryUseFact, ObjectKind, ObjectModel, PredicateFacts, - PreparedFunctionFacts, SSAFunction, SSAOp, SSAVar, SsaArtifact, + CallSiteFact, CallSiteFacts, DecompilePrepFacts, MemoryDefFact, MemoryLocation, MemoryUseFact, + ObjectKind, ObjectModel, PredicateFacts, PreparedFunctionFacts, SSAFunction, SSAOp, SSAVar, + SsaArtifact, }; #[cfg(test)] use r2types::StackSlotKey; @@ -172,8 +172,15 @@ impl<'a> FoldingContext<'a> { self.inputs.prepared_ssa } - pub(crate) fn interproc_summary_set(&self) -> Option<&InterprocSummarySet> { - self.inputs.interproc_summary_set + pub(crate) fn summary_view(&self) -> Option<&r2types::InterprocSummaryView> { + self.inputs.summary_view + } + + pub(crate) fn summary_helper_view_for_name( + &self, + name: &str, + ) -> Option<&r2types::SummaryHelperView> { + self.summary_view()?.helper_view_for_name(name) } pub(crate) fn prepared_semantic_view(&self) -> Option<&analysis::PreparedSemanticView> { @@ -277,22 +284,6 @@ impl<'a> FoldingContext<'a> { .and_then(|view| view.call_view_for_site((block_addr, op_idx))) } - pub(crate) fn interproc_summary_for_name( - &self, - name: &str, - ) -> Option<&FunctionSemanticSummary> { - let normalized = normalize_callee_name(name); - self.interproc_summary_set()? - .summaries - .values() - .find(|summary| { - summary - .name - .as_deref() - .is_some_and(|summary_name| normalize_callee_name(summary_name) == normalized) - }) - } - pub(crate) fn prepared_memory_uses_for_current_op(&self) -> Option<&[MemoryUseFact]> { let prepared = self.inputs.prepared_ssa?; let block_addr = self.current_block_addr.get()?; diff --git a/crates/r2dec/src/fold/tests/flags.rs b/crates/r2dec/src/fold/tests/flags.rs index 1968ad4..80c1b2f 100644 --- a/crates/r2dec/src/fold/tests/flags.rs +++ b/crates/r2dec/src/fold/tests/flags.rs @@ -1,4 +1,76 @@ use super::*; +use crate::fold::FoldingContext; +use crate::fold::context::{FoldArchConfig, FoldInputs}; +use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, Varnode}; +use std::collections::{BTreeMap, HashMap, HashSet}; + +fn make_test_arch_x86_64() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.add_register(RegisterDef::new("RAX", 0x00, 8)); + arch.add_register(RegisterDef::new("RDI", 0x10, 8)); + arch.add_register(RegisterDef::new("RSI", 0x18, 8)); + arch.add_register(RegisterDef::new("RBP", 0x20, 8)); + arch.add_register(RegisterDef::new("RSP", 0x28, 8)); + arch +} + +fn prepared_from_r2il_blocks(blocks: &[R2ILBlock], arch: &ArchSpec) -> r2ssa::SsaArtifact { + r2ssa::SsaArtifact::for_decompile(blocks, Some(arch)).expect("prepared SSA should build") +} + +fn make_x86_64_ctx_with_prepared<'a>(prepared_ssa: &'a r2ssa::SsaArtifact) -> FoldingContext<'a> { + let arch = Box::leak(Box::new(FoldArchConfig { + ptr_size: 64, + sp_name: "rsp".to_string(), + fp_name: "rbp".to_string(), + ret_reg_name: "rax".to_string(), + arg_regs: vec![ + "rdi".to_string(), + "rsi".to_string(), + "rdx".to_string(), + "rcx".to_string(), + "r8".to_string(), + "r9".to_string(), + ], + caller_saved_regs: HashSet::new(), + })); + let empty_u64 = Box::leak(Box::new(HashMap::new())); + let empty_stack = Box::leak(Box::new(HashMap::new())); + let empty_stack_slots = Box::leak(Box::new(BTreeMap::new())); + let empty_visible = Box::leak(Box::new(Vec::new())); + let empty_str = Box::leak(Box::new(HashMap::new())); + let empty_fn = Box::leak(Box::new(HashMap::new())); + let empty_callee = Box::leak(Box::new(BTreeMap::new())); + let empty_ty = Box::leak(Box::new(HashMap::new())); + + let mut ctx = FoldingContext::from_inputs(FoldInputs { + arch, + function_names: empty_u64, + strings: empty_u64, + symbols: empty_u64, + known_function_signatures: empty_fn, + callee_facts: empty_callee, + stack_slots: empty_stack_slots, + external_stack_vars: empty_stack, + visible_bindings: empty_visible, + external_type_db: Box::leak(Box::new(r2types::ExternalTypeDb::default())), + semantic_artifact: None, + param_register_aliases: empty_str, + type_hints: empty_ty, + type_oracle: None, + function_return_type: None, + prepared_ssa: None, + interproc_summary_set: None, + summary_view: None, + prepared_semantic_view: None, + prepared_objects: None, + prepared_memory: None, + prepared_predicates: None, + prepared_call_sites: None, + }); + ctx.inputs.prepared_ssa = Some(prepared_ssa); + ctx +} #[test] fn expr_contains_opaque_temp_uses_visit_over_nested_nodes() { @@ -75,3 +147,300 @@ fn tmp_flag_aliases_reconstruct_signed_ge_condition() { ) ); } + +#[test] +fn exact_compiled_condition_shortcuts_to_literal_condition() { + let arch = make_test_arch_x86_64(); + let mut entry = R2ILBlock::new(0x1000, 4); + entry.push(R2ILOp::IntNotEqual { + dst: Varnode::unique(1, 1), + a: Varnode::register(0x10, 4), + b: Varnode::constant(0, 4), + }); + entry.push(R2ILOp::CBranch { + target: Varnode::constant(0x1008, 8), + cond: Varnode::unique(1, 1), + }); + let mut fallthrough = R2ILBlock::new(0x1004, 4); + fallthrough.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + let mut taken = R2ILBlock::new(0x1008, 4); + taken.push(R2ILOp::Return { + target: Varnode::constant(1, 8), + }); + + let prepared = + prepared_from_r2il_blocks(&[entry, fallthrough, taken], &arch).with_name("exact_compiled"); + let mut ctx = make_x86_64_ctx_with_prepared(&prepared); + let region = crate::test_semantic_region( + 0x1000, + std::collections::BTreeSet::from([0x1004, 0x1008]), + vec![crate::test_control_fact( + 0x1004, + r2sym::SymbolicReachabilityStatus::Reachable, + Some(false), + Some("false"), + Some(r2sym::BackwardConditionSummary { + simplified: "false".to_string(), + terms: vec!["false".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + r2sym::SemanticEvidence::exact(), + )], + Vec::new(), + ); + ctx.inputs.semantic_artifact = Some(crate::leaked_test_semantic_artifact( + crate::test_native_semantic_artifact( + r2sym::RefinementStage::Compiled, + r2sym::ArtifactGranularity::WholeFunction, + r2sym::SliceClass::Worker, + false, + Vec::new(), + vec![region], + ), + )); + + let entry = prepared.function().get_block(0x1000).expect("entry"); + assert_eq!( + ctx.symbolic_actionable_compiled_condition_expr(entry.addr), + Some(CExpr::IntLit(0)) + ); + assert_eq!( + ctx.extract_condition_from_block(entry), + Some(CExpr::IntLit(0)) + ); +} + +#[test] +fn actionable_control_island_shortcuts_when_branch_fact_is_not_decisive() { + let arch = make_test_arch_x86_64(); + let mut entry = R2ILBlock::new(0x2000, 4); + entry.push(R2ILOp::IntNotEqual { + dst: Varnode::unique(1, 1), + a: Varnode::register(0x10, 4), + b: Varnode::constant(0, 4), + }); + entry.push(R2ILOp::CBranch { + target: Varnode::constant(0x2008, 8), + cond: Varnode::unique(1, 1), + }); + let mut fallthrough = R2ILBlock::new(0x2004, 4); + fallthrough.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + let mut taken = R2ILBlock::new(0x2008, 4); + taken.push(R2ILOp::Return { + target: Varnode::constant(1, 8), + }); + + let prepared = + prepared_from_r2il_blocks(&[entry, fallthrough, taken], &arch).with_name("control_island"); + let mut ctx = make_x86_64_ctx_with_prepared(&prepared); + let frontier = std::collections::BTreeSet::from([0x2004, 0x2008]); + let region = r2sym::SemanticRegion { + anchor: 0x2000, + frontier: frontier.clone(), + control: vec![r2sym::Judged::new( + r2sym::ControlFact { + target: 0x2008, + status: r2sym::SymbolicReachabilityStatus::Unknown, + branch_truth: Some(true), + condition: Some("false".to_string()), + compiled: Some(r2sym::BackwardConditionSummary { + simplified: "false".to_string(), + terms: vec!["false".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + r2sym::SemanticEvidence::exact(), + )], + memory: Vec::new(), + pre: Vec::new(), + post: Vec::new(), + targets: Vec::new(), + }; + let artifact = r2sym::SemanticArtifact { + stage: r2sym::RefinementStage::Compiled, + granularity: r2sym::ArtifactGranularity::Regioned, + execution: r2sym::ExecutionModel::Native, + body: r2sym::SemanticArtifactBody::Native(r2sym::NativeArtifactBody { + summary: r2sym::NativeFunctionSummary { + slice_class: r2sym::SliceClass::Worker, + closure_functions: 1, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: Default::default(), + }, + regions: std::collections::BTreeMap::from([(region.key(), region)]), + }), + diagnostics: r2sym::SemanticArtifactDiagnostics { + branches_evaluated: 0, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: false, + residual_reasons: Vec::new(), + ambiguous_targets: Vec::new(), + cache_hit: false, + }, + }; + ctx.inputs.semantic_artifact = Some(Box::leak(Box::new(artifact))); + + let entry = prepared.function().get_block(0x2000).expect("entry"); + assert_eq!( + ctx.symbolic_actionable_compiled_condition_expr(entry.addr), + Some(CExpr::IntLit(0)) + ); +} + +#[test] +fn actionable_control_island_parses_non_literal_compiled_condition_expr() { + let arch = make_test_arch_x86_64(); + let mut entry = R2ILBlock::new(0x3000, 4); + entry.push(R2ILOp::IntNotEqual { + dst: Varnode::unique(1, 1), + a: Varnode::register(0x10, 4), + b: Varnode::constant(0, 4), + }); + entry.push(R2ILOp::CBranch { + target: Varnode::constant(0x3008, 8), + cond: Varnode::unique(1, 1), + }); + let mut fallthrough = R2ILBlock::new(0x3004, 4); + fallthrough.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + let mut taken = R2ILBlock::new(0x3008, 4); + taken.push(R2ILOp::Return { + target: Varnode::constant(1, 8), + }); + + let prepared = prepared_from_r2il_blocks(&[entry, fallthrough, taken], &arch) + .with_name("parsed_control_island"); + let mut ctx = make_x86_64_ctx_with_prepared(&prepared); + let region = crate::test_semantic_region( + 0x3000, + std::collections::BTreeSet::from([0x3008]), + vec![crate::test_control_fact( + 0x3008, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(r2sym::BackwardConditionSummary { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + r2sym::SemanticEvidence::exact(), + )], + Vec::new(), + ); + ctx.inputs.semantic_artifact = Some(crate::leaked_test_semantic_artifact( + crate::test_native_semantic_artifact( + r2sym::RefinementStage::Compiled, + r2sym::ArtifactGranularity::Regioned, + r2sym::SliceClass::Worker, + false, + Vec::new(), + vec![region], + ), + )); + + assert_eq!( + ctx.symbolic_actionable_compiled_condition_expr(0x3000), + Some(CExpr::binary( + BinaryOp::Eq, + CExpr::Var("x".to_string()), + CExpr::IntLit(0), + )) + ); +} + +#[test] +fn actionable_memory_island_parses_memory_condition_expr() { + let arch = make_test_arch_x86_64(); + let mut entry = R2ILBlock::new(0x4000, 4); + entry.push(R2ILOp::IntNotEqual { + dst: Varnode::unique(1, 1), + a: Varnode::register(0x10, 4), + b: Varnode::constant(0, 4), + }); + entry.push(R2ILOp::CBranch { + target: Varnode::constant(0x4008, 8), + cond: Varnode::unique(1, 1), + }); + let mut fallthrough = R2ILBlock::new(0x4004, 4); + fallthrough.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + let mut taken = R2ILBlock::new(0x4008, 4); + taken.push(R2ILOp::Return { + target: Varnode::constant(1, 8), + }); + + let prepared = + prepared_from_r2il_blocks(&[entry, fallthrough, taken], &arch).with_name("memory_island"); + let mut ctx = make_x86_64_ctx_with_prepared(&prepared); + let region = crate::test_semantic_region( + 0x4000, + std::collections::BTreeSet::new(), + Vec::new(), + vec![crate::test_memory_fact( + r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 8, + offset_hi: 8, + size: 4, + exact_offset: true, + evidence: r2sym::SemanticEvidence::exact(), + binding: None, + expr: "*(arg0 + 0x8)".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }, + r2sym::SemanticEvidence::exact(), + )], + ); + ctx.inputs.semantic_artifact = Some(crate::leaked_test_semantic_artifact( + crate::test_native_semantic_artifact( + r2sym::RefinementStage::Residual, + r2sym::ArtifactGranularity::Regioned, + r2sym::SliceClass::Worker, + true, + vec![r2sym::ResidualReason::LargeCfg], + vec![region], + ), + )); + + assert_eq!( + ctx.extract_condition_from_block(prepared.function().get_block(0x4000).expect("entry")), + Some(CExpr::binary( + BinaryOp::Eq, + CExpr::deref(CExpr::binary( + BinaryOp::Add, + CExpr::Var("arg0".to_string()), + CExpr::IntLit(0x8), + )), + CExpr::IntLit(0x2a), + )) + ); +} diff --git a/crates/r2dec/src/fold/tests/pipeline.rs b/crates/r2dec/src/fold/tests/pipeline.rs index 8c57280..59ad150 100644 --- a/crates/r2dec/src/fold/tests/pipeline.rs +++ b/crates/r2dec/src/fold/tests/pipeline.rs @@ -226,12 +226,14 @@ mod tests { external_stack_vars: empty_stack, visible_bindings: empty_visible, external_type_db: Box::leak(Box::new(r2types::ExternalTypeDb::default())), + semantic_artifact: None, param_register_aliases: empty_str, type_hints: empty_ty, type_oracle: None, function_return_type: None, prepared_ssa: None, interproc_summary_set: None, + summary_view: None, prepared_semantic_view: None, prepared_objects: None, prepared_memory: None, @@ -275,12 +277,14 @@ mod tests { external_stack_vars: empty_stack, visible_bindings: empty_visible, external_type_db: Box::leak(Box::new(r2types::ExternalTypeDb::default())), + semantic_artifact: None, param_register_aliases: empty_str, type_hints: empty_ty, type_oracle: None, function_return_type: None, prepared_ssa: None, interproc_summary_set: None, + summary_view: None, prepared_semantic_view: None, prepared_objects: None, prepared_memory: None, @@ -16231,4 +16235,93 @@ mod tests { )) ); } + + #[test] + fn prepared_predicate_candidate_for_branch_block_uses_block_assumptions_fallback() { + let arch = make_test_arch_x86_64(); + let mut entry = R2ILBlock::new(0x5000, 4); + entry.push(R2ILOp::IntEqual { + dst: Varnode::unique(1, 1), + a: Varnode::register(0x00, 8), + b: Varnode::register(0x18, 8), + }); + entry.push(R2ILOp::CBranch { + target: Varnode::constant(0x5008, 8), + cond: Varnode::unique(1, 1), + }); + let mut fallthrough = R2ILBlock::new(0x5004, 4); + fallthrough.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + let mut taken = R2ILBlock::new(0x5008, 4); + taken.push(R2ILOp::Return { + target: Varnode::constant(1, 8), + }); + + let prepared = prepared_from_r2il_blocks(&[entry, fallthrough, taken], &arch) + .with_name("branch_assumption"); + let mut ctx = make_x86_64_ctx_with_prepared(&prepared); + let cond = make_var("tmp:pred", 1, 1); + let lhs_value_id = prepared + .graph() + .values + .iter() + .find(|value| value.var.name.eq_ignore_ascii_case("rax") && value.var.version == 0) + .map(|value| value.id) + .expect("lhs value id"); + let rhs_value_id = prepared + .graph() + .values + .iter() + .find(|value| value.var.name.eq_ignore_ascii_case("rsi") && value.var.version == 0) + .map(|value| value.id) + .expect("rhs value id"); + let cond_value_id = prepared + .graph() + .values + .iter() + .find(|value| value.var.is_temp()) + .map(|value| value.id) + .expect("cond value id"); + let mut predicate_facts = r2ssa::PredicateFacts::default(); + predicate_facts.predicates.insert( + r2ssa::PredicateId(1), + r2ssa::PredicateFact { + id: r2ssa::PredicateId(1), + block_addr: 0x5000, + condition: lhs_value_id, + comparison: Some(r2ssa::CompareProvenance { + kind: r2ssa::CompareKind::Equal, + lhs: lhs_value_id, + rhs: rhs_value_id, + }), + true_target: 0x5008, + false_target: 0x5004, + }, + ); + predicate_facts.block_assumptions.insert( + 0x5000, + vec![r2ssa::BlockAssumption { + predecessor: 0x5000, + predicate: r2ssa::PredicateId(1), + truth: true, + }], + ); + ctx.inputs.prepared_predicates = Some(Box::leak(Box::new(predicate_facts))); + + assert_ne!( + lhs_value_id, cond_value_id, + "test setup must force the direct predicate match to miss" + ); + assert!( + matches!( + ctx.prepared_predicate_candidate_for_branch_block_for_test(0x5000, &cond), + Some(CExpr::Binary { + op: BinaryOp::Eq, + .. + }) + ), + "expected block_assumptions fallback to recover a compare expression" + ); + } } diff --git a/crates/r2dec/src/lib.rs b/crates/r2dec/src/lib.rs index 81065f7..767351e 100644 --- a/crates/r2dec/src/lib.rs +++ b/crates/r2dec/src/lib.rs @@ -32,8 +32,13 @@ pub(crate) mod address; pub(crate) mod analysis; pub mod ast; pub mod codegen; +pub(crate) mod consumer_fallback; +pub(crate) mod consumer_linear; +pub(crate) mod consumer_structured; +pub(crate) mod consumer_vm; pub mod fold; pub(crate) mod normalize; +pub(crate) mod planner; pub(crate) mod post_rename; pub mod region; pub(crate) mod registers; @@ -43,19 +48,23 @@ pub mod variable; pub use ast::{BinaryOp, CExpr, CFunction, CStmt, CType, UnaryOp}; pub use codegen::{CodeGenConfig, CodeGenerator, generate}; pub use fold::lower_ssa_ops_to_stmts; +pub use planner::SemanticRoutePlan; pub use region::{Region, RegionAnalyzer}; pub use structure::ControlFlowStructurer; pub use variable::VariableRecovery; use crate::fold::FoldingContext; use crate::fold::context::{FoldArchConfig, FoldInputs}; +use r2il::R2ILBlock; use r2ssa::SSAFunction; use r2ssa::SSAOp; use r2types::{ - CTypeLike, ExternalRegisterParamSpec, ExternalTypeDb, FunctionSignatureSpec, FunctionType, - FunctionTypeFacts, StackSlotKey, TypeInference, TypeOracle, VisibleBinding, VisibleBindingKind, + CTypeLike, ExternalRegisterParamSpec, ExternalTypeDb, FunctionFacts, FunctionSignatureSpec, + FunctionType, FunctionTypeFacts, StackSlotKey, TypeInference, TypeOracle, VisibleBinding, + VisibleBindingKind, }; use std::collections::HashSet; +use std::fmt::Write as _; fn is_generic_arg_name(name: &str) -> bool { let lower = name.trim().to_ascii_lowercase(); @@ -82,19 +91,23 @@ fn normalize_callee_name(name: &str) -> String { out } -fn should_skip_runtime_type_inference(prepared: Option<&r2ssa::SsaArtifact>) -> bool { - let Some(prepared) = prepared else { - return false; - }; - let summary = prepared.function().cfg_risk_summary(); - summary.block_count >= 96 - && summary.switch_block_count > 0 - && summary.max_switch_cases >= 32 - && summary.back_edge_count == 0 +fn prefer_symbolic_large_worker_decompile(function_facts: &FunctionFacts) -> bool { + planner::prefer_symbolic_large_worker_decompile(function_facts) +} + +fn should_skip_runtime_type_inference( + prepared: Option<&r2ssa::SsaArtifact>, + _type_facts: &FunctionTypeFacts, + function_facts: &FunctionFacts, +) -> bool { + planner::should_skip_runtime_type_inference(prepared, _type_facts, function_facts) } -fn should_use_prepared_semantic_view(prepared: Option<&r2ssa::SsaArtifact>) -> bool { - prepared.is_some() +fn should_use_prepared_semantic_view( + prepared: Option<&r2ssa::SsaArtifact>, + function_facts: &FunctionFacts, +) -> bool { + planner::should_use_prepared_semantic_view(prepared, function_facts) } fn seed_runtime_type_hints_from_facts_and_recovery( @@ -176,6 +189,275 @@ fn type_like_to_ctype(ty: &CTypeLike) -> CType { } } +fn format_vm_target_list(targets: &[u64]) -> String { + if targets.is_empty() { + return "[]".to_string(); + } + let rendered = targets + .iter() + .map(|target| format!("0x{:x}", target)) + .collect::>() + .join(", "); + format!("[{rendered}]") +} + +fn format_vm_value_expr(value: &r2sym::VmValueExpr) -> String { + match value { + r2sym::VmValueExpr::Const(value) => format!("0x{value:x}"), + r2sym::VmValueExpr::Var(name) | r2sym::VmValueExpr::Expr(name) => name.clone(), + r2sym::VmValueExpr::Unary { op, arg } => { + let op = match op { + r2sym::VmUnaryOp::Neg => "-", + r2sym::VmUnaryOp::BitNot => "~", + r2sym::VmUnaryOp::BoolNot => "!", + }; + format!("({}{})", op, format_vm_value_expr(arg)) + } + r2sym::VmValueExpr::Binary { op, lhs, rhs } => { + let op = match op { + r2sym::VmBinaryOp::Add => "+", + r2sym::VmBinaryOp::Sub => "-", + r2sym::VmBinaryOp::Mul => "*", + r2sym::VmBinaryOp::Div => "/", + r2sym::VmBinaryOp::Rem => "%", + r2sym::VmBinaryOp::And => "&", + r2sym::VmBinaryOp::Or => "|", + r2sym::VmBinaryOp::Xor => "^", + r2sym::VmBinaryOp::Shl => "<<", + r2sym::VmBinaryOp::LShr | r2sym::VmBinaryOp::AShr => ">>", + r2sym::VmBinaryOp::Eq => "==", + r2sym::VmBinaryOp::Ne => "!=", + r2sym::VmBinaryOp::Lt | r2sym::VmBinaryOp::SLt => "<", + r2sym::VmBinaryOp::Le | r2sym::VmBinaryOp::SLe => "<=", + r2sym::VmBinaryOp::BoolAnd => "&&", + r2sym::VmBinaryOp::BoolOr => "||", + }; + format!( + "({} {} {})", + format_vm_value_expr(lhs), + op, + format_vm_value_expr(rhs) + ) + } + } +} + +fn format_vm_state_updates(updates: &[r2sym::VmStateUpdate]) -> String { + if updates.is_empty() { + return "[]".to_string(); + } + let rendered = updates + .iter() + .map(|update| format!("{}={}", update.output, format_vm_value_expr(&update.value))) + .collect::>() + .join(", "); + format!("[{rendered}]") +} + +fn format_vm_guarded_exits(guards: &[r2sym::VmGuardedExit]) -> String { + if guards.is_empty() { + return "[]".to_string(); + } + let rendered = guards + .iter() + .map(|guard| format!("0x{:x}:{}", guard.target, guard.guard.expr)) + .collect::>() + .join(", "); + format!("[{rendered}]") +} + +fn format_vm_memory_conditions(conditions: &[r2sym::VmMemoryCondition]) -> String { + if conditions.is_empty() { + return "[]".to_string(); + } + let rendered = conditions + .iter() + .map(|condition| { + let region = condition.region.name.clone(); + let binding = condition + .binding + .as_deref() + .map(|binding| format!(" -> {binding}")) + .unwrap_or_default(); + let value = condition + .value_expr + .as_deref() + .map(|value| format!(" = {value}")) + .unwrap_or_default(); + format!( + "{}@[{:#x}..{:#x}]/{}:{}{}{}", + region, + condition.offset_lo, + condition.offset_hi, + condition.size, + condition.expr, + binding, + value, + ) + }) + .collect::>() + .join(", "); + format!("[{rendered}]") +} + +pub(crate) fn format_vm_summary_kind(kind: r2sym::InterpreterKind) -> &'static str { + match kind { + r2sym::InterpreterKind::SwitchDispatch => "switch_dispatch", + r2sym::InterpreterKind::IndirectDispatch => "indirect_dispatch", + } +} + +pub(crate) fn is_autogenerated_function_name(name: &str) -> bool { + let underscore_hex_addr = name + .strip_prefix('_') + .is_some_and(|rest| !rest.is_empty() && rest.chars().all(|ch| ch.is_ascii_hexdigit())); + name.is_empty() + || name.starts_with("fcn.") + || name.starts_with("fcn_") + || name.starts_with("sub.") + || name.starts_with("sub_") + || name.starts_with("loc.") + || underscore_hex_addr +} + +pub(crate) fn semantic_mode_label(artifact: &r2sym::SemanticArtifact) -> &'static str { + match (artifact.execution, artifact.stage, artifact.granularity) { + (r2sym::ExecutionModel::Vm, _, _) => "vm_summary", + (_, r2sym::RefinementStage::Raw, _) => "raw", + (_, r2sym::RefinementStage::Compiled, r2sym::ArtifactGranularity::Regioned) => { + "island_compiled" + } + (_, r2sym::RefinementStage::Compiled, _) => "compiled", + (_, r2sym::RefinementStage::Residual, _) => "residual", + } +} + +pub(crate) fn semantic_slice_class_label(slice_class: r2sym::SliceClass) -> &'static str { + match slice_class { + r2sym::SliceClass::Wrapper => "wrapper", + r2sym::SliceClass::Worker => "worker", + r2sym::SliceClass::RecursiveGroup => "recursive_group", + r2sym::SliceClass::InterpreterSwitch => "interpreter_switch", + r2sym::SliceClass::InterpreterIndirect => "interpreter_indirect", + r2sym::SliceClass::GenericLarge => "generic_large", + } +} + +pub(crate) fn semantic_residual_reason_label(reason: r2sym::ResidualReason) -> &'static str { + match reason { + r2sym::ResidualReason::MissingArch => "missing_arch", + r2sym::ResidualReason::LargeCfg => "large_cfg", + r2sym::ResidualReason::SummaryBudgetExhausted => "summary_budget_exhausted", + r2sym::ResidualReason::SccBudgetExhausted => "scc_budget_exhausted", + r2sym::ResidualReason::InterpreterRequiresStepSummary => { + "interpreter_requires_step_summary" + } + } +} + +pub fn semantic_fallback_comment( + func_name: &str, + semantic_artifact: Option<&r2sym::SemanticArtifact>, +) -> Option { + let function_facts = r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + semantic_artifact.cloned(), + ); + consumer_fallback::semantic_fallback_comment(func_name, &function_facts) +} + +pub fn preferred_semantic_fallback_comment( + func_name: &str, + semantic_artifact: Option<&r2sym::SemanticArtifact>, +) -> Option { + let function_facts = r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + semantic_artifact.cloned(), + ); + planner::preferred_semantic_fallback_comment(func_name, &function_facts) +} + +pub fn detached_semantic_linearization_reason( + func_name: &str, + blocks: &[R2ILBlock], + function_facts: &FunctionFacts, +) -> Option { + planner::detached_semantic_linearization_reason(func_name, blocks, function_facts) +} + +pub fn detached_semantic_route_plan( + func_name: &str, + blocks: &[R2ILBlock], + function_facts: &FunctionFacts, +) -> Option { + planner::detached_semantic_route_plan(func_name, blocks, function_facts) +} + +#[cfg_attr(not(test), allow(dead_code))] +fn preferred_semantic_linearization_reason( + func_name: &str, + semantic_artifact: Option<&r2sym::SemanticArtifact>, + cfg_summary: &r2ssa::CFGRiskSummary, +) -> Option { + let function_facts = r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + semantic_artifact.cloned(), + ); + planner::preferred_semantic_linearization_reason(func_name, &function_facts, cfg_summary) +} + +#[cfg_attr(not(test), allow(dead_code))] +fn preferred_semantic_structuring_reason( + func_name: &str, + semantic_artifact: Option<&r2sym::SemanticArtifact>, + cfg_summary: &r2ssa::CFGRiskSummary, +) -> Option { + let function_facts = r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + semantic_artifact.cloned(), + ); + planner::preferred_semantic_structuring_reason(func_name, &function_facts, cfg_summary) +} + +fn preferred_semantic_worker_reason(cfg_summary: &r2ssa::CFGRiskSummary) -> String { + planner::preferred_semantic_worker_reason(cfg_summary) +} + +pub fn cfg_guard_reason_from_summary(summary: &r2ssa::CFGRiskSummary) -> Option { + planner::cfg_guard_reason_from_summary(summary) +} + +pub fn cfg_guard_reason(blocks: &[R2ILBlock]) -> Option { + planner::cfg_guard_reason(blocks) +} + +pub fn block_guard_fallback_comment(func_name: &str, blocks: usize, max_blocks: usize) -> String { + planner::block_guard_fallback_comment(func_name, blocks, max_blocks) +} + +pub fn artifact_guard_fallback_comment(func_name: &str, reason: &str) -> String { + planner::artifact_guard_fallback_comment(func_name, reason) +} + +pub fn render_semantic_worker_linearization( + plan: &r2types::TypeWritebackPlan, + semantic_artifact: Option<&r2sym::SemanticArtifact>, + reason: &str, +) -> String { + consumer_linear::render_semantic_worker_linearization(plan, semantic_artifact, reason) +} + +pub fn render_vm_semantic_summary( + func_name: &str, + function_facts: &FunctionFacts, +) -> Option { + consumer_vm::render_vm_semantic_summary( + func_name, + &function_facts.types, + function_facts.semantics.as_ref()?, + ) +} + fn merge_params_with_external_signature( recovered_params: Vec, signature: Option<&FunctionSignatureSpec>, @@ -283,6 +565,18 @@ fn register_alias_names(reg_name: &str) -> Vec { vec![lower] } +pub fn normalize_sig_arch_name(arch: Option<&r2il::ArchSpec>) -> Option { + let arch = arch?; + let lower = arch.name.to_ascii_lowercase(); + if matches!(lower.as_str(), "x86-64" | "x86_64" | "x64" | "amd64") { + return Some("x86-64".to_string()); + } + if matches!(lower.as_str(), "x86" | "x86-32" | "i386" | "i686") { + return Some("x86".to_string()); + } + Some(arch.name.clone()) +} + fn build_param_register_aliases( params: &[ast::CParam], recovered_params: &[(r2ssa::SSAVar, ast::CParam)], @@ -372,6 +666,30 @@ impl Default for DecompilerConfig { } impl DecompilerConfig { + pub fn for_arch_name(arch_name: &str, ptr_bits: u32) -> Self { + match (arch_name, ptr_bits) { + ("x86", 32) | ("x86-32", _) => Self::x86(), + ("x86-64", _) | ("x86_64", _) | ("x64", _) | ("amd64", _) => Self::x86_64(), + ("arm", _) | ("ARM", _) if ptr_bits == 32 => Self::arm(), + ("aarch64", _) | ("arm64", _) | ("ARM64", _) => Self::aarch64(), + ("riscv32", _) | ("rv32", _) | ("rv32gc", _) => Self::riscv32(), + ("riscv64", _) | ("rv64", _) | ("rv64gc", _) => Self::riscv64(), + ("riscv", _) if ptr_bits == 32 => Self::riscv32(), + ("riscv", _) => Self::riscv64(), + _ => Self { + ptr_size: ptr_bits, + ..Self::default() + }, + } + } + + pub fn for_arch(arch: Option<&r2il::ArchSpec>) -> (String, u32, Self) { + let arch_name = normalize_sig_arch_name(arch).unwrap_or_else(|| "unknown".to_string()); + let ptr_bits = arch.map(|spec| spec.addr_size * 8).unwrap_or(64); + let config = Self::for_arch_name(&arch_name, ptr_bits); + (arch_name, ptr_bits, config) + } + /// Create a configuration for 32-bit x86. pub fn x86() -> Self { Self { @@ -495,11 +813,75 @@ pub struct DecompilerContext { pub strings: std::collections::HashMap, /// Symbol/global variable names. pub symbols: std::collections::HashMap, - /// Externally recovered type and layout facts. - pub type_facts: FunctionTypeFacts, + /// Canonical combined type and semantic facts. + pub function_facts: FunctionFacts, } impl DecompilerContext { + fn canonicalize_function_facts(mut function_facts: FunctionFacts) -> FunctionFacts { + function_facts.types = function_facts.types.canonicalized(); + function_facts.refresh_plans(); + function_facts + } + + pub fn type_facts(&self) -> &FunctionTypeFacts { + &self.function_facts.types + } + + pub fn type_facts_mut(&mut self) -> &mut FunctionTypeFacts { + &mut self.function_facts.types + } + + pub fn semantic_artifact(&self) -> Option<&r2sym::SemanticArtifact> { + self.function_facts.semantics.as_ref() + } + + pub fn from_analysis_inputs( + mut type_facts: FunctionTypeFacts, + function_names: std::collections::HashMap, + strings: std::collections::HashMap, + symbols: std::collections::HashMap, + ptr_bits: u32, + ) -> Self { + r2types::enrich_known_function_signatures_from_names( + &mut type_facts, + &function_names, + ptr_bits, + ); + r2types::enrich_known_function_signatures_from_names(&mut type_facts, &symbols, ptr_bits); + Self { + function_names, + strings, + symbols, + function_facts: FunctionFacts::new(type_facts.canonicalized(), None), + } + } + + pub fn from_function_facts( + mut function_facts: FunctionFacts, + function_names: std::collections::HashMap, + strings: std::collections::HashMap, + symbols: std::collections::HashMap, + ptr_bits: u32, + ) -> Self { + r2types::enrich_known_function_signatures_from_names( + &mut function_facts.types, + &function_names, + ptr_bits, + ); + r2types::enrich_known_function_signatures_from_names( + &mut function_facts.types, + &symbols, + ptr_bits, + ); + Self { + function_names, + strings, + symbols, + function_facts: Self::canonicalize_function_facts(function_facts), + } + } + pub fn with_function_names( mut self, function_names: std::collections::HashMap, @@ -519,7 +901,21 @@ impl DecompilerContext { } pub fn with_type_facts(mut self, type_facts: FunctionTypeFacts) -> Self { - self.type_facts = type_facts.canonicalized(); + self.function_facts.types = type_facts.canonicalized(); + self.function_facts.refresh_plans(); + self + } + + pub fn with_semantic_artifact( + mut self, + semantic_artifact: Option, + ) -> Self { + self.function_facts.set_semantics(semantic_artifact); + self + } + + pub fn with_function_facts(mut self, function_facts: FunctionFacts) -> Self { + self.function_facts = Self::canonicalize_function_facts(function_facts); self } } @@ -533,16 +929,20 @@ pub struct DecompilerInput { impl DecompilerInput { pub fn new(prepared_ssa: r2ssa::SsaArtifact, mut context: DecompilerContext) -> Self { - context.type_facts = context.type_facts.canonicalized(); + context.function_facts = + DecompilerContext::canonicalize_function_facts(context.function_facts); + let interproc_summary_set = context.function_facts.interproc_summary_set().cloned(); Self { prepared_ssa, - interproc_summary_set: None, + interproc_summary_set, context, } } pub fn with_context(mut self, mut context: DecompilerContext) -> Self { - context.type_facts = context.type_facts.canonicalized(); + context.function_facts = + DecompilerContext::canonicalize_function_facts(context.function_facts); + self.interproc_summary_set = context.function_facts.interproc_summary_set().cloned(); self.context = context; self } @@ -551,6 +951,8 @@ impl DecompilerInput { mut self, interproc_summary_set: Option, ) -> Self { + self.context.function_facts.summary_view = + r2types::InterprocSummaryView::new(interproc_summary_set.clone()); self.interproc_summary_set = interproc_summary_set; self } @@ -573,7 +975,8 @@ impl Decompiler { /// Set external context (function names, strings, symbols). pub fn with_context(mut self, mut context: DecompilerContext) -> Self { - context.type_facts = context.type_facts.canonicalized(); + context.function_facts = + DecompilerContext::canonicalize_function_facts(context.function_facts); self.context = context; self } @@ -600,7 +1003,7 @@ impl Decompiler { ) where T: Into, { - self.context.type_facts.known_function_signatures = signatures + self.context.type_facts_mut().known_function_signatures = signatures .into_iter() .map(|(name, sig)| (name, sig.into())) .collect(); @@ -608,16 +1011,50 @@ impl Decompiler { /// Set externally recovered host type database. pub fn set_external_type_db(&mut self, external_type_db: ExternalTypeDb) { - self.context.type_facts.external_type_db = external_type_db; + self.context.type_facts_mut().external_type_db = external_type_db; } /// Set externally recovered type facts. pub fn set_type_facts(&mut self, type_facts: FunctionTypeFacts) { - self.context.type_facts = type_facts.canonicalized(); + self.context.function_facts.types = type_facts.canonicalized(); + self.context.function_facts.refresh_plans(); + } + + pub fn set_function_facts(&mut self, function_facts: FunctionFacts) { + self.context = self.context.clone().with_function_facts(function_facts); + } + + fn vm_summary_output_for_route( + &self, + func_name: &str, + route: &planner::SemanticRoutePlan, + ) -> Option { + match route { + planner::SemanticRoutePlan::VmSummary { .. } => { + crate::consumer_vm::render_vm_semantic_summary( + func_name, + self.context.type_facts(), + self.context.semantic_artifact()?, + ) + } + _ => None, + } } /// Decompile an SSA function to C code. pub fn decompile(&self, func: &SSAFunction) -> String { + let func_name = func + .name + .clone() + .unwrap_or_else(|| format!("sub_{:x}", func.entry)); + let semantic_route = planner::semantic_route_plan( + &func_name, + &self.context.function_facts, + &func.cfg_risk_summary(), + ); + if let Some(output) = self.vm_summary_output_for_route(&func_name, &semantic_route) { + return output; + } // Build the C function let c_func = self.build_function(func); @@ -628,6 +1065,19 @@ impl Decompiler { /// Decompile a prepared function with an explicit typed context payload. pub fn decompile_input(&self, input: &DecompilerInput) -> String { + let func = input.prepared_ssa.function(); + let func_name = func + .name + .clone() + .unwrap_or_else(|| format!("sub_{:x}", func.entry)); + let semantic_route = planner::semantic_route_plan( + &func_name, + &input.context.function_facts, + &func.cfg_risk_summary(), + ); + if let Some(output) = self.vm_summary_output_for_route(&func_name, &semantic_route) { + return output; + } let c_func = self.build_function_from_input(input); let mut codegen = CodeGenerator::new(self.config.codegen.clone()); codegen.generate_function(&c_func) @@ -643,7 +1093,7 @@ impl Decompiler { ) } - fn stmt_has_content(stmt: &CStmt) -> bool { + pub(crate) fn stmt_has_content(stmt: &CStmt) -> bool { match stmt { CStmt::Empty => false, CStmt::Block(stmts) => !stmts.is_empty(), @@ -651,7 +1101,7 @@ impl Decompiler { } } - fn prepend_comment(stmt: CStmt, text: String) -> CStmt { + pub(crate) fn prepend_comment(stmt: CStmt, text: String) -> CStmt { let comment = CStmt::comment(text); match stmt { CStmt::Empty => CStmt::Block(vec![comment]), @@ -663,6 +1113,229 @@ impl Decompiler { } } + fn semantic_vm_summary_comment(&self) -> Option { + let vm_body = self.context.semantic_artifact()?.vm_body()?; + let vm_step = vm_body + .step_summary + .as_ref() + .or(vm_body.transfer_summary.as_ref())?; + let exact_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.evidence().allows_hard_proof()) + .count(); + let likely_transfers = vm_step + .transfers + .iter() + .filter(|transfer| matches!(transfer.confidence(), r2sym::SemanticConfidence::Likely)) + .count(); + let heuristic_transfers = vm_step + .transfers + .iter() + .filter(|transfer| { + matches!(transfer.confidence(), r2sym::SemanticConfidence::Heuristic) + }) + .count(); + let redispatch_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.redispatch) + .count(); + let returning_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.may_return) + .count(); + let selector_updates = vm_step + .transfers + .iter() + .filter(|transfer| transfer.selector_update.is_some()) + .count(); + let exact_exit_guards = vm_step + .transfers + .iter() + .flat_map(|transfer| transfer.exit_guards.iter()) + .filter(|guard| guard.guard.evidence().allows_hard_proof()) + .count(); + let total_read_effects: usize = vm_step + .handler_memory_read_effects + .values() + .map(Vec::len) + .sum(); + let total_write_effects: usize = vm_step + .handler_memory_write_effects + .values() + .map(Vec::len) + .sum(); + let total_reads: usize = vm_step.handler_memory_reads.values().copied().sum(); + let total_writes: usize = vm_step.handler_memory_writes.values().copied().sum(); + + let mut out = String::new(); + let _ = writeln!(&mut out, "r2dec semantic summary: vm_summary"); + let _ = writeln!( + &mut out, + "kind={} dispatch_header=0x{:x} loop_header=0x{:x} selector={} targets={} default_target={} latches={} step_blocks={} transfers={} exact_transfers={} likely_transfers={} heuristic_transfers={} redispatch_transfers={} returning_transfers={} selector_updates={} exact_exit_guards={} total_reads={} total_writes={} total_read_effects={} total_write_effects={}", + format_vm_summary_kind(vm_step.kind), + vm_step.dispatch_header, + vm_step.loop_header, + vm_step.selector.as_deref().unwrap_or(""), + vm_step.dispatch_targets.len(), + vm_step + .default_target + .map(|target| format!("0x{:x}", target)) + .unwrap_or_else(|| "none".to_string()), + vm_step.loop_latches.len(), + vm_step.step_blocks.len(), + vm_step.transfers.len(), + exact_transfers, + likely_transfers, + heuristic_transfers, + redispatch_transfers, + returning_transfers, + selector_updates, + exact_exit_guards, + total_reads, + total_writes, + total_read_effects, + total_write_effects, + ); + if !vm_step.state_inputs.is_empty() || !vm_step.state_outputs.is_empty() { + let _ = writeln!( + &mut out, + "state_inputs=[{}] state_outputs=[{}]", + vm_step.state_inputs.join(", "), + vm_step.state_outputs.join(", "), + ); + } + + let transfer_preview = vm_step.transfers.iter().take(4).collect::>(); + if !transfer_preview.is_empty() { + for transfer in transfer_preview { + let selector_update = transfer + .selector_update + .as_ref() + .map(|update| format!("{}={}", update.output, update.expr)) + .unwrap_or_else(|| "none".to_string()); + let _ = writeln!( + &mut out, + "transfer handler=0x{:x} cases={} blocks={} exits={} exit_guards={} updates={} selector_update={} reads={} writes={} exact={} confidence={:?} reasons={} residual_guards={} residual_memory={} redispatch={} return={} truncated={}", + transfer.handler_target, + format_vm_target_list(&transfer.case_values), + format_vm_target_list(&transfer.region_blocks), + format_vm_target_list(&transfer.exit_targets), + format_vm_guarded_exits(&transfer.exit_guards), + format_vm_state_updates(&transfer.state_updates), + selector_update, + format_vm_memory_conditions(&transfer.memory_reads), + format_vm_memory_conditions(&transfer.memory_writes), + transfer.evidence().allows_hard_proof(), + transfer.confidence(), + format_args!("{:?}", transfer.evidence().reasons), + transfer.residual_guards, + transfer.residual_memory_effects, + transfer.redispatch, + transfer.may_return, + transfer.truncated, + ); + } + } + + let mut preview_handlers = vm_step.handler_regions.keys().copied().collect::>(); + preview_handlers.sort_unstable(); + for handler in preview_handlers.into_iter().take(4) { + let regions = vm_step + .handler_regions + .get(&handler) + .map(|regions| format_vm_target_list(regions)) + .unwrap_or_else(|| "[]".to_string()); + let cases = vm_step + .case_values_by_target + .get(&handler) + .map(|values| format_vm_target_list(values)) + .unwrap_or_else(|| "[]".to_string()); + let updates = vm_step + .handler_state_updates + .get(&handler) + .map(|updates| format_vm_state_updates(updates)) + .unwrap_or_else(|| "[]".to_string()); + let inputs = vm_step + .handler_state_inputs + .get(&handler) + .map(|values| format!("[{}]", values.join(", "))) + .unwrap_or_else(|| "[]".to_string()); + let outputs = vm_step + .handler_state_outputs + .get(&handler) + .map(|values| format!("[{}]", values.join(", "))) + .unwrap_or_else(|| "[]".to_string()); + let exits = vm_step + .handler_exit_targets + .get(&handler) + .map(|values| format_vm_target_list(values)) + .unwrap_or_else(|| "[]".to_string()); + let guards = vm_step + .handler_exit_guards + .get(&handler) + .map(|values| format_vm_guarded_exits(values)) + .unwrap_or_else(|| "[]".to_string()); + let read_effects = vm_step + .handler_memory_read_effects + .get(&handler) + .map(|values| format_vm_memory_conditions(values)) + .unwrap_or_else(|| "[]".to_string()); + let write_effects = vm_step + .handler_memory_write_effects + .get(&handler) + .map(|values| format_vm_memory_conditions(values)) + .unwrap_or_else(|| "[]".to_string()); + let _ = writeln!( + &mut out, + "handler 0x{:x}: regions={} cases={} inputs={} outputs={} updates={} reads={} writes={} read_effects={} write_effects={} calls={} branches={} exits={} guards={}", + handler, + regions, + cases, + inputs, + outputs, + updates, + vm_step + .handler_memory_reads + .get(&handler) + .copied() + .unwrap_or(0), + vm_step + .handler_memory_writes + .get(&handler) + .copied() + .unwrap_or(0), + read_effects, + write_effects, + vm_step.handler_calls.get(&handler).copied().unwrap_or(0), + vm_step + .handler_conditional_branches + .get(&handler) + .copied() + .unwrap_or(0), + exits, + guards, + ); + } + + if !vm_step.redispatch_handlers.is_empty() + || !vm_step.returning_handlers.is_empty() + || !vm_step.truncated_handlers.is_empty() + { + let _ = writeln!( + &mut out, + "redispatch_handlers={} returning_handlers={} truncated_handlers={}", + format_vm_target_list(&vm_step.redispatch_handlers), + format_vm_target_list(&vm_step.returning_handlers), + format_vm_target_list(&vm_step.truncated_handlers), + ); + } + + Some(out.trim_end().to_string()) + } + fn linearize_function_body( &self, func: &SSAFunction, @@ -705,10 +1378,14 @@ impl Decompiler { self.config.arg_regs.clone(), self.config.ret_regs.clone(), ); - var_recovery.set_type_facts(self.context.type_facts.clone()); + var_recovery.set_type_facts(self.context.type_facts().clone()); var_recovery.recover(func); - let skip_runtime_type_inference = should_skip_runtime_type_inference(prepared); + let skip_runtime_type_inference = should_skip_runtime_type_inference( + prepared, + self.context.type_facts(), + &self.context.function_facts, + ); let type_inference = (!skip_runtime_type_inference).then(|| { let mut type_inference = TypeInference::new_with_abi( self.config.ptr_size, @@ -718,17 +1395,23 @@ impl Decompiler { if !self.context.function_names.is_empty() { type_inference.set_function_names(self.context.function_names.clone()); } - type_inference.set_external_signature(self.context.type_facts.merged_signature.clone()); - for (name, signature) in &self.context.type_facts.known_function_signatures { + type_inference + .set_external_signature(self.context.type_facts().merged_signature.clone()); + for (name, signature) in &self.context.type_facts().known_function_signatures { type_inference.add_function_type(name, signature.clone()); } - type_inference.set_external_stack_slots(self.context.type_facts.stack_slots.clone()); - if !self.context.type_facts.external_type_db.structs.is_empty() - || !self.context.type_facts.external_type_db.unions.is_empty() - || !self.context.type_facts.external_type_db.enums.is_empty() + type_inference.set_external_stack_slots(self.context.type_facts().stack_slots.clone()); + if !self + .context + .type_facts() + .external_type_db + .structs + .is_empty() + || !self.context.type_facts().external_type_db.unions.is_empty() + || !self.context.type_facts().external_type_db.enums.is_empty() { type_inference - .set_external_type_db(self.context.type_facts.external_type_db.clone()); + .set_external_type_db(self.context.type_facts().external_type_db.clone()); } if let Some(prepared) = prepared { type_inference.set_prepared_ssa(prepared); @@ -745,7 +1428,10 @@ impl Decompiler { .map(|(name, ty)| (name, type_like_to_ctype(&ty))) .collect::>() } else { - seed_runtime_type_hints_from_facts_and_recovery(&self.context.type_facts, &var_recovery) + seed_runtime_type_hints_from_facts_and_recovery( + self.context.type_facts(), + &var_recovery, + ) }; let combined_type_oracle = type_inference .as_ref() @@ -756,7 +1442,7 @@ impl Decompiler { let known_function_signatures = self .context - .type_facts + .type_facts() .known_function_signatures .iter() .map(|(name, ty)| (normalize_callee_name(name), ty.clone())) @@ -785,12 +1471,12 @@ impl Decompiler { .iter() .map(|(_, param)| param.clone()) .collect(), - self.context.type_facts.merged_signature.as_ref(), + self.context.type_facts().merged_signature.as_ref(), ); let param_register_aliases = build_param_register_aliases( ¶ms, &recovered_param_infos, - &self.context.type_facts.register_params, + &self.context.type_facts().register_params, &self.config.arg_regs, ); for (idx, (_ssa_var, _)) in recovered_param_infos.iter().enumerate() { @@ -817,7 +1503,7 @@ impl Decompiler { .map(|type_inference| self.infer_return_type(func, type_inference)) .or_else(|| { self.context - .type_facts + .type_facts() .merged_signature .as_ref() .and_then(|sig| sig.ret_type.as_ref().map(type_like_to_ctype)) @@ -825,11 +1511,10 @@ impl Decompiler { .unwrap_or(CType::Unknown); let signature_ret_type = self .context - .type_facts + .type_facts() .merged_signature .as_ref() .and_then(|sig| sig.ret_type.as_ref().map(type_like_to_ctype)); - let fold_arch = FoldArchConfig { ptr_size: self.config.ptr_size, sp_name: self.config.sp_name.clone(), @@ -843,38 +1528,41 @@ impl Decompiler { arg_regs: self.config.arg_regs.clone(), caller_saved_regs: self.config.caller_saved_regs.clone(), }; - let prepared_semantic_view = should_use_prepared_semantic_view(prepared).then(|| { - analysis::PreparedSemanticView::build(analysis::PreparedSemanticViewInputs { - prepared: prepared.expect("prepared semantic view requires prepared artifact"), - interproc_summary_set, - abi_arg_regs: &self.config.arg_regs, - ret_reg_name: &fold_arch.ret_reg_name, - function_names: &self.context.function_names, - symbols: &self.context.symbols, - callee_facts: &self.context.type_facts.callee_facts, - stack_slots: &self.context.type_facts.stack_slots, - visible_bindings: &self.context.type_facts.visible_bindings, - param_register_aliases: ¶m_register_aliases, - }) - }); + let prepared_semantic_view = + should_use_prepared_semantic_view(prepared, &self.context.function_facts).then(|| { + analysis::PreparedSemanticView::build(analysis::PreparedSemanticViewInputs { + prepared: prepared.expect("prepared semantic view requires prepared artifact"), + interproc_summary_set, + abi_arg_regs: &self.config.arg_regs, + ret_reg_name: &fold_arch.ret_reg_name, + function_names: &self.context.function_names, + symbols: &self.context.symbols, + callee_facts: &self.context.type_facts().callee_facts, + stack_slots: &self.context.type_facts().stack_slots, + visible_bindings: &self.context.type_facts().visible_bindings, + param_register_aliases: ¶m_register_aliases, + }) + }); let fold_inputs = FoldInputs { arch: &fold_arch, function_names: &self.context.function_names, strings: &self.context.strings, symbols: &self.context.symbols, known_function_signatures: &known_function_signatures, - callee_facts: &self.context.type_facts.callee_facts, - stack_slots: &self.context.type_facts.stack_slots, + callee_facts: &self.context.type_facts().callee_facts, + stack_slots: &self.context.type_facts().stack_slots, #[cfg(test)] - external_stack_vars: &self.context.type_facts.external_stack_vars, - visible_bindings: &self.context.type_facts.visible_bindings, - external_type_db: &self.context.type_facts.external_type_db, + external_stack_vars: &self.context.type_facts().external_stack_vars, + visible_bindings: &self.context.type_facts().visible_bindings, + external_type_db: &self.context.type_facts().external_type_db, + semantic_artifact: self.context.semantic_artifact(), param_register_aliases: ¶m_register_aliases, type_hints: &type_hints, type_oracle, function_return_type: signature_ret_type.as_ref().or(Some(&inferred_ret_type)), prepared_ssa: prepared, interproc_summary_set, + summary_view: Some(&self.context.function_facts.summary_view), prepared_semantic_view: prepared_semantic_view.as_ref(), prepared_objects: prepared.map(|artifact| artifact.objects()), prepared_memory: prepared.map(|artifact| artifact.memory()), @@ -885,73 +1573,59 @@ impl Decompiler { let fold_blocks: Vec<_> = func.blocks().cloned().collect(); fold_ctx.analyze_blocks(&fold_blocks); fold_ctx.analyze_function_structure(func); + let func_name = func + .name + .clone() + .unwrap_or_else(|| format!("sub_{:x}", func.entry)); + let semantic_route = planner::semantic_route_plan( + &func_name, + &self.context.function_facts, + &func.cfg_risk_summary(), + ); // Structure control flow (primary path: folded) let mut structurer = ControlFlowStructurer::new(func, &fold_ctx); // Get set of variables that survive folding before structuring. let emitted_vars = structurer.emitted_var_names(); - let mut use_conservative_locals = false; - let mut is_linear_fallback = false; - - let folded_stmt = structurer.structure(); - let mut body_stmt = folded_stmt; + let routed_body = consumer_structured::primary_body_for_semantic_route( + &semantic_route, + &mut structurer, + || self.linearize_function_body(func, &fold_ctx), + ); + let mut use_conservative_locals = routed_body.use_conservative_locals; + let mut is_linear_fallback = routed_body.is_linear_fallback; + let mut body_stmt = routed_body.body_stmt; - if !Self::stmt_has_content(&body_stmt) { + if matches!(semantic_route, planner::SemanticRoutePlan::Standard) + && !Self::stmt_has_content(&body_stmt) + { let folded_reason = structurer .safety_reason() .map(str::to_string) .unwrap_or_else(|| "folded structuring produced empty output".to_string()); - - // Fallback 1: unfolded structuring - let mut unfolded = ControlFlowStructurer::new_unfolded(func, &fold_ctx); - let unfolded_stmt = unfolded.structure(); - - if Self::stmt_has_content(&unfolded_stmt) { - use_conservative_locals = true; - body_stmt = Self::prepend_comment( - unfolded_stmt, - format!("r2dec fallback: {}", folded_reason), - ); - } else { - let unfolded_reason = unfolded - .safety_reason() - .map(str::to_string) - .unwrap_or_else(|| "unfolded structuring produced empty output".to_string()); - - // Fallback 2: linear block emission - let mut linear_stmts = self.linearize_function_body(func, &fold_ctx); - let fallback_reason = format!("{}; {}", folded_reason, unfolded_reason); - - use_conservative_locals = true; - is_linear_fallback = true; - if linear_stmts.is_empty() { - body_stmt = CStmt::Block(vec![CStmt::comment(format!( - "r2dec fallback: {} -> no statements recovered", - fallback_reason - ))]); - } else { - linear_stmts.insert( - 0, - CStmt::comment(format!( - "r2dec fallback: {} -> linear block emission", - fallback_reason - )), - ); - body_stmt = CStmt::Block(linear_stmts); - } - } + let empty_fallback = consumer_fallback::recover_empty_structuring( + func, + &fold_ctx, + folded_reason, + prefer_symbolic_large_worker_decompile(&self.context.function_facts) + .then(|| preferred_semantic_worker_reason(&func.cfg_risk_summary())) + .as_deref(), + || self.linearize_function_body(func, &fold_ctx), + ); + use_conservative_locals = empty_fallback.use_conservative_locals; + is_linear_fallback = empty_fallback.is_linear_fallback; + body_stmt = empty_fallback.body_stmt; } body_stmt = fold_ctx.normalize_final_stmt_calls(body_stmt); body_stmt = fold_ctx.prune_dead_temp_assignments_in_stmt(body_stmt); - // Build the C function - let func_name = func - .name - .clone() - .unwrap_or_else(|| format!("sub_{:x}", func.entry)); + if let Some(comment) = self.semantic_vm_summary_comment() { + body_stmt = Self::prepend_comment(body_stmt, comment); + } + // Build the C function // Convert body to statements let body = self.stmt_to_vec(body_stmt); let body_visible_names = collect_stmt_var_names(&body); @@ -969,7 +1643,7 @@ impl Decompiler { }) .chain( self.context - .type_facts + .type_facts() .visible_bindings .iter() .filter_map(|binding| { @@ -981,8 +1655,8 @@ impl Decompiler { .collect::>(); let body_visible_stack_offsets = collect_visible_stack_offsets( &body_visible_names, - &self.context.type_facts.visible_bindings, - &self.context.type_facts.stack_slots, + &self.context.type_facts().visible_bindings, + &self.context.type_facts().stack_slots, ¶m_name_set, ); @@ -1041,7 +1715,7 @@ impl Decompiler { name: func_name, ret_type: self .context - .type_facts + .type_facts() .merged_signature .as_ref() .and_then(|sig| sig.ret_type.as_ref().map(type_like_to_ctype)) @@ -1058,7 +1732,7 @@ impl Decompiler { for name in self.context.function_names.values() { known_function_names.insert(name.to_ascii_lowercase()); } - for name in self.context.type_facts.known_function_signatures.keys() { + for name in self.context.type_facts().known_function_signatures.keys() { known_function_names.insert(name.to_ascii_lowercase()); } post_rename::rewrite_function_identifiers(&mut c_function, &known_function_names); @@ -1920,14 +2594,123 @@ pub fn infer_local_struct_field_accesses( .collect() } +#[cfg(test)] +pub(crate) fn test_control_fact( + target: u64, + status: r2sym::SymbolicReachabilityStatus, + branch_truth: Option, + condition: Option<&str>, + compiled: Option, + evidence: r2sym::SemanticEvidence, +) -> r2sym::Judged { + r2sym::Judged::new( + r2sym::ControlFact { + target, + status, + branch_truth, + condition: condition.map(str::to_string), + compiled, + }, + evidence, + ) +} + +#[cfg(test)] +pub(crate) fn test_memory_fact( + term: r2sym::BackwardMemoryCondition, + evidence: r2sym::SemanticEvidence, +) -> r2sym::Judged { + r2sym::Judged::new(r2sym::MemoryFact { term }, evidence) +} + +#[cfg(test)] +pub(crate) fn test_semantic_region( + anchor: u64, + frontier: std::collections::BTreeSet, + control: Vec>, + memory: Vec>, +) -> r2sym::SemanticRegion { + let targets = control + .iter() + .map(|fact| { + r2sym::Judged::new( + r2sym::TargetFact { + target: fact.value.target, + status: fact.value.status, + branch_truth: fact.value.branch_truth, + }, + fact.evidence.clone(), + ) + }) + .collect(); + r2sym::SemanticRegion { + anchor, + frontier, + control, + memory, + pre: Vec::new(), + post: Vec::new(), + targets, + } +} + +#[cfg(test)] +pub(crate) fn test_native_semantic_artifact( + stage: r2sym::RefinementStage, + granularity: r2sym::ArtifactGranularity, + slice_class: r2sym::SliceClass, + skipped_large_cfg: bool, + residual_reasons: Vec, + regions: Vec, +) -> r2sym::SemanticArtifact { + let regions = regions + .into_iter() + .map(|region| (region.key(), region)) + .collect(); + r2sym::SemanticArtifact { + stage, + granularity, + execution: r2sym::ExecutionModel::Native, + body: r2sym::SemanticArtifactBody::Native(r2sym::NativeArtifactBody { + summary: r2sym::NativeFunctionSummary { + slice_class, + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: Default::default(), + }, + regions, + }), + diagnostics: r2sym::SemanticArtifactDiagnostics { + branches_evaluated: 0, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg, + residual_reasons, + ambiguous_targets: Vec::new(), + cache_hit: false, + }, + } +} + +#[cfg(test)] +pub(crate) fn leaked_test_semantic_artifact( + artifact: r2sym::SemanticArtifact, +) -> &'static r2sym::SemanticArtifact { + Box::leak(Box::new(artifact)) +} + #[cfg(test)] mod tests { use super::*; use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; use r2ssa::SSAFunction; use r2types::{ - ExternalField, ExternalStruct, FunctionParamSpec, FunctionSignatureSpec, FunctionTypeFacts, + ExternalField, ExternalStruct, FunctionFacts, FunctionParamSpec, FunctionSignatureSpec, + FunctionTypeFacts, }; + use std::collections::{BTreeMap, BTreeSet}; fn ssa_from_ops(ops: Vec, arch: &ArchSpec) -> SSAFunction { let mut block = R2ILBlock::new(0x1000, 4); @@ -1975,6 +2758,66 @@ mod tests { } } + fn arg_memory_term( + offset: i64, + size: u32, + evidence: r2sym::SemanticEvidence, + value_expr: Option<&str>, + exact_value: bool, + ) -> r2sym::BackwardMemoryCondition { + r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: offset, + offset_hi: offset, + size, + exact_offset: true, + evidence, + binding: None, + expr: if offset == 0 { + "*arg0".to_string() + } else { + format!("*(arg0 + {offset})") + }, + value_expr: value_expr.map(str::to_string), + exact_value, + } + } + + fn compiled_summary( + simplified: &str, + precision: r2sym::BackwardConditionPrecision, + supported_paths: usize, + total_paths: usize, + memory_terms: Vec, + ) -> r2sym::BackwardConditionSummary { + r2sym::BackwardConditionSummary { + simplified: simplified.to_string(), + terms: vec![simplified.to_string()], + memory_terms, + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision, + supported_paths, + total_paths, + } + } + + fn large_cfg_worker_artifact( + stage: r2sym::RefinementStage, + residual_reasons: Vec, + regions: Vec, + ) -> r2sym::SemanticArtifact { + test_native_semantic_artifact( + stage, + r2sym::ArtifactGranularity::Regioned, + r2sym::SliceClass::Worker, + true, + residual_reasons, + regions, + ) + } + #[test] fn test_decompiler_config_default() { let config = DecompilerConfig::default(); @@ -2366,7 +3209,7 @@ mod tests { let context = DecompilerContext::default().with_type_facts(type_facts); let mut legacy = Decompiler::new(DecompilerConfig::x86_64()); - legacy.set_type_facts(context.type_facts.clone()); + legacy.set_type_facts(context.type_facts().clone()); let input = DecompilerInput::new(prepared, context); let typed = Decompiler::new(DecompilerConfig::x86_64()); @@ -3541,7 +4384,7 @@ mod tests { config.arg_regs.clone(), config.ret_regs.clone(), ); - var_recovery.set_type_facts(decompiler.context.type_facts.clone()); + var_recovery.set_type_facts(decompiler.context.type_facts().clone()); var_recovery.recover(func); let mut type_inference = TypeInference::new_with_abi( @@ -3550,9 +4393,11 @@ mod tests { config.ret_regs.clone(), ); type_inference - .set_external_signature(decompiler.context.type_facts.merged_signature.clone()); - type_inference.set_external_stack_slots(decompiler.context.type_facts.stack_slots.clone()); - type_inference.set_external_type_db(decompiler.context.type_facts.external_type_db.clone()); + .set_external_signature(decompiler.context.type_facts().merged_signature.clone()); + type_inference + .set_external_stack_slots(decompiler.context.type_facts().stack_slots.clone()); + type_inference + .set_external_type_db(decompiler.context.type_facts().external_type_db.clone()); type_inference.set_decompile_prep_facts(func.decompile_prep_facts()); type_inference.infer_function(func); let mut type_hints = type_inference @@ -3578,12 +4423,12 @@ mod tests { .iter() .map(|(_, param)| param.clone()) .collect(), - decompiler.context.type_facts.merged_signature.as_ref(), + decompiler.context.type_facts().merged_signature.as_ref(), ); let param_register_aliases = build_param_register_aliases( ¶ms, &recovered_param_infos, - &decompiler.context.type_facts.register_params, + &decompiler.context.type_facts().register_params, &config.arg_regs, ); for (idx, (_ssa_var, _)) in recovered_param_infos.iter().enumerate() { @@ -3623,7 +4468,7 @@ mod tests { let inferred_ret_type = decompiler.infer_return_type(func, &type_inference); let signature_ret_type = decompiler .context - .type_facts + .type_facts() .merged_signature .as_ref() .and_then(|sig| sig.ret_type.as_ref().map(type_like_to_ctype)); @@ -3633,12 +4478,13 @@ mod tests { strings: &decompiler.context.strings, symbols: &decompiler.context.symbols, known_function_signatures: &known_function_signatures, - callee_facts: &decompiler.context.type_facts.callee_facts, - stack_slots: &decompiler.context.type_facts.stack_slots, + callee_facts: &decompiler.context.type_facts().callee_facts, + stack_slots: &decompiler.context.type_facts().stack_slots, #[cfg(test)] - external_stack_vars: &decompiler.context.type_facts.external_stack_vars, - visible_bindings: &decompiler.context.type_facts.visible_bindings, - external_type_db: &decompiler.context.type_facts.external_type_db, + external_stack_vars: &decompiler.context.type_facts().external_stack_vars, + visible_bindings: &decompiler.context.type_facts().visible_bindings, + external_type_db: &decompiler.context.type_facts().external_type_db, + semantic_artifact: decompiler.context.semantic_artifact(), param_register_aliases: ¶m_register_aliases, type_hints: &type_hints, type_oracle: combined_type_oracle @@ -3647,6 +4493,7 @@ mod tests { function_return_type: signature_ret_type.as_ref().or(Some(&inferred_ret_type)), prepared_ssa: None, interproc_summary_set: None, + summary_view: Some(&decompiler.context.function_facts.summary_view), prepared_semantic_view: None, prepared_objects: None, prepared_memory: None, @@ -4003,4 +4850,715 @@ mod tests { "observed arm64 struct-array return path should stay semantic, got:\n{output}" ); } + + #[test] + fn decompiler_prepends_vm_semantic_summary_comment() { + let func = ssa_from_ops( + vec![R2ILOp::Return { + target: Varnode::constant(0, 8), + }], + &test_arch_for_decompile(), + ); + + let mut decompiler = Decompiler::new(DecompilerConfig::default()); + let vm_step = r2sym::VmStepSummary { + kind: r2sym::InterpreterKind::SwitchDispatch, + loop_header: 0x1000, + dispatch_header: 0x1000, + selector: Some("vm.sel".to_string()), + dispatch_targets: vec![0x1004, 0x1008], + default_target: Some(0x1010), + case_values_by_target: BTreeMap::from([(0x1004, vec![1, 2]), (0x1008, vec![3])]), + loop_latches: vec![0x1000], + state_inputs: vec!["state".to_string(), "pc".to_string()], + state_outputs: vec!["state".to_string(), "pc".to_string()], + step_blocks: vec![0x1000, 0x1004], + handler_regions: BTreeMap::from([(0x1004, vec![0x1004, 0x1008])]), + handler_state_inputs: BTreeMap::from([(0x1004, vec!["state".to_string()])]), + handler_state_outputs: BTreeMap::from([(0x1004, vec!["state".to_string()])]), + handler_state_updates: BTreeMap::from([( + 0x1004, + vec![r2sym::VmStateUpdate { + output: "state".to_string(), + expr: "state + 1".to_string(), + value: r2sym::VmValueExpr::Expr("state + 1".to_string()), + exact: false, + }], + )]), + handler_exit_guards: BTreeMap::from([( + 0x1004, + vec![r2sym::VmGuardedExit { + target: 0x1008, + guard: r2sym::VmGuardCondition { + expr: "(state == 0x1)".to_string(), + value: r2sym::VmValueExpr::Expr("state == 0x1".to_string()), + expect_nonzero: true, + exact: false, + }, + }], + )]), + handler_memory_read_effects: BTreeMap::from([( + 0x1004, + vec![r2sym::VmMemoryCondition { + region: r2sym::VmMemoryRegionRef { + id: 1, + kind: r2sym::MemoryRegionKind::Global, + name: "ram:0x2000".to_string(), + }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + binding: Some("mem:r1:0:1".to_string()), + expr: "vm.sel".to_string(), + value_expr: None, + value: None, + exact_value: false, + }], + )]), + handler_memory_write_effects: BTreeMap::from([( + 0x1004, + vec![r2sym::VmMemoryCondition { + region: r2sym::VmMemoryRegionRef { + id: 2, + kind: r2sym::MemoryRegionKind::Heap, + name: "heap_alloc@1".to_string(), + }, + offset_lo: 4, + offset_hi: 4, + size: 1, + exact_offset: true, + binding: Some("mem:r2:4:1".to_string()), + expr: "state".to_string(), + value_expr: Some("state".to_string()), + value: Some(r2sym::VmValueExpr::Var("state".to_string())), + exact_value: false, + }], + )]), + handler_memory_reads: BTreeMap::from([(0x1004, 1)]), + handler_memory_writes: BTreeMap::from([(0x1004, 1)]), + handler_calls: BTreeMap::from([(0x1004, 0)]), + handler_conditional_branches: BTreeMap::from([(0x1004, 0)]), + handler_exit_targets: BTreeMap::from([(0x1004, vec![0x1008])]), + redispatch_handlers: vec![0x1000], + returning_handlers: vec![], + truncated_handlers: vec![], + transfers: vec![r2sym::VmTransferArm { + handler_target: 0x1004, + case_values: vec![1, 2], + region_blocks: vec![0x1004, 0x1008], + exit_targets: vec![0x1008], + exit_guards: vec![r2sym::VmGuardedExit { + target: 0x1008, + guard: r2sym::VmGuardCondition { + expr: "(state == 0x1)".to_string(), + value: r2sym::VmValueExpr::Expr("state == 0x1".to_string()), + expect_nonzero: true, + exact: false, + }, + }], + state_updates: vec![r2sym::VmStateUpdate { + output: "state".to_string(), + expr: "state + 1".to_string(), + value: r2sym::VmValueExpr::Expr("state + 1".to_string()), + exact: false, + }], + selector_update: Some(r2sym::VmStateUpdate { + output: "vm.sel".to_string(), + expr: "3".to_string(), + value: r2sym::VmValueExpr::Const(3), + exact: true, + }), + memory_reads: vec![r2sym::VmMemoryCondition { + region: r2sym::VmMemoryRegionRef { + id: 1, + kind: r2sym::MemoryRegionKind::Global, + name: "ram:0x2000".to_string(), + }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + binding: Some("mem:r1:0:1".to_string()), + expr: "vm.sel".to_string(), + value_expr: None, + value: None, + exact_value: false, + }], + memory_writes: vec![r2sym::VmMemoryCondition { + region: r2sym::VmMemoryRegionRef { + id: 2, + kind: r2sym::MemoryRegionKind::Heap, + name: "heap_alloc@1".to_string(), + }, + offset_lo: 4, + offset_hi: 4, + size: 1, + exact_offset: true, + binding: Some("mem:r2:4:1".to_string()), + expr: "state".to_string(), + value_expr: Some("state".to_string()), + value: Some(r2sym::VmValueExpr::Var("state".to_string())), + exact_value: false, + }], + residual_guards: false, + residual_memory_effects: false, + exact: false, + redispatch: false, + may_return: false, + truncated: false, + }], + }; + let semantic_artifact = r2sym::SemanticArtifact { + stage: r2sym::RefinementStage::Residual, + granularity: r2sym::ArtifactGranularity::SummaryOnly, + execution: r2sym::ExecutionModel::Vm, + body: r2sym::SemanticArtifactBody::Vm(Box::new(r2sym::VmArtifactBody { + interpreter: None, + step_summary: Some(vm_step), + transfer_summary: None, + })), + diagnostics: r2sym::SemanticArtifactDiagnostics { + branches_evaluated: 0, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: false, + residual_reasons: Vec::new(), + ambiguous_targets: Vec::new(), + cache_hit: false, + }, + }; + decompiler.set_function_facts(FunctionFacts::new( + FunctionTypeFacts::default(), + Some(semantic_artifact), + )); + + let output = decompiler.decompile(&func); + assert!( + output.contains("r2dec semantic summary: vm_summary"), + "expected VM semantic summary output, got:\n{output}" + ); + assert!( + output.contains("switch (vm.sel)") + && output.contains("case 0x1:") + && output.contains("handler 0x1004") + && output.contains("state = state + 1;") + && output.contains("read ram:0x2000") + && output.contains("vm.sel = 3;"), + "expected structured VM summary rendering, got:\n{output}" + ); + } + + #[test] + fn preferred_semantic_fallback_comment_allows_ready_large_worker_decompile() { + let region = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(compiled_summary( + "x == 0", + r2sym::BackwardConditionPrecision::Exact, + 1, + 1, + Vec::new(), + )), + r2sym::SemanticEvidence::exact(), + )], + Vec::new(), + ); + let semantic_artifact = large_cfg_worker_artifact( + r2sym::RefinementStage::Residual, + vec![r2sym::ResidualReason::LargeCfg], + vec![region], + ); + assert!( + preferred_semantic_fallback_comment("fcn.401000", Some(&semantic_artifact)).is_none(), + "expected semantically ready autogenerated large worker to keep decompilation path" + ); + assert!( + preferred_semantic_fallback_comment("named_worker", Some(&semantic_artifact)).is_none(), + "expected named function to keep full decompilation path" + ); + } + + #[test] + fn preferred_semantic_linearization_reason_allows_ready_large_worker_linear_path() { + let region = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + Vec::new(), + Vec::new(), + ); + let summary = r2ssa::CFGRiskSummary { + block_count: 107, + loop_count: 16, + back_edge_count: 16, + switch_block_count: 0, + max_switch_cases: 0, + }; + let semantic_artifact = large_cfg_worker_artifact( + r2sym::RefinementStage::Residual, + vec![r2sym::ResidualReason::LargeCfg], + vec![region], + ); + let reason = preferred_semantic_linearization_reason( + "fcn.401000", + Some(&semantic_artifact), + &summary, + ) + .expect("ready large worker should prefer linearized decompile path"); + assert!(reason.contains("complex loop graph")); + } + + #[test] + fn preferred_semantic_structuring_reason_allows_strict_large_worker_structuring_path() { + let likely = + r2sym::SemanticEvidence::likely(r2sym::SemanticEvidenceReason::PartialPathCoverage); + let memory_term = arg_memory_term(0, 1, likely.clone(), Some("0x0:8"), true); + let region = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(compiled_summary( + "x == 0", + r2sym::BackwardConditionPrecision::OverApprox, + 1, + 2, + vec![memory_term.clone()], + )), + likely.clone(), + )], + vec![test_memory_fact(memory_term, likely.clone())], + ); + let summary = r2ssa::CFGRiskSummary { + block_count: 107, + loop_count: 16, + back_edge_count: 16, + switch_block_count: 0, + max_switch_cases: 0, + }; + let semantic_artifact = large_cfg_worker_artifact( + r2sym::RefinementStage::Compiled, + vec![r2sym::ResidualReason::LargeCfg], + vec![region], + ); + let reason = + preferred_semantic_structuring_reason("fcn.401000", Some(&semantic_artifact), &summary) + .expect("strict ready large worker should prefer structured semantic path"); + assert!(reason.contains("complex loop graph")); + assert!( + preferred_semantic_linearization_reason( + "fcn.401000", + Some(&semantic_artifact), + &summary, + ) + .is_none(), + "structured-ready worker should not fall back to linearization" + ); + } + + #[test] + fn preferred_semantic_structuring_reason_rejects_body_wide_memory_without_target_local_support() + { + let likely = + r2sym::SemanticEvidence::likely(r2sym::SemanticEvidenceReason::PartialPathCoverage); + let region = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(compiled_summary( + "x == 0", + r2sym::BackwardConditionPrecision::OverApprox, + 1, + 2, + Vec::new(), + )), + likely.clone(), + )], + vec![test_memory_fact( + arg_memory_term(0, 1, likely.clone(), Some("0x0:8"), true), + likely.clone(), + )], + ); + let summary = r2ssa::CFGRiskSummary { + block_count: 107, + loop_count: 16, + back_edge_count: 16, + switch_block_count: 0, + max_switch_cases: 0, + }; + let semantic_artifact = large_cfg_worker_artifact( + r2sym::RefinementStage::Compiled, + vec![r2sym::ResidualReason::LargeCfg], + vec![region], + ); + assert!( + preferred_semantic_structuring_reason("fcn.401000", Some(&semantic_artifact), &summary) + .is_none(), + "body-wide memory support should not overstate semantic structuring readiness" + ); + assert!( + preferred_semantic_linearization_reason( + "fcn.401000", + Some(&semantic_artifact), + &summary, + ) + .is_some(), + "large workers without target-local structuring support should fall back to linearization" + ); + } + + #[test] + fn preferred_semantic_structuring_reason_rejects_ambiguous_target_sources() { + let exact = r2sym::SemanticEvidence::exact(); + let likely = + r2sym::SemanticEvidence::likely(r2sym::SemanticEvidenceReason::PartialPathCoverage); + let summary = r2ssa::CFGRiskSummary { + block_count: 107, + loop_count: 16, + back_edge_count: 16, + switch_block_count: 0, + max_switch_cases: 0, + }; + let region_a = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + Some(true), + Some("x == 0"), + Some(compiled_summary( + "x == 0", + r2sym::BackwardConditionPrecision::Exact, + 1, + 1, + vec![arg_memory_term(0, 1, exact.clone(), Some("0x2a"), true)], + )), + exact.clone(), + )], + vec![test_memory_fact( + arg_memory_term(0, 1, exact.clone(), Some("0x2a"), true), + exact.clone(), + )], + ); + let region_b = test_semantic_region( + 0x401004, + BTreeSet::from([0x401010, 0x401024]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + Some(true), + Some("y == 1"), + Some(compiled_summary( + "y == 1", + r2sym::BackwardConditionPrecision::OverApprox, + 1, + 2, + vec![arg_memory_term(0, 1, likely.clone(), Some("0x2b"), true)], + )), + likely.clone(), + )], + vec![test_memory_fact( + arg_memory_term(0, 1, likely.clone(), Some("0x2b"), true), + likely.clone(), + )], + ); + let semantic_artifact = large_cfg_worker_artifact( + r2sym::RefinementStage::Compiled, + vec![r2sym::ResidualReason::LargeCfg], + vec![region_a, region_b], + ); + assert!( + semantic_artifact.target_has_ambiguous_sources(0x401010), + "same-target conflicting worker regions must surface explicit ambiguity" + ); + assert!( + preferred_semantic_structuring_reason("fcn.401000", Some(&semantic_artifact), &summary) + .is_none(), + "ambiguous target sources must block semantic structuring" + ); + assert!( + preferred_semantic_linearization_reason( + "fcn.401000", + Some(&semantic_artifact), + &summary, + ) + .is_some(), + "ambiguous target sources should downgrade to linearization instead of structuring" + ); + } + + #[test] + fn preferred_semantic_structuring_reason_blocks_conflicting_assumptions_and_downgrades_linearization() + { + let likely = + r2sym::SemanticEvidence::likely(r2sym::SemanticEvidenceReason::PartialPathCoverage); + let memory_term = arg_memory_term(0, 1, likely.clone(), Some("0x0:8"), true); + let region = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(compiled_summary( + "x == 0", + r2sym::BackwardConditionPrecision::OverApprox, + 1, + 2, + vec![memory_term.clone()], + )), + likely.clone(), + )], + vec![test_memory_fact(memory_term, likely.clone())], + ); + let summary = r2ssa::CFGRiskSummary { + block_count: 107, + loop_count: 16, + back_edge_count: 16, + switch_block_count: 0, + max_switch_cases: 0, + }; + let semantic_artifact = large_cfg_worker_artifact( + r2sym::RefinementStage::Compiled, + vec![r2sym::ResidualReason::LargeCfg], + vec![region], + ); + let mut assumption_usage = r2ssa::AssumptionUsageReport::default(); + assumption_usage.mark_conflict( + &r2ssa::AnalysisAssumption { + id: Some("assumption_a".to_string()), + subject: r2ssa::AssumptionSubject::Parameter { index: 0 }, + value: r2ssa::AssumptionValue::TypeHint { + ty: "int32_t".to_string(), + }, + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::User, + }, + "parameter type conflict", + ); + assumption_usage.mark_conflict( + &r2ssa::AnalysisAssumption { + id: Some("assumption_b".to_string()), + subject: r2ssa::AssumptionSubject::Register { + name: "rax".to_string(), + }, + value: r2ssa::AssumptionValue::TypeHint { + ty: "int64_t".to_string(), + }, + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::User, + }, + "register type conflict", + ); + let function_facts = r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + Some(semantic_artifact), + ) + .with_assumption_usage(assumption_usage); + assert!( + crate::planner::preferred_semantic_structuring_reason( + "fcn.401000", + &function_facts, + &summary, + ) + .is_none(), + "assumption conflicts must block semantic structuring" + ); + let linear_reason = crate::planner::preferred_semantic_linearization_reason( + "fcn.401000", + &function_facts, + &summary, + ) + .expect("assumption conflicts should downgrade to linearization"); + assert!(linear_reason.contains("complex loop graph")); + } + + #[test] + fn autogenerated_name_detection_accepts_underscore_hex_labels() { + assert!(is_autogenerated_function_name("_140010138")); + assert!(is_autogenerated_function_name("_401000")); + assert!(!is_autogenerated_function_name("_named_worker")); + } + + #[test] + fn preferred_semantic_structuring_reason_uses_worker_label_when_cfg_is_benign() { + let exact = r2sym::SemanticEvidence::exact(); + let region = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(compiled_summary( + "x == 0", + r2sym::BackwardConditionPrecision::Exact, + 1, + 1, + vec![arg_memory_term(0, 1, exact.clone(), Some("0x0:8"), true)], + )), + exact.clone(), + )], + Vec::new(), + ); + let summary = r2ssa::CFGRiskSummary { + block_count: 40, + loop_count: 1, + back_edge_count: 1, + switch_block_count: 0, + max_switch_cases: 0, + }; + let semantic_artifact = + large_cfg_worker_artifact(r2sym::RefinementStage::Compiled, Vec::new(), vec![region]); + assert_eq!( + preferred_semantic_structuring_reason("_140010138", Some(&semantic_artifact), &summary) + .as_deref(), + Some("semantic worker islands") + ); + } + + #[test] + fn preferred_semantic_linearization_reason_uses_worker_label_when_cfg_is_benign() { + let likely = + r2sym::SemanticEvidence::likely(r2sym::SemanticEvidenceReason::PartialPathCoverage); + let region = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(compiled_summary( + "x == 0", + r2sym::BackwardConditionPrecision::OverApprox, + 1, + 2, + Vec::new(), + )), + likely.clone(), + )], + Vec::new(), + ); + let summary = r2ssa::CFGRiskSummary { + block_count: 40, + loop_count: 1, + back_edge_count: 1, + switch_block_count: 0, + max_switch_cases: 0, + }; + let semantic_artifact = + large_cfg_worker_artifact(r2sym::RefinementStage::Compiled, Vec::new(), vec![region]); + assert_eq!( + preferred_semantic_linearization_reason( + "_140010138", + Some(&semantic_artifact), + &summary, + ) + .as_deref(), + Some("semantic worker islands") + ); + } + + #[test] + fn semantic_fallback_comment_reports_actionable_control_islands() { + let exact = r2sym::SemanticEvidence::exact(); + let memory_term = arg_memory_term(8, 4, exact.clone(), None, false); + let region = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + Some(true), + Some("x == 0"), + Some(compiled_summary( + "x == 0", + r2sym::BackwardConditionPrecision::Exact, + 1, + 1, + vec![memory_term.clone()], + )), + exact.clone(), + )], + vec![test_memory_fact(memory_term, exact.clone())], + ); + let semantic_artifact = large_cfg_worker_artifact( + r2sym::RefinementStage::Residual, + vec![r2sym::ResidualReason::LargeCfg], + vec![region], + ); + let output = semantic_fallback_comment("_401000", Some(&semantic_artifact)) + .expect("typed semantic fallback comment"); + assert!(output.contains("semantic fallback: worker slice in residual mode")); + assert!(output.contains("regions=1")); + assert!(output.contains("actionable_conditions=1")); + assert!(output.contains("exact_conditions=1")); + assert!(output.contains("actionable_preview=[0x401000: x == 0]")); + } + + #[test] + fn semantic_fallback_comment_reports_assumption_conflicts() { + let semantic_artifact = large_cfg_worker_artifact( + r2sym::RefinementStage::Residual, + vec![r2sym::ResidualReason::LargeCfg], + Vec::new(), + ); + let mut assumption_usage = r2ssa::AssumptionUsageReport::default(); + assumption_usage.mark_conflict( + &r2ssa::AnalysisAssumption { + id: Some("assumption_c".to_string()), + subject: r2ssa::AssumptionSubject::Parameter { index: 0 }, + value: r2ssa::AssumptionValue::TypeHint { + ty: "int32_t".to_string(), + }, + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::User, + }, + "parameter type conflict", + ); + assumption_usage.mark_conflict( + &r2ssa::AnalysisAssumption { + id: Some("assumption_d".to_string()), + subject: r2ssa::AssumptionSubject::Register { + name: "rdi".to_string(), + }, + value: r2ssa::AssumptionValue::TypeHint { + ty: "int64_t".to_string(), + }, + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::User, + }, + "register type conflict", + ); + let function_facts = r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + Some(semantic_artifact), + ) + .with_assumption_usage(assumption_usage); + let output = + crate::consumer_fallback::semantic_fallback_comment("_401000", &function_facts) + .expect("typed semantic fallback comment"); + assert!( + output.contains("assumption_conflicts=2"), + "expected fallback comment to report the assumption conflict count, got:\n{output}" + ); + } } diff --git a/crates/r2dec/src/planner.rs b/crates/r2dec/src/planner.rs new file mode 100644 index 0000000..2f8e12f --- /dev/null +++ b/crates/r2dec/src/planner.rs @@ -0,0 +1,225 @@ +use r2il::R2ILBlock; +use r2ssa::{CFGRiskSummary, SSAFunction, SsaArtifact}; +use r2types::{DecompileCapabilityView, FunctionFacts, FunctionTypeFacts}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SemanticRoutePlan { + Standard, + StructuredWorker { reason: String }, + LinearWorker { reason: String }, + VmSummary { reason: String }, + FallbackComment { comment: String }, +} + +fn decompile_capability(function_facts: &FunctionFacts) -> DecompileCapabilityView { + function_facts.decompile_capability() +} + +pub(crate) fn prefer_symbolic_large_worker_decompile(function_facts: &FunctionFacts) -> bool { + let capability = decompile_capability(function_facts); + capability + .plan + .as_ref() + .is_some_and(r2sym::DecompilePlan::allows_native_linearization) + && capability.skipped_large_cfg + && matches!(capability.slice_class, Some(r2sym::SliceClass::Worker)) + && capability.has_native_regions +} + +pub(crate) fn should_skip_runtime_type_inference( + prepared: Option<&SsaArtifact>, + _type_facts: &FunctionTypeFacts, + function_facts: &FunctionFacts, +) -> bool { + if prefer_symbolic_large_worker_decompile(function_facts) { + return true; + } + let Some(prepared) = prepared else { + return false; + }; + let summary = prepared.function().cfg_risk_summary(); + summary.block_count >= 96 + && summary.switch_block_count > 0 + && summary.max_switch_cases >= 32 + && summary.back_edge_count == 0 +} + +pub(crate) fn should_use_prepared_semantic_view( + prepared: Option<&SsaArtifact>, + function_facts: &FunctionFacts, +) -> bool { + prepared.is_some() && !prefer_symbolic_large_worker_decompile(function_facts) +} + +pub(crate) fn preferred_semantic_fallback_comment( + func_name: &str, + function_facts: &FunctionFacts, +) -> Option { + let capability = decompile_capability(function_facts); + if !crate::is_autogenerated_function_name(func_name) { + return None; + } + if capability + .plan + .as_ref() + .is_some_and(r2sym::DecompilePlan::allows_native_linearization) + { + return None; + } + if capability.skipped_large_cfg { + return crate::consumer_fallback::semantic_fallback_comment(func_name, function_facts); + } + capability + .residual_reasons + .contains(&r2sym::ResidualReason::InterpreterRequiresStepSummary) + .then(|| crate::consumer_fallback::semantic_fallback_comment(func_name, function_facts)) + .flatten() +} + +pub(crate) fn preferred_vm_summary_reason(function_facts: &FunctionFacts) -> Option { + match function_facts.decompile_plan()? { + r2sym::DecompilePlan::VmSummaryOnly { reason } => Some(reason), + _ => None, + } +} + +pub(crate) fn preferred_semantic_linearization_reason( + func_name: &str, + function_facts: &FunctionFacts, + cfg_summary: &CFGRiskSummary, +) -> Option { + let capability = decompile_capability(function_facts); + if !crate::is_autogenerated_function_name(func_name) { + return None; + } + let plan = capability.plan.as_ref()?; + let downgraded_from_structured = matches!(plan, r2sym::DecompilePlan::NativeStructured) + && (capability.assumption_conflicted + || capability.summary_conflicted + || !capability.ambiguous_targets.is_empty()); + let linear_ready = + matches!(plan, r2sym::DecompilePlan::NativeLinear { .. }) || downgraded_from_structured; + if !linear_ready || !capability.skipped_large_cfg || !capability.has_native_regions { + return None; + } + Some(preferred_semantic_worker_reason(cfg_summary)) +} + +pub(crate) fn preferred_semantic_structuring_reason( + func_name: &str, + function_facts: &FunctionFacts, + cfg_summary: &CFGRiskSummary, +) -> Option { + let capability = decompile_capability(function_facts); + if !crate::is_autogenerated_function_name(func_name) { + return None; + } + if !capability + .plan + .as_ref() + .is_some_and(r2sym::DecompilePlan::allows_native_structuring) + { + return None; + } + if !capability.skipped_large_cfg || !capability.has_native_regions { + return None; + } + if capability.actionable_region_count == 0 + || capability.assumption_conflicted + || capability.summary_conflicted + || !capability.ambiguous_targets.is_empty() + { + return None; + } + Some(preferred_semantic_worker_reason(cfg_summary)) +} + +pub(crate) fn preferred_semantic_worker_reason(cfg_summary: &CFGRiskSummary) -> String { + cfg_guard_reason_from_summary(cfg_summary) + .unwrap_or_else(|| "semantic worker islands".to_string()) +} + +pub fn cfg_guard_reason_from_summary(summary: &CFGRiskSummary) -> Option { + if summary.loop_count > 8 || summary.back_edge_count > 16 { + return Some(format!( + "complex loop graph (loops={}, back_edges={})", + summary.loop_count, summary.back_edge_count + )); + } + + if summary.loop_count > 4 && summary.block_count >= 96 && summary.max_switch_cases >= 32 { + return Some(format!( + "large dense switch in looped CFG (blocks={}, loops={}, max_switch_cases={})", + summary.block_count, summary.loop_count, summary.max_switch_cases + )); + } + + None +} + +pub fn cfg_guard_reason(blocks: &[R2ILBlock]) -> Option { + let ssa_func = SSAFunction::from_blocks_raw_no_arch(blocks)?; + cfg_guard_reason_from_summary(&ssa_func.cfg_risk_summary()) +} + +pub fn block_guard_fallback_comment(func_name: &str, blocks: usize, max_blocks: usize) -> String { + format!( + "/* r2dec fallback: skipped decompilation for {} ({} blocks > limit {}). Set SLEIGH_DEC_MAX_BLOCKS to override. */", + func_name, blocks, max_blocks + ) +} + +pub fn artifact_guard_fallback_comment(func_name: &str, reason: &str) -> String { + format!( + "/* r2dec fallback: skipped decompilation for {} ({}) */", + func_name, reason + ) +} + +pub fn semantic_route_plan( + func_name: &str, + function_facts: &FunctionFacts, + cfg_summary: &CFGRiskSummary, +) -> SemanticRoutePlan { + if let Some(reason) = preferred_vm_summary_reason(function_facts) { + return SemanticRoutePlan::VmSummary { reason }; + } + if let Some(comment) = preferred_semantic_fallback_comment(func_name, function_facts) { + return SemanticRoutePlan::FallbackComment { comment }; + } + if let Some(reason) = + preferred_semantic_structuring_reason(func_name, function_facts, cfg_summary) + { + return SemanticRoutePlan::StructuredWorker { reason }; + } + if let Some(reason) = + preferred_semantic_linearization_reason(func_name, function_facts, cfg_summary) + { + return SemanticRoutePlan::LinearWorker { reason }; + } + SemanticRoutePlan::Standard +} + +pub fn detached_semantic_route_plan( + func_name: &str, + blocks: &[R2ILBlock], + function_facts: &FunctionFacts, +) -> Option { + let ssa_func = SSAFunction::from_blocks_raw_no_arch(blocks)?; + Some(semantic_route_plan( + func_name, + function_facts, + &ssa_func.cfg_risk_summary(), + )) +} + +pub fn detached_semantic_linearization_reason( + func_name: &str, + blocks: &[R2ILBlock], + function_facts: &FunctionFacts, +) -> Option { + match detached_semantic_route_plan(func_name, blocks, function_facts)? { + SemanticRoutePlan::LinearWorker { reason } => Some(reason), + _ => None, + } +} diff --git a/crates/r2dec/src/structure.rs b/crates/r2dec/src/structure.rs index 9902a34..8b9a4ae 100644 --- a/crates/r2dec/src/structure.rs +++ b/crates/r2dec/src/structure.rs @@ -178,6 +178,14 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { else_region, merge_block, } => { + if let Some(rewritten) = self.try_structure_symbolic_actionable_if( + *cond_block, + then_region, + else_region.as_deref(), + *merge_block, + ) { + return rewritten; + } if let Some(rewritten) = self.try_structure_if_else_with_slot_merge_returns( *cond_block, then_region, @@ -252,6 +260,22 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { None => return CExpr::Var("switch_expr".to_string()), }; + if let Some(vm_step) = self + .fold_ctx + .inputs + .semantic_artifact + .and_then(|artifact| artifact.vm_step_for_dispatch_header(switch_addr)) + .or_else(|| { + self.fold_ctx + .inputs + .semantic_artifact + .and_then(|artifact| artifact.vm_transfer_for_dispatch_header(switch_addr)) + }) + && let Some(selector) = vm_step.selector.as_ref() + { + return CExpr::Var(selector.clone()); + } + if let Some(expr) = self.fold_ctx.resolve_switch_expr_for_block(switch_addr) { return expr; } @@ -401,6 +425,359 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { Some((true_target?, false_target?)) } + fn symbolic_exact_reachable_target(&self, cond_block: u64) -> Option { + self.fold_ctx + .inputs + .semantic_artifact? + .exact_reachable_target_for_block(cond_block) + } + + fn symbolic_actionable_reachable_target(&self, cond_block: u64) -> Option { + self.fold_ctx + .inputs + .semantic_artifact? + .actionable_reachable_target_for_block(cond_block) + } + + fn region_supports_semantic_structuring(&self, region: &r2sym::SemanticRegion) -> bool { + let reachable_target = region + .exact_reachable_target() + .or_else(|| region.actionable_reachable_target()); + if let Some(target) = reachable_target + && self + .fold_ctx + .inputs + .semantic_artifact + .is_some_and(|artifact| artifact.target_has_ambiguous_sources(target)) + { + return false; + } + region.supports_guarded_structuring() + } + + fn region_supporting_compiled_condition( + region: &r2sym::SemanticRegion, + ) -> Option<&r2sym::BackwardConditionSummary> { + let reachable_target = region + .exact_reachable_target() + .or_else(|| region.actionable_reachable_target())?; + region.actionable_compiled_condition_for_target(reachable_target) + } + + fn branch_condition_for_reachable_target( + &mut self, + region: &r2sym::SemanticRegion, + reachable_target: u64, + ) -> CExpr { + let cond = self.get_branch_condition(region.anchor); + if Self::region_supporting_compiled_condition(region).is_some() { + return cond; + } + if self + .resolve_conditional_targets(region.anchor) + .is_some_and(|(_, false_target)| false_target == reachable_target) + { + return Self::negate_condition(cond); + } + if region.branch_truth_for_target(reachable_target) == Some(false) { + return Self::negate_condition(cond); + } + cond + } + + fn structure_cfg_suffix_from_target(&mut self, target: u64) -> Option { + let mut stmts = Vec::new(); + let mut current = target; + let mut seen = HashSet::new(); + + loop { + if !seen.insert(current) { + break; + } + Self::append_stmt_body_flat(&mut stmts, self.structure_block(current)); + let cfg_block = self.func.cfg().get_block(current)?; + let next = match cfg_block.terminator { + BlockTerminator::Fallthrough { next } + | BlockTerminator::Branch { target: next } => Some(next), + _ => None, + }; + let Some(next) = next else { + break; + }; + if self.func.get_block(next).is_none() { + break; + } + current = next; + } + + Some(if stmts.len() == 1 { + stmts.into_iter().next().unwrap_or(CStmt::Empty) + } else { + CStmt::Block(stmts) + }) + } + + fn structure_function_suffix_from_target(&mut self, target: u64) -> Option { + if self.region_analyzer.is_none() { + self.region_analyzer = Some(RegionAnalyzer::new(self.func)); + } + let root_region = self.region_analyzer.as_mut()?.analyze(); + let structured = self.structure_region_suffix_from_target(&root_region, target); + match structured { + Some(CStmt::Empty) | None => self.structure_cfg_suffix_from_target(target), + other => other, + } + } + + fn structure_semantic_region(&mut self, region: &r2sym::SemanticRegion) -> Option { + if !self.region_supports_semantic_structuring(region) { + return None; + } + let reachable_target = region + .exact_reachable_target() + .or_else(|| region.actionable_reachable_target())?; + let cond = self.branch_condition_for_reachable_target(region, reachable_target); + let memory_terms = region.actionable_memory_terms_for_target(reachable_target); + let mut then_stmts = Vec::new(); + if let Some(compiled) = Self::region_supporting_compiled_condition(region) { + then_stmts.push(CStmt::comment(format!( + "semantic worker branch: {}", + compiled.simplified + ))); + } + for term in memory_terms.into_iter().take(2) { + then_stmts.push(CStmt::comment(format!( + "semantic memory: {} [{:?}]", + term.expr, term.evidence.tier + ))); + } + if self.func.get_block(reachable_target).is_some() { + let reachable_stmt = self + .structure_function_suffix_from_target(reachable_target) + .unwrap_or_else(|| self.structure_block(reachable_target)); + Self::append_stmt_body_flat(&mut then_stmts, reachable_stmt); + } else { + then_stmts.push(CStmt::comment(format!( + "semantic worker target: 0x{reachable_target:x}" + ))); + } + let then_body = if then_stmts.len() == 1 { + then_stmts.into_iter().next().unwrap_or(CStmt::Empty) + } else { + CStmt::Block(then_stmts) + }; + let mut prefix = self.structure_block_prefix_stmts(region.anchor); + prefix.push(CStmt::if_stmt(cond, then_body, None)); + Some(if prefix.len() == 1 { + prefix.into_iter().next().unwrap_or(CStmt::Empty) + } else { + CStmt::Block(prefix) + }) + } + + pub fn structure_semantic_worker_islands(&mut self, limit: usize) -> Option { + let regions = self + .fold_ctx + .inputs + .semantic_artifact + .map(|artifact| artifact.actionable_regions()) + .unwrap_or_default(); + let mut stmts = Vec::new(); + let mut seen_targets = HashSet::new(); + for region in regions.into_iter().take(limit) { + let Some(target) = region + .exact_reachable_target() + .or_else(|| region.actionable_reachable_target()) + else { + continue; + }; + if !seen_targets.insert((region.anchor, target)) { + continue; + } + if let Some(stmt) = self.structure_semantic_region(region) { + Self::append_stmt_body_flat(&mut stmts, stmt); + } + } + if stmts.is_empty() { + None + } else if stmts.len() == 1 { + Some(stmts.into_iter().next().unwrap_or(CStmt::Empty)) + } else { + Some(CStmt::Block(stmts)) + } + } + + fn structure_region_suffix_from_target( + &mut self, + region: &Region, + target: u64, + ) -> Option { + match region { + Region::Block(addr) => (*addr == target).then(|| self.structure_block(*addr)), + Region::Sequence(regions) => { + let start_idx = regions + .iter() + .position(|child| child.blocks().contains(&target))?; + let mut stmts = Vec::new(); + Self::append_stmt_body_flat( + &mut stmts, + self.structure_region_suffix_from_target(®ions[start_idx], target)?, + ); + for child in ®ions[start_idx + 1..] { + Self::append_stmt_body_flat(&mut stmts, self.structure_region(child)); + } + Some(if stmts.len() == 1 { + stmts.into_iter().next().unwrap_or(CStmt::Empty) + } else { + CStmt::Block(stmts) + }) + } + Region::IfThenElse { + cond_block, + then_region, + else_region, + merge_block, + } => { + if *cond_block == target { + return Some(self.structure_region(region)); + } + let mut stmts = if then_region.blocks().contains(&target) { + let mut stmts = Vec::new(); + Self::append_stmt_body_flat( + &mut stmts, + self.structure_region_suffix_from_target(then_region, target)?, + ); + stmts + } else if else_region + .as_deref() + .is_some_and(|region| region.blocks().contains(&target)) + { + let mut stmts = Vec::new(); + Self::append_stmt_body_flat( + &mut stmts, + self.structure_region_suffix_from_target(else_region.as_deref()?, target)?, + ); + stmts + } else if merge_block.is_some_and(|merge| merge == target) { + vec![self.structure_block(target)] + } else { + return None; + }; + if let Some(merge_addr) = merge_block.filter(|merge| *merge != target) { + Self::append_stmt_body_flat(&mut stmts, self.structure_block(merge_addr)); + } + Some(if stmts.len() == 1 { + stmts.into_iter().next().unwrap_or(CStmt::Empty) + } else { + CStmt::Block(stmts) + }) + } + Region::WhileLoop { header, body } => { + if *header == target { + Some(self.structure_region(region)) + } else if body.blocks().contains(&target) { + self.structure_region_suffix_from_target(body, target) + } else { + None + } + } + Region::DoWhileLoop { body, cond_block } => { + if *cond_block == target { + return Some(self.structure_block(*cond_block)); + } + if !body.blocks().contains(&target) { + return None; + } + let mut stmts = Vec::new(); + Self::append_stmt_body_flat( + &mut stmts, + self.structure_region_suffix_from_target(body, target)?, + ); + Self::append_stmt_body_flat(&mut stmts, self.structure_block(*cond_block)); + Some(if stmts.len() == 1 { + stmts.into_iter().next().unwrap_or(CStmt::Empty) + } else { + CStmt::Block(stmts) + }) + } + Region::Switch { + switch_block, + cases, + default, + merge_block, + } => { + if *switch_block == target { + return Some(self.structure_region(region)); + } + let mut stmts = if let Some((_, case_region)) = cases + .iter() + .find(|(_, case_region)| case_region.blocks().contains(&target)) + { + let mut stmts = Vec::new(); + Self::append_stmt_body_flat( + &mut stmts, + self.structure_region_suffix_from_target(case_region, target)?, + ); + stmts + } else if default + .as_deref() + .is_some_and(|region| region.blocks().contains(&target)) + { + let mut stmts = Vec::new(); + Self::append_stmt_body_flat( + &mut stmts, + self.structure_region_suffix_from_target(default.as_deref()?, target)?, + ); + stmts + } else if merge_block.is_some_and(|merge| merge == target) { + vec![self.structure_block(target)] + } else { + return None; + }; + if let Some(merge_addr) = merge_block.filter(|merge| *merge != target) { + Self::append_stmt_body_flat(&mut stmts, self.structure_block(merge_addr)); + } + Some(if stmts.len() == 1 { + stmts.into_iter().next().unwrap_or(CStmt::Empty) + } else { + CStmt::Block(stmts) + }) + } + Region::Irreducible { blocks, .. } => blocks + .contains(&target) + .then(|| self.structure_block(target)), + } + } + + fn try_structure_symbolic_actionable_if( + &mut self, + cond_block: u64, + then_region: &Region, + else_region: Option<&Region>, + merge_block: Option, + ) -> Option { + let reachable_target = self + .symbolic_exact_reachable_target(cond_block) + .or_else(|| self.symbolic_actionable_reachable_target(cond_block))?; + let reachable_stmt = if then_region.blocks().contains(&reachable_target) { + self.structure_region_suffix_from_target(then_region, reachable_target)? + } else { + let else_region = else_region?; + self.structure_region_suffix_from_target(else_region, reachable_target)? + }; + + let mut prefix = self.structure_block_prefix_stmts(cond_block); + Self::append_stmt_body_flat(&mut prefix, reachable_stmt); + if let Some(merge_addr) = merge_block { + Self::append_stmt_body_flat(&mut prefix, self.structure_block(merge_addr)); + } + Some(if prefix.len() == 1 { + prefix.into_iter().next().unwrap_or(CStmt::Empty) + } else { + CStmt::Block(prefix) + }) + } + fn switch_case_display_bias( &self, switch_block: u64, @@ -776,6 +1153,7 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { then_body, else_body, }; + let stmt = Self::rewrite_constant_condition_stmt(stmt); let stmt = Self::rewrite_if_short_circuit(stmt); let stmt = Self::rewrite_if_condition_inversion(stmt); let stmt = Self::rewrite_empty_if_bodies(stmt); @@ -833,6 +1211,32 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { } } + fn rewrite_constant_condition_stmt(stmt: CStmt) -> CStmt { + match stmt { + CStmt::If { + cond, + then_body, + else_body, + } if Self::is_const_true_expr(&cond) => *then_body, + CStmt::If { + cond, + then_body: _, + else_body, + } if Self::is_const_false_expr(&cond) => { + else_body.map(|stmt| *stmt).unwrap_or(CStmt::Empty) + } + other => other, + } + } + + fn is_const_true_expr(expr: &CExpr) -> bool { + matches!(expr, CExpr::IntLit(1) | CExpr::UIntLit(1)) + } + + fn is_const_false_expr(expr: &CExpr) -> bool { + matches!(expr, CExpr::IntLit(0) | CExpr::UIntLit(0)) + } + fn rewrite_if_short_circuit(stmt: CStmt) -> CStmt { let CStmt::If { cond, @@ -2140,6 +2544,10 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { mod tests { use super::ControlFlowStructurer; use crate::ast::{BinaryOp, CExpr, CStmt, UnaryOp}; + use crate::fold::FoldingContext; + use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, Varnode}; + use r2ssa::SSAFunction; + use std::collections::{BTreeMap, BTreeSet}; fn v(name: &str) -> CExpr { CExpr::Var(name.to_string()) @@ -2153,6 +2561,128 @@ mod tests { expr_stmt(CExpr::assign(v(lhs), rhs)) } + fn test_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.add_register(RegisterDef::new("RAX", 0x00, 8)); + arch.add_register(RegisterDef::new("RDI", 0x10, 8)); + arch.add_register(RegisterDef::new("RBP", 0x20, 8)); + arch.add_register(RegisterDef::new("RSP", 0x28, 8)); + arch + } + + fn function_with_single_block(addr: u64) -> SSAFunction { + let mut block = R2ILBlock::new(addr, 4); + block.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + SSAFunction::from_blocks_with_arch(&[block], Some(&test_arch())) + .expect("ssa function") + .with_name("vm_summary_demo") + } + + fn function_with_return_blocks(addrs: &[u64]) -> SSAFunction { + let blocks = addrs + .iter() + .map(|addr| { + let mut block = R2ILBlock::new(*addr, 4); + block.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + block + }) + .collect::>(); + SSAFunction::from_blocks_with_arch(&blocks, Some(&test_arch())) + .expect("ssa function") + .with_name("worker_region_demo") + } + + fn function_with_conditional_return_blocks( + cond_block: u64, + true_target: u64, + false_target: u64, + ) -> SSAFunction { + let mut cond = R2ILBlock::new(cond_block, 4); + cond.push(R2ILOp::IntEqual { + dst: Varnode::register(0x80, 1), + a: Varnode::register(0x10, 8), + b: Varnode::constant(0, 8), + }); + cond.push(R2ILOp::CBranch { + target: Varnode::constant(true_target, 8), + cond: Varnode::register(0x80, 1), + }); + + let mut false_block = R2ILBlock::new(false_target, 4); + false_block.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + + let mut true_block = R2ILBlock::new(true_target, 4); + true_block.push(R2ILOp::Return { + target: Varnode::constant(1, 8), + }); + + SSAFunction::from_blocks_with_arch(&[cond, false_block, true_block], Some(&test_arch())) + .expect("ssa function") + .with_name("worker_structured_demo") + } + + fn function_with_multi_block_true_arm( + cond_block: u64, + true_entry: u64, + true_tail: u64, + false_target: u64, + ) -> SSAFunction { + let mut cond = R2ILBlock::new(cond_block, 4); + cond.push(R2ILOp::IntEqual { + dst: Varnode::register(0x80, 1), + a: Varnode::register(0x10, 8), + b: Varnode::constant(0, 8), + }); + cond.push(R2ILOp::CBranch { + target: Varnode::constant(true_entry, 8), + cond: Varnode::register(0x80, 1), + }); + + let mut false_block = R2ILBlock::new(false_target, 4); + false_block.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + + let mut true_head = R2ILBlock::new(true_entry, 4); + true_head.push(R2ILOp::Copy { + dst: Varnode::register(0x00, 8), + src: Varnode::constant(1, 8), + }); + true_head.push(R2ILOp::Branch { + target: Varnode::constant(true_tail, 8), + }); + + let mut true_tail_block = R2ILBlock::new(true_tail, 4); + true_tail_block.push(R2ILOp::Return { + target: Varnode::register(0x00, 8), + }); + + SSAFunction::from_blocks_with_arch( + &[cond, false_block, true_head, true_tail_block], + Some(&test_arch()), + ) + .expect("ssa function") + .with_name("worker_multiblock_demo") + } + + fn trailing_if_stmt(stmt: &CStmt) -> &CStmt { + match stmt { + CStmt::If { .. } => stmt, + CStmt::Block(stmts) => stmts + .iter() + .rev() + .find(|stmt| matches!(stmt, CStmt::If { .. })) + .expect("trailing if statement"), + _ => panic!("expected structured if statement, got {stmt:?}"), + } + } + #[test] fn rewrites_canonical_while_to_for() { let input = CStmt::Block(vec![ @@ -2531,6 +3061,24 @@ mod tests { assert_eq!(cleaned, CStmt::Empty); } + #[test] + fn constant_true_if_collapses_to_then_body() { + let input = CStmt::if_stmt(CExpr::IntLit(1), assign("x", CExpr::IntLit(7)), None); + let cleaned = ControlFlowStructurer::cleanup(input); + assert_eq!(cleaned, assign("x", CExpr::IntLit(7))); + } + + #[test] + fn constant_false_if_collapses_to_else_body() { + let input = CStmt::if_stmt( + CExpr::IntLit(0), + assign("x", CExpr::IntLit(7)), + Some(assign("x", CExpr::IntLit(9))), + ); + let cleaned = ControlFlowStructurer::cleanup(input); + assert_eq!(cleaned, assign("x", CExpr::IntLit(9))); + } + #[test] fn guarded_switch_with_trailing_return_becomes_switch_default() { let input = CStmt::Block(vec![CStmt::if_stmt( @@ -2719,4 +3267,664 @@ mod tests { "Suffix-equivalent loop vars (local/local_4) should be treated as matching" ); } + + #[test] + fn uses_vm_transfer_selector_when_vm_step_is_absent() { + let func = function_with_single_block(0x1000); + let mut ctx = FoldingContext::new(64); + let artifact = r2sym::SemanticArtifact { + stage: r2sym::RefinementStage::Residual, + granularity: r2sym::ArtifactGranularity::SummaryOnly, + execution: r2sym::ExecutionModel::Vm, + body: r2sym::SemanticArtifactBody::Vm(Box::new(r2sym::VmArtifactBody { + interpreter: None, + step_summary: None, + transfer_summary: Some(r2sym::VmStepSummary { + kind: r2sym::InterpreterKind::SwitchDispatch, + loop_header: 0x1000, + dispatch_header: 0x1000, + selector: Some("vm.sel".to_string()), + dispatch_targets: vec![0x1004], + default_target: None, + case_values_by_target: BTreeMap::from([(0x1004, vec![1, 2])]), + loop_latches: vec![0x1000], + state_inputs: vec!["state".to_string()], + state_outputs: vec!["state".to_string()], + step_blocks: vec![0x1000], + handler_regions: BTreeMap::from([(0x1004, vec![0x1004, 0x1008])]), + handler_state_inputs: BTreeMap::from([(0x1004, vec!["state".to_string()])]), + handler_state_outputs: BTreeMap::from([(0x1004, vec!["state".to_string()])]), + handler_state_updates: BTreeMap::from([( + 0x1004, + vec![r2sym::VmStateUpdate { + output: "state".to_string(), + expr: "state + 1".to_string(), + value: r2sym::VmValueExpr::Expr("state + 1".to_string()), + exact: false, + }], + )]), + handler_exit_guards: BTreeMap::new(), + handler_memory_read_effects: BTreeMap::new(), + handler_memory_write_effects: BTreeMap::new(), + handler_memory_reads: BTreeMap::from([(0x1004, 1)]), + handler_memory_writes: BTreeMap::from([(0x1004, 1)]), + handler_calls: BTreeMap::from([(0x1004, 0)]), + handler_conditional_branches: BTreeMap::from([(0x1004, 0)]), + handler_exit_targets: BTreeMap::from([(0x1004, vec![0x1008])]), + redispatch_handlers: vec![0x1000], + returning_handlers: vec![], + truncated_handlers: vec![], + transfers: vec![r2sym::VmTransferArm { + handler_target: 0x1004, + case_values: vec![1, 2], + region_blocks: vec![0x1004, 0x1008], + exit_targets: vec![0x1008], + exit_guards: Vec::new(), + state_updates: vec![r2sym::VmStateUpdate { + output: "state".to_string(), + expr: "state + 1".to_string(), + value: r2sym::VmValueExpr::Expr("state + 1".to_string()), + exact: false, + }], + selector_update: None, + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, + exact: false, + redispatch: false, + may_return: false, + truncated: false, + }], + }), + })), + diagnostics: r2sym::SemanticArtifactDiagnostics { + branches_evaluated: 0, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: false, + residual_reasons: Vec::new(), + ambiguous_targets: Vec::new(), + cache_hit: false, + }, + }; + ctx.inputs.semantic_artifact = Some(Box::leak(Box::new(artifact))); + + let mut structurer = ControlFlowStructurer::new(&func, &ctx); + assert_eq!( + structurer.get_switch_expression(0x1000), + CExpr::Var("vm.sel".to_string()) + ); + } + + #[test] + fn symbolic_exact_reachable_target_uses_control_island_fallback() { + let func = function_with_single_block(0x2000); + let mut ctx = FoldingContext::new(64); + let region = crate::test_semantic_region( + 0x2000, + BTreeSet::from([0x2004, 0x2008]), + vec![ + crate::test_control_fact( + 0x2004, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + None, + r2sym::SemanticEvidence::exact(), + ), + crate::test_control_fact( + 0x2008, + r2sym::SymbolicReachabilityStatus::Unreachable, + None, + Some("!(x == 0)"), + None, + r2sym::SemanticEvidence::exact(), + ), + ], + Vec::new(), + ); + ctx.inputs.semantic_artifact = Some(crate::leaked_test_semantic_artifact( + crate::test_native_semantic_artifact( + r2sym::RefinementStage::Compiled, + r2sym::ArtifactGranularity::Regioned, + r2sym::SliceClass::Worker, + false, + Vec::new(), + vec![region], + ), + )); + + let structurer = ControlFlowStructurer::new(&func, &ctx); + assert_eq!( + structurer.symbolic_exact_reachable_target(0x2000), + Some(0x2004) + ); + } + + #[test] + fn symbolic_actionable_reachable_target_uses_likely_control_island_fallback() { + let func = function_with_single_block(0x2000); + let mut ctx = FoldingContext::new(64); + let likely = + r2sym::SemanticEvidence::likely(r2sym::SemanticEvidenceReason::PartialPathCoverage); + let region = crate::test_semantic_region( + 0x2000, + BTreeSet::from([0x2004, 0x2008]), + vec![ + crate::test_control_fact( + 0x2004, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(r2sym::BackwardConditionSummary { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }), + likely.clone(), + ), + crate::test_control_fact( + 0x2008, + r2sym::SymbolicReachabilityStatus::Unreachable, + None, + Some("!(x == 0)"), + None, + likely.clone(), + ), + ], + Vec::new(), + ); + ctx.inputs.semantic_artifact = Some(crate::leaked_test_semantic_artifact( + crate::test_native_semantic_artifact( + r2sym::RefinementStage::Compiled, + r2sym::ArtifactGranularity::Regioned, + r2sym::SliceClass::Worker, + false, + Vec::new(), + vec![region], + ), + )); + + let structurer = ControlFlowStructurer::new(&func, &ctx); + assert_eq!( + structurer.symbolic_actionable_reachable_target(0x2000), + Some(0x2004) + ); + } + + #[test] + fn symbolic_actionable_if_accepts_reachable_target_inside_region() { + let func = function_with_return_blocks(&[0x2000, 0x2004, 0x2008]); + let mut ctx = FoldingContext::new(64); + let likely = + r2sym::SemanticEvidence::likely(r2sym::SemanticEvidenceReason::PartialPathCoverage); + let region = crate::test_semantic_region( + 0x2000, + BTreeSet::from([0x2004, 0x2008]), + vec![ + crate::test_control_fact( + 0x2008, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(r2sym::BackwardConditionSummary { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }), + likely.clone(), + ), + crate::test_control_fact( + 0x2004, + r2sym::SymbolicReachabilityStatus::Unreachable, + None, + Some("!(x == 0)"), + Some(r2sym::BackwardConditionSummary { + simplified: "!(x == 0)".to_string(), + terms: vec!["!(x == 0)".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }), + likely.clone(), + ), + ], + Vec::new(), + ); + ctx.inputs.semantic_artifact = Some(crate::leaked_test_semantic_artifact( + crate::test_native_semantic_artifact( + r2sym::RefinementStage::Compiled, + r2sym::ArtifactGranularity::Regioned, + r2sym::SliceClass::Worker, + false, + Vec::new(), + vec![region], + ), + )); + + let mut structurer = ControlFlowStructurer::new(&func, &ctx); + let then_region = crate::region::Region::Sequence(vec![ + crate::region::Region::Block(0x2004), + crate::region::Region::Block(0x2008), + ]); + + let rewritten = + structurer.try_structure_symbolic_actionable_if(0x2000, &then_region, None, None); + assert!( + rewritten.is_some(), + "reachable targets inside the structured region should still drive symbolic if shaping" + ); + } + + #[test] + fn semantic_worker_island_structuring_builds_real_if_statement() { + let func = function_with_conditional_return_blocks(0x2000, 0x2004, 0x2008); + let mut ctx = FoldingContext::new(64); + let region = r2sym::SemanticRegion { + anchor: 0x2000, + frontier: std::collections::BTreeSet::from([0x2004, 0x2008]), + control: vec![ + r2sym::Judged::new( + r2sym::ControlFact { + target: 0x2004, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("x == 0".to_string()), + compiled: Some(r2sym::BackwardConditionSummary { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: vec![r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: r2sym::SemanticEvidence::exact(), + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x0:8".to_string()), + exact_value: true, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + r2sym::SemanticEvidence::exact(), + ), + r2sym::Judged::new( + r2sym::ControlFact { + target: 0x2008, + status: r2sym::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + condition: Some("!(x == 0)".to_string()), + compiled: None, + }, + r2sym::SemanticEvidence::exact(), + ), + ], + memory: vec![r2sym::Judged::new( + r2sym::MemoryFact { + term: r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: r2sym::SemanticEvidence::exact(), + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x0:8".to_string()), + exact_value: true, + }, + }, + r2sym::SemanticEvidence::exact(), + )], + pre: Vec::new(), + post: Vec::new(), + targets: vec![ + r2sym::Judged::new( + r2sym::TargetFact { + target: 0x2004, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + }, + r2sym::SemanticEvidence::exact(), + ), + r2sym::Judged::new( + r2sym::TargetFact { + target: 0x2008, + status: r2sym::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + }, + r2sym::SemanticEvidence::exact(), + ), + ], + }; + let artifact = r2sym::SemanticArtifact { + stage: r2sym::RefinementStage::Compiled, + granularity: r2sym::ArtifactGranularity::Regioned, + execution: r2sym::ExecutionModel::Native, + body: r2sym::SemanticArtifactBody::Native(r2sym::NativeArtifactBody { + summary: r2sym::NativeFunctionSummary { + slice_class: r2sym::SliceClass::Worker, + closure_functions: 1, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: Default::default(), + }, + regions: BTreeMap::from([(region.key(), region)]), + }), + diagnostics: r2sym::SemanticArtifactDiagnostics { + branches_evaluated: 1, + branches_pruned: 1, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: true, + residual_reasons: vec![r2sym::ResidualReason::LargeCfg], + ambiguous_targets: Vec::new(), + cache_hit: false, + }, + }; + ctx.inputs.semantic_artifact = Some(Box::leak(Box::new(artifact))); + + let mut structurer = ControlFlowStructurer::new(&func, &ctx); + let rewritten = structurer + .structure_semantic_worker_islands(4) + .expect("semantic worker structuring"); + assert!( + matches!(rewritten, CStmt::If { .. } | CStmt::Block(_)), + "expected structured semantic worker output, got {rewritten:?}" + ); + } + + #[test] + fn semantic_worker_structuring_negates_false_reachable_branch() { + let func = function_with_conditional_return_blocks(0x2000, 0x2008, 0x2004); + let mut ctx = FoldingContext::new(64); + let likely = + r2sym::SemanticEvidence::likely(r2sym::SemanticEvidenceReason::DerivedFromRanking); + let region = r2sym::SemanticRegion { + anchor: 0x2000, + frontier: std::collections::BTreeSet::from([0x2004, 0x2008]), + control: vec![ + r2sym::Judged::new( + r2sym::ControlFact { + target: 0x2008, + status: r2sym::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(true), + condition: Some("x == 0".to_string()), + compiled: None, + }, + likely.clone(), + ), + r2sym::Judged::new( + r2sym::ControlFact { + target: 0x2004, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(false), + condition: Some("!(x == 0)".to_string()), + compiled: Some(r2sym::BackwardConditionSummary { + simplified: "!(x == 0)".to_string(), + terms: vec!["!(x == 0)".to_string()], + memory_terms: vec![r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: likely.clone(), + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x1:8".to_string()), + exact_value: true, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }), + }, + likely.clone(), + ), + ], + memory: vec![r2sym::Judged::new( + r2sym::MemoryFact { + term: r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: likely.clone(), + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x1:8".to_string()), + exact_value: true, + }, + }, + likely.clone(), + )], + pre: Vec::new(), + post: Vec::new(), + targets: vec![ + r2sym::Judged::new( + r2sym::TargetFact { + target: 0x2008, + status: r2sym::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(true), + }, + likely.clone(), + ), + r2sym::Judged::new( + r2sym::TargetFact { + target: 0x2004, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(false), + }, + likely.clone(), + ), + ], + }; + let artifact = r2sym::SemanticArtifact { + stage: r2sym::RefinementStage::Compiled, + granularity: r2sym::ArtifactGranularity::Regioned, + execution: r2sym::ExecutionModel::Native, + body: r2sym::SemanticArtifactBody::Native(r2sym::NativeArtifactBody { + summary: r2sym::NativeFunctionSummary { + slice_class: r2sym::SliceClass::Worker, + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: Default::default(), + }, + regions: BTreeMap::from([(region.key(), region)]), + }), + diagnostics: r2sym::SemanticArtifactDiagnostics { + branches_evaluated: 1, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: true, + residual_reasons: vec![r2sym::ResidualReason::LargeCfg], + ambiguous_targets: Vec::new(), + cache_hit: false, + }, + }; + ctx.inputs.semantic_artifact = Some(Box::leak(Box::new(artifact))); + + let mut structurer = ControlFlowStructurer::new(&func, &ctx); + let rewritten = structurer + .structure_semantic_worker_islands(4) + .expect("semantic worker structuring"); + let if_stmt = trailing_if_stmt(&rewritten); + let CStmt::If { cond, .. } = if_stmt else { + panic!("expected if statement, got {if_stmt:?}"); + }; + assert!( + matches!( + cond, + CExpr::Binary { + op: BinaryOp::Ne, + .. + } | CExpr::Unary { + op: UnaryOp::Not, + .. + } + ), + "reachable false successor must negate the branch condition, got {cond:?}" + ); + } + + #[test] + fn semantic_worker_structuring_keeps_multi_block_target_suffix() { + let func = function_with_multi_block_true_arm(0x2000, 0x2008, 0x200c, 0x2004); + let mut ctx = FoldingContext::new(64); + let region = r2sym::SemanticRegion { + anchor: 0x2000, + frontier: BTreeSet::from([0x2008, 0x2004]), + control: vec![ + r2sym::Judged::new( + r2sym::ControlFact { + target: 0x2008, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("x == 0".to_string()), + compiled: Some(r2sym::BackwardConditionSummary { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: vec![r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: r2sym::SemanticEvidence::exact(), + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x0:8".to_string()), + exact_value: true, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + r2sym::SemanticEvidence::exact(), + ), + r2sym::Judged::new( + r2sym::ControlFact { + target: 0x2004, + status: r2sym::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + condition: Some("!(x == 0)".to_string()), + compiled: None, + }, + r2sym::SemanticEvidence::exact(), + ), + ], + memory: vec![r2sym::Judged::new( + r2sym::MemoryFact { + term: r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: r2sym::SemanticEvidence::exact(), + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x0:8".to_string()), + exact_value: true, + }, + }, + r2sym::SemanticEvidence::exact(), + )], + pre: Vec::new(), + post: Vec::new(), + targets: vec![ + r2sym::Judged::new( + r2sym::TargetFact { + target: 0x2008, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + }, + r2sym::SemanticEvidence::exact(), + ), + r2sym::Judged::new( + r2sym::TargetFact { + target: 0x2004, + status: r2sym::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + }, + r2sym::SemanticEvidence::exact(), + ), + ], + }; + let artifact = r2sym::SemanticArtifact { + stage: r2sym::RefinementStage::Compiled, + granularity: r2sym::ArtifactGranularity::Regioned, + execution: r2sym::ExecutionModel::Native, + body: r2sym::SemanticArtifactBody::Native(r2sym::NativeArtifactBody { + summary: r2sym::NativeFunctionSummary { + slice_class: r2sym::SliceClass::Worker, + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: Default::default(), + }, + regions: BTreeMap::from([(region.key(), region)]), + }), + diagnostics: r2sym::SemanticArtifactDiagnostics { + branches_evaluated: 1, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: true, + residual_reasons: vec![r2sym::ResidualReason::LargeCfg], + ambiguous_targets: Vec::new(), + cache_hit: false, + }, + }; + ctx.inputs.semantic_artifact = Some(Box::leak(Box::new(artifact))); + + let mut structurer = ControlFlowStructurer::new(&func, &ctx); + let rewritten = structurer + .structure_semantic_worker_islands(4) + .expect("semantic worker structuring"); + let if_stmt = trailing_if_stmt(&rewritten); + let CStmt::If { then_body, .. } = if_stmt else { + panic!("expected if statement, got {if_stmt:?}"); + }; + assert!( + ControlFlowStructurer::stmt_guarantees_termination(then_body.as_ref()), + "structured semantic worker arm should keep the full target suffix, got {then_body:?}" + ); + } } diff --git a/crates/r2sleigh-cli/src/main.rs b/crates/r2sleigh-cli/src/main.rs index a304a70..4eb656b 100644 --- a/crates/r2sleigh-cli/src/main.rs +++ b/crates/r2sleigh-cli/src/main.rs @@ -63,7 +63,7 @@ enum Commands { /// Generate a test architecture specification TestArch { - /// Architecture name (x86-64, arm, riscv64, riscv32) + /// Architecture name (x86-64, arm, mips32be, mips32le, riscv64, riscv32) arch: String, /// Output r2il binary file @@ -367,6 +367,26 @@ fn cmd_test_arch(arch: &str, output: Option<&PathBuf>) -> Result<(), String> { println!("Generating ARM test specification..."); create_arm_spec() } + #[cfg(feature = "mips")] + "mips" | "mips32" | "mips32be" | "mipsbe" | "mipseb" => { + println!("Generating MIPS32 big-endian test specification..."); + build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS32BE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32be", + ) + .map_err(|e| e.to_string())? + } + #[cfg(feature = "mips")] + "mipsel" | "mips32le" | "mips32el" => { + println!("Generating MIPS32 little-endian test specification..."); + build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS32LE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32le", + ) + .map_err(|e| e.to_string())? + } "riscv64" | "rv64" | "rv64gc" => { println!("Generating RISC-V RV64 test specification..."); create_riscv64_spec() @@ -377,7 +397,7 @@ fn cmd_test_arch(arch: &str, output: Option<&PathBuf>) -> Result<(), String> { } _ => { return Err(format!( - "Unknown architecture: {}. Supported: x86-64, arm, riscv64, riscv32", + "Unknown architecture: {}. Supported: x86-64, arm, mips32be, mips32le, riscv64, riscv32", arch )); } @@ -676,6 +696,74 @@ fn get_disassembler_with_spec(arch: &str) -> Result<(Disassembler, r2il::ArchSpe disasm.set_userop_map(userop_map_for_arch("arm")); Ok((disasm, spec)) } + #[cfg(feature = "mips")] + "mips" | "mips32" | "mips32be" | "mipsbe" | "mipseb" => { + let spec = build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS32BE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32be", + ) + .map_err(|e| e.to_string())?; + let mut disasm = Disassembler::from_sla( + sleigh_config::processor_mips::SLA_MIPS32BE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32be", + ) + .map_err(|e| e.to_string())?; + disasm.set_userop_map(userop_map_for_arch("mips32be")); + Ok((disasm, spec)) + } + #[cfg(feature = "mips")] + "mipsel" | "mips32le" | "mips32el" => { + let spec = build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS32LE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32le", + ) + .map_err(|e| e.to_string())?; + let mut disasm = Disassembler::from_sla( + sleigh_config::processor_mips::SLA_MIPS32LE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32le", + ) + .map_err(|e| e.to_string())?; + disasm.set_userop_map(userop_map_for_arch("mips32le")); + Ok((disasm, spec)) + } + #[cfg(feature = "mips")] + "mips64" | "mips64be" => { + let spec = build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS64BE, + sleigh_config::processor_mips::PSPEC_MIPS64, + "mips64be", + ) + .map_err(|e| e.to_string())?; + let mut disasm = Disassembler::from_sla( + sleigh_config::processor_mips::SLA_MIPS64BE, + sleigh_config::processor_mips::PSPEC_MIPS64, + "mips64be", + ) + .map_err(|e| e.to_string())?; + disasm.set_userop_map(userop_map_for_arch("mips64be")); + Ok((disasm, spec)) + } + #[cfg(feature = "mips")] + "mips64el" | "mips64le" => { + let spec = build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS64LE, + sleigh_config::processor_mips::PSPEC_MIPS64, + "mips64le", + ) + .map_err(|e| e.to_string())?; + let mut disasm = Disassembler::from_sla( + sleigh_config::processor_mips::SLA_MIPS64LE, + sleigh_config::processor_mips::PSPEC_MIPS64, + "mips64le", + ) + .map_err(|e| e.to_string())?; + disasm.set_userop_map(userop_map_for_arch("mips64le")); + Ok((disasm, spec)) + } #[cfg(feature = "riscv")] "riscv64" | "rv64" | "rv64gc" => { let spec = build_arch_spec( @@ -716,12 +804,14 @@ fn get_disassembler_with_spec(arch: &str) -> Result<(Disassembler, r2il::ArchSpe supported.extend(["x86-64", "x86"]); #[cfg(feature = "arm")] supported.push("arm"); + #[cfg(feature = "mips")] + supported.extend(["mips32be", "mips32le", "mips64be", "mips64le"]); #[cfg(feature = "riscv")] supported.extend(["riscv64", "riscv32"]); if supported.is_empty() { Err( - "No architectures enabled. Build with --features x86, arm, or riscv" + "No architectures enabled. Build with --features x86, arm, mips, or riscv" .to_string(), ) } else { diff --git a/crates/r2sleigh-lift/src/userops.rs b/crates/r2sleigh-lift/src/userops.rs index d15e22e..55eb9c5 100644 --- a/crates/r2sleigh-lift/src/userops.rs +++ b/crates/r2sleigh-lift/src/userops.rs @@ -12,6 +12,10 @@ fn arch_to_slaspec(arch: &str) -> Option<(&'static str, &'static str)> { "x86" | "x86-32" | "i386" | "i686" => Some(("x86", "x86.slaspec")), "arm" | "arm32" | "arm-le" => Some(("ARM", "ARM8_le.slaspec")), "aarch64" | "arm64" | "arm64e" => Some(("AARCH64", "AARCH64_AppleSilicon.slaspec")), + "mips" | "mips32" | "mips32be" | "mipsbe" | "mipseb" => Some(("MIPS", "mips32be.slaspec")), + "mipsel" | "mips32le" | "mips32el" => Some(("MIPS", "mips32le.slaspec")), + "mips64" | "mips64be" => Some(("MIPS", "mips64be.slaspec")), + "mips64el" | "mips64le" => Some(("MIPS", "mips64le.slaspec")), "riscv64" | "rv64" | "rv64gc" => Some(("RISCV", "riscv.lp64d.slaspec")), "riscv32" | "rv32" | "rv32gc" => Some(("RISCV", "riscv.ilp32d.slaspec")), _ => None, diff --git a/crates/r2ssa/src/assumption.rs b/crates/r2ssa/src/assumption.rs new file mode 100644 index 0000000..f7bd406 --- /dev/null +++ b/crates/r2ssa/src/assumption.rs @@ -0,0 +1,225 @@ +use serde::{Deserialize, Serialize}; + +use crate::PredicateId; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AssumptionScope { + #[default] + Function, + Query, + Replay, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AssumptionProvenance { + User, + #[default] + ImportedContext, + Replay, + Derived, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AssumptionSubject { + Parameter { + index: usize, + }, + Register { + name: String, + }, + StackSlot { + base: String, + offset: i64, + }, + Predicate { + predicate: PredicateId, + block_addr: u64, + predecessor: Option, + }, + Target { + addr: u64, + }, + MemoryWindow { + addr: u64, + size: u32, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AssumptionValue { + Constant { + value: u64, + }, + Range { + min: u64, + max: u64, + }, + FiniteSet { + values: Vec, + }, + EnumDomain { + name: Option, + values: Vec, + }, + TypeHint { + ty: String, + }, + Branch { + truth: bool, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AnalysisAssumption { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub id: Option, + pub subject: AssumptionSubject, + pub value: AssumptionValue, + #[serde(default)] + pub scope: AssumptionScope, + #[serde(default)] + pub provenance: AssumptionProvenance, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AnalysisAssumptionConflict { + pub assumption: AnalysisAssumption, + pub reason: String, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct AssumptionUsageReport { + pub applied: Vec, + pub ignored: Vec, + pub conflicts: Vec, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct AssumptionSet { + #[serde(default)] + pub items: Vec, +} + +impl AssumptionSet { + pub fn new(items: Vec) -> Self { + Self { items } + } + + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } + + pub fn iter(&self) -> impl Iterator { + self.items.iter() + } + + pub fn push(&mut self, assumption: AnalysisAssumption) { + self.items.push(assumption); + } + + pub fn extend(&mut self, iter: I) + where + I: IntoIterator, + { + self.items.extend(iter); + } + + pub fn type_hints_for_parameter(&self, index: usize) -> impl Iterator { + self.items.iter().filter_map(move |assumption| { + match (&assumption.subject, &assumption.value) { + ( + AssumptionSubject::Parameter { index: subject }, + AssumptionValue::TypeHint { ty }, + ) if *subject == index => Some(ty.as_str()), + _ => None, + } + }) + } + + pub fn type_hints_for_register<'a>(&'a self, reg: &'a str) -> impl Iterator { + self.items.iter().filter_map(move |assumption| { + match (&assumption.subject, &assumption.value) { + (AssumptionSubject::Register { name }, AssumptionValue::TypeHint { ty }) + if name.eq_ignore_ascii_case(reg) => + { + Some(ty.as_str()) + } + _ => None, + } + }) + } + + pub fn branch_truth_for_predicate(&self, predicate: PredicateId) -> Option { + self.items.iter().find_map( + |assumption| match (&assumption.subject, &assumption.value) { + ( + AssumptionSubject::Predicate { + predicate: subject, .. + }, + AssumptionValue::Branch { truth }, + ) if *subject == predicate => Some(*truth), + _ => None, + }, + ) + } +} + +impl AssumptionUsageReport { + pub fn is_empty(&self) -> bool { + self.applied.is_empty() && self.ignored.is_empty() && self.conflicts.is_empty() + } + + pub fn mark_applied(&mut self, assumption: &AnalysisAssumption) { + self.ignored.retain(|item| item != assumption); + self.conflicts.retain(|item| item.assumption != *assumption); + if !self.applied.iter().any(|item| item == assumption) { + self.applied.push(assumption.clone()); + } + } + + pub fn mark_ignored(&mut self, assumption: &AnalysisAssumption) { + if self.applied.iter().any(|item| item == assumption) + || self + .conflicts + .iter() + .any(|item| item.assumption == *assumption) + { + return; + } + if !self.ignored.iter().any(|item| item == assumption) { + self.ignored.push(assumption.clone()); + } + } + + pub fn mark_conflict(&mut self, assumption: &AnalysisAssumption, reason: impl Into) { + self.applied.retain(|item| item != assumption); + self.ignored.retain(|item| item != assumption); + let reason = reason.into(); + if !self + .conflicts + .iter() + .any(|item| item.assumption == *assumption && item.reason == reason) + { + self.conflicts.push(AnalysisAssumptionConflict { + assumption: assumption.clone(), + reason, + }); + } + } + + pub fn extend(&mut self, other: &Self) { + for assumption in &other.applied { + self.mark_applied(assumption); + } + for assumption in &other.ignored { + self.mark_ignored(assumption); + } + for conflict in &other.conflicts { + self.mark_conflict(&conflict.assumption, conflict.reason.clone()); + } + } +} diff --git a/crates/r2ssa/src/cfg.rs b/crates/r2ssa/src/cfg.rs index ecc20bf..2fd4c31 100644 --- a/crates/r2ssa/src/cfg.rs +++ b/crates/r2ssa/src/cfg.rs @@ -3,7 +3,7 @@ //! This module provides a CFG data structure built from r2il blocks, //! which is the foundation for inter-procedural SSA analysis. -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap, HashSet}; use petgraph::Direction; use petgraph::graph::{DiGraph, NodeIndex}; @@ -203,6 +203,136 @@ impl BasicBlock { } } +fn op_direct_control_target(op: &R2ILOp) -> Option { + match op { + R2ILOp::Branch { target } | R2ILOp::CBranch { target, .. } => { + BasicBlock::extract_const_addr(target) + } + _ => None, + } +} + +fn op_terminates_basic_block(op: &R2ILOp) -> bool { + matches!( + op, + R2ILOp::Branch { .. } + | R2ILOp::CBranch { .. } + | R2ILOp::BranchInd { .. } + | R2ILOp::Return { .. } + ) +} + +fn op_instruction_addr(block: &R2ILBlock, op_idx: usize) -> Option { + block + .op_metadata + .get(&op_idx) + .and_then(|metadata| metadata.instruction_addr) +} + +fn split_internal_control_flow_targets(block: &R2ILBlock) -> Vec { + if block.switch_info.is_some() || block.ops.is_empty() { + return vec![block.clone()]; + } + + let block_end = block.addr.saturating_add(block.size as u64); + if block_end <= block.addr { + return vec![block.clone()]; + } + + let instruction_addrs = block + .op_metadata + .values() + .filter_map(|metadata| metadata.instruction_addr) + .collect::>(); + if instruction_addrs.is_empty() { + return vec![block.clone()]; + } + + let mut op_instruction_addrs = Vec::with_capacity(block.ops.len()); + let mut last_instruction_addr = block.addr; + for op_idx in 0..block.ops.len() { + if let Some(instruction_addr) = op_instruction_addr(block, op_idx) { + last_instruction_addr = instruction_addr; + } + op_instruction_addrs.push(last_instruction_addr); + } + + let mut split_points = BTreeSet::new(); + for (op_idx, op) in block.ops.iter().enumerate() { + if let Some(target) = op_direct_control_target(op) + && target > block.addr + && target < block_end + && instruction_addrs.contains(&target) + { + split_points.insert(target); + } + + if op_terminates_basic_block(op) { + let current_addr = op_instruction_addrs + .get(op_idx) + .copied() + .unwrap_or(block.addr); + let fallthrough = op_instruction_addrs + .iter() + .skip(op_idx + 1) + .copied() + .find(|addr| *addr > current_addr); + if let Some(fallthrough) = fallthrough + && fallthrough > block.addr + && fallthrough < block_end + && instruction_addrs.contains(&fallthrough) + { + split_points.insert(fallthrough); + } + } + } + if split_points.is_empty() { + return vec![block.clone()]; + } + + let mut starts = Vec::with_capacity(split_points.len() + 1); + starts.push(block.addr); + starts.extend(split_points); + + let mut chunks = starts + .iter() + .enumerate() + .map(|(idx, &start)| { + let end = starts.get(idx + 1).copied().unwrap_or(block_end); + R2ILBlock { + addr: start, + size: end.saturating_sub(start).min(u32::MAX as u64) as u32, + ops: Vec::new(), + switch_info: None, + op_metadata: Default::default(), + } + }) + .collect::>(); + + for (op_idx, op) in block.ops.iter().cloned().enumerate() { + let instruction_addr = op_instruction_addrs + .get(op_idx) + .copied() + .unwrap_or(block.addr); + let chunk_idx = starts + .partition_point(|start| *start <= instruction_addr) + .saturating_sub(1) + .min(chunks.len().saturating_sub(1)); + let next_op_idx = chunks[chunk_idx].ops.len(); + chunks[chunk_idx].ops.push(op); + if let Some(metadata) = block.op_metadata.get(&op_idx) { + chunks[chunk_idx] + .op_metadata + .insert(next_op_idx, metadata.clone()); + } + } + + chunks + .into_iter() + .filter(|chunk| !chunk.ops.is_empty()) + .collect() +} + /// A Control Flow Graph for a function. #[derive(Debug, Clone)] pub struct CFG { @@ -257,8 +387,13 @@ impl CFG { let entry = blocks[0].addr; let mut cfg = Self::new(entry); + let normalized_blocks = blocks + .iter() + .flat_map(split_internal_control_flow_targets) + .collect::>(); + // First pass: add all blocks as nodes - for block in blocks { + for block in &normalized_blocks { let bb = BasicBlock::from_r2il(block); cfg.add_block(bb); } @@ -301,11 +436,17 @@ impl CFG { true_target, false_target, } => { - if let Some(&true_idx) = self.addr_to_node.get(&true_target) { - self.graph.add_edge(node_idx, true_idx, CFGEdge::True); - } - if let Some(&false_idx) = self.addr_to_node.get(&false_target) { - self.graph.add_edge(node_idx, false_idx, CFGEdge::False); + if true_target == false_target { + if let Some(&target_idx) = self.addr_to_node.get(&true_target) { + self.graph.add_edge(node_idx, target_idx, CFGEdge::Normal); + } + } else { + if let Some(&true_idx) = self.addr_to_node.get(&true_target) { + self.graph.add_edge(node_idx, true_idx, CFGEdge::True); + } + if let Some(&false_idx) = self.addr_to_node.get(&false_target) { + self.graph.add_edge(node_idx, false_idx, CFGEdge::False); + } } } BlockTerminator::Call { fallthrough, .. } @@ -526,7 +667,7 @@ impl CFG { #[cfg(test)] mod tests { use super::*; - use r2il::{R2ILBlock, R2ILOp, SpaceId, Varnode}; + use r2il::{OpMetadata, R2ILBlock, R2ILOp, SpaceId, Varnode}; fn make_const(val: u64, size: u32) -> Varnode { Varnode { @@ -887,4 +1028,124 @@ mod tests { vec![0x1000, 0x1004, 0x1008] ); } + + #[test] + fn test_internal_pcode_branch_target_splits_block() { + let mut op_metadata = std::collections::BTreeMap::new(); + for op_idx in 0..3 { + op_metadata.insert( + op_idx, + OpMetadata { + instruction_addr: Some(0x1000), + ..Default::default() + }, + ); + } + op_metadata.insert( + 3, + OpMetadata { + instruction_addr: Some(0x1004), + ..Default::default() + }, + ); + + let blocks = vec![R2ILBlock { + addr: 0x1000, + size: 8, + ops: vec![ + R2ILOp::Copy { + dst: make_ram(0x3000, 8), + src: make_const(1, 8), + }, + R2ILOp::CBranch { + target: make_ram(0x1004, 8), + cond: make_const(1, 1), + }, + R2ILOp::Copy { + dst: make_ram(0x3008, 8), + src: make_const(2, 8), + }, + R2ILOp::Return { + target: make_ram(0, 8), + }, + ], + switch_info: None, + op_metadata, + }]; + + let cfg = CFG::from_blocks(&blocks).expect("cfg"); + assert_eq!(cfg.block_addrs().collect::>(), vec![0x1000, 0x1004]); + assert_eq!(cfg.get_block(0x1000).expect("entry").ops.len(), 3); + assert_eq!(cfg.get_block(0x1004).expect("target").ops.len(), 1); + assert_eq!(cfg.successors(0x1000), vec![0x1004]); + assert_eq!(cfg.predecessors(0x1004), vec![0x1000]); + } + + #[test] + fn test_internal_conditional_fallthrough_splits_block() { + let mut op_metadata = std::collections::BTreeMap::new(); + for (op_idx, instruction_addr) in [ + (0, 0x1000), + (1, 0x1004), + (2, 0x1008), + (3, 0x100c), + (4, 0x1010), + (5, 0x1014), + ] { + op_metadata.insert( + op_idx, + OpMetadata { + instruction_addr: Some(instruction_addr), + ..Default::default() + }, + ); + } + + let blocks = vec![R2ILBlock { + addr: 0x1000, + size: 0x18, + ops: vec![ + R2ILOp::Copy { + dst: make_ram(0x3000, 8), + src: make_const(1, 8), + }, + R2ILOp::CBranch { + target: make_ram(0x1010, 8), + cond: make_const(1, 1), + }, + R2ILOp::Copy { + dst: make_ram(0x3000, 8), + src: make_const(0, 8), + }, + R2ILOp::Branch { + target: make_ram(0x1014, 8), + }, + R2ILOp::Copy { + dst: make_ram(0x3000, 8), + src: make_const(2, 8), + }, + R2ILOp::Return { + target: make_ram(0, 8), + }, + ], + switch_info: None, + op_metadata, + }]; + + let cfg = CFG::from_blocks(&blocks).expect("cfg"); + assert_eq!( + cfg.block_addrs().collect::>(), + vec![0x1000, 0x1008, 0x1010, 0x1014] + ); + assert_eq!( + cfg.get_block(0x1000).expect("entry").terminator, + BlockTerminator::ConditionalBranch { + true_target: 0x1010, + false_target: 0x1008 + } + ); + assert_eq!(cfg.successors(0x1000), vec![0x1010, 0x1008]); + assert_eq!(cfg.successors(0x1008), vec![0x1014]); + assert_eq!(cfg.successors(0x1010), vec![0x1014]); + } } diff --git a/crates/r2ssa/src/function.rs b/crates/r2ssa/src/function.rs index 6277cf1..2bbfc58 100644 --- a/crates/r2ssa/src/function.rs +++ b/crates/r2ssa/src/function.rs @@ -11,6 +11,7 @@ use std::sync::{Arc, OnceLock, RwLock}; use r2il::{ArchSpec, R2ILBlock}; use serde::{Deserialize, Serialize}; +use crate::AssumptionSet; use crate::block::SSABlock as LocalSSABlock; use crate::cfg::{CFG, CFGEdge}; use crate::defuse::{BackwardSlice, SliceOpRef, backward_slice_from_op, backward_slice_from_var}; @@ -156,6 +157,20 @@ impl SsaArtifact { &self.facts } + pub fn with_assumptions(&self, assumptions: &AssumptionSet) -> Self { + let facts = PreparedFunctionFacts::collect_with_assumptions( + &self.function, + &self.graph, + assumptions, + ); + Self { + function: self.function.clone(), + graph: self.graph.clone(), + mode: self.mode, + facts, + } + } + pub fn objects(&self) -> &ObjectModel { &self.facts.objects } @@ -172,6 +187,14 @@ impl SsaArtifact { &self.facts.call_sites } + pub fn resolved_call_target(&self, call: &crate::semantic::CallSiteFact) -> Option { + call.direct_target.or_else(|| { + let value_id = canonical_root_value_id(self, call.target); + let var = self.value_var(value_id)?; + parse_literal_value_name(&var.name) + }) + } + pub fn value_var(&self, value_id: crate::graph::ValueId) -> Option<&SSAVar> { self.graph.value(value_id).map(|value| &value.var) } @@ -225,6 +248,58 @@ impl SsaArtifact { } } +fn canonical_root_value_id( + prepared: &SsaArtifact, + value_id: crate::graph::ValueId, +) -> crate::graph::ValueId { + let Some(facts) = prepared.function().decompile_prep_facts() else { + return value_id; + }; + let Some(start) = prepared.value_var(value_id) else { + return value_id; + }; + let mut current = start.clone(); + let mut current_id = value_id; + for _ in 0..32 { + let Some(next) = facts.canonical_root_of(¤t) else { + break; + }; + if next == ¤t { + break; + } + let Some(next_id) = prepared.graph().value_id_for_var(next) else { + break; + }; + current = next.clone(); + current_id = next_id; + } + current_id +} + +fn parse_literal_value_name(name: &str) -> Option { + let value_str = if let Some(value) = name.strip_prefix("const:") { + value + } else if let Some(value) = name.strip_prefix("ram:") { + value + } else { + return None; + }; + let value_str = value_str.split('_').next().unwrap_or(value_str); + if let Some(dec) = value_str + .strip_prefix("0d") + .or_else(|| value_str.strip_prefix("0D")) + { + return dec.parse().ok(); + } + if let Some(hex) = value_str + .strip_prefix("0x") + .or_else(|| value_str.strip_prefix("0X")) + { + return u64::from_str_radix(hex, 16).ok(); + } + value_str.parse().ok() +} + impl Deref for SsaArtifact { type Target = SSAFunction; @@ -2858,6 +2933,51 @@ mod tests { assert_eq!(call.fallthrough, Some(0x1304)); } + #[test] + fn symbolic_function_ssa_recovers_indirect_call_target_from_copied_ram_literal() { + let tmp = Varnode { + space: SpaceId::Unique, + offset: 0x10, + size: 8, + meta: None, + }; + let blocks = vec![ + R2ILBlock { + addr: 0x1310, + size: 4, + ops: vec![ + R2ILOp::Copy { + dst: tmp.clone(), + src: make_ram(0x1400a6010, 8), + }, + R2ILOp::CallInd { target: tmp }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1314, + size: 4, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + + let prepared = + SsaArtifact::for_symbolic(&blocks, None).expect("symbolic prepared SSA should build"); + let call = prepared + .call_sites() + .by_id + .values() + .next() + .expect("call site fact"); + assert_eq!(call.direct_target, Some(0x1400a6010)); + assert_eq!(call.fallthrough, Some(0x1314)); + } + #[test] fn prepared_function_ssa_builds_memory_phis_per_object() { let blocks = vec![ diff --git a/crates/r2ssa/src/interproc.rs b/crates/r2ssa/src/interproc.rs index 1616f4a..6217b70 100644 --- a/crates/r2ssa/src/interproc.rs +++ b/crates/r2ssa/src/interproc.rs @@ -322,6 +322,18 @@ impl AbiProfile { } } + pub fn windows_x64() -> Self { + Self::new( + vec![ + ("rcx", 8, &["ecx", "cx", "cl"][..]), + ("rdx", 8, &["edx", "dx", "dl"][..]), + ("r8", 8, &["r8d", "r8w", "r8b"][..]), + ("r9", 8, &["r9d", "r9w", "r9b"][..]), + ], + &[("rax", &["eax", "ax", "al"][..])], + ) + } + fn new( args: Vec<(&'static str, u32, &'static [&'static str])>, rets: &[(&'static str, &'static [&'static str])], @@ -368,6 +380,13 @@ enum SummaryOperand { Unknown, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum CallArgObservation { + Arg(usize), + Const(u64), + Unknown, +} + #[derive(Debug, Clone, PartialEq, Eq)] enum SummaryValueObservation { Arg(usize), @@ -731,6 +750,9 @@ fn resolve_return_relation( observations: &[SummaryValueObservation], current: &BTreeMap, ) -> SummaryReturnRelation { + if observations.is_empty() { + return SummaryReturnRelation::Unknown; + } let mut relation: Option = None; for observation in observations { let next = match observation { @@ -766,7 +788,7 @@ fn resolve_return_relation( } } - relation.unwrap_or(SummaryReturnRelation::Void) + relation.unwrap_or(SummaryReturnRelation::Unknown) } fn collect_local_summary_facts(prepared: &SsaArtifact, abi: &AbiProfile) -> LocalSummaryFacts { @@ -981,34 +1003,46 @@ fn classify_memory_access_location_value( } let rooted = canonical_root_value(prepared, value_id); - if let Some(var) = prepared.value_var(rooted) { - if let Some(idx) = abi.arg_index_for_name(&var.name) { - return arg_location(idx, Some(0), Some(width)); - } - if let Some(address) = parse_const_name(&var.name) { - return global_location(address, Some(0), Some(width)); - } + let mut candidates = vec![value_id]; + if rooted != value_id { + candidates.push(rooted); } - if let Some(object_id) = prepared.objects().object_for_value(rooted) - && let Some(object) = prepared.objects().object(object_id) - { - match object.kind { - ObjectKind::Global { address, .. } => { + for candidate in &candidates { + if let Some(var) = prepared.value_var(*candidate) { + if let Some(idx) = abi.arg_index_for_name(&var.name) { + return arg_location(idx, Some(0), Some(width)); + } + if let Some(address) = parse_const_name(&var.name) { return global_location(address, Some(0), Some(width)); } - ObjectKind::HeapAlloc { .. } => { - return SummaryMemoryLocation { - region: SummaryMemoryRegion::HeapReturn, - range: exact_range(0, width), - }; + } + + if let Some(object_id) = prepared.objects().object_for_value(*candidate) + && let Some(object) = prepared.objects().object(object_id) + { + match object.kind { + ObjectKind::Global { address, .. } => { + return global_location(address, Some(0), Some(width)); + } + ObjectKind::HeapAlloc { .. } => { + return SummaryMemoryLocation { + region: SummaryMemoryRegion::HeapReturn, + range: exact_range(0, width), + }; + } + ObjectKind::EscapedUnknown => {} + _ => {} } - ObjectKind::EscapedUnknown => return unknown_location(), - _ => {} } } - let Some(def_inst) = prepared.graph().def_inst(rooted) else { + let Some((op_value_id, def_inst)) = candidates.iter().find_map(|candidate| { + prepared + .graph() + .def_inst(*candidate) + .map(|inst| (*candidate, inst)) + }) else { return unknown_location(); }; let Some(inst) = prepared.graph().inst(def_inst) else { @@ -1058,6 +1092,9 @@ fn classify_memory_access_location_value( AdditiveLocationCtx::new(width, depth + 1, -1, op), ) } + _ if op_value_id != value_id => { + classify_memory_access_location_value(prepared, abi, value_id, width, depth + 1) + } _ => unknown_location(), } } @@ -1094,12 +1131,8 @@ fn classify_memory_additive_location( right_id: ValueId, ctx: AdditiveLocationCtx, ) -> SummaryMemoryLocation { - let left_const = prepared - .value_var(canonical_root_value(prepared, left_id)) - .and_then(|var| parse_const_name(&var.name)); - let right_const = prepared - .value_var(canonical_root_value(prepared, right_id)) - .and_then(|var| parse_const_name(&var.name)); + let left_const = summary_const_value(prepared, abi, left_id, ctx.depth); + let right_const = summary_const_value(prepared, abi, right_id, ctx.depth); if let Some(k) = right_const { let mut base = @@ -1122,6 +1155,20 @@ fn classify_memory_additive_location( unknown_location() } +fn summary_const_value( + prepared: &SsaArtifact, + abi: &AbiProfile, + value_id: ValueId, + depth: u32, +) -> Option { + match classify_value_operand(prepared, abi, value_id, depth) { + SummaryOperand::Const(value) => Some(value), + _ => prepared + .value_var(canonical_root_value(prepared, value_id)) + .and_then(|var| parse_const_name(&var.name)), + } +} + fn record_arg_count_hint( prepared: &SsaArtifact, abi: &AbiProfile, @@ -1249,6 +1296,26 @@ fn collect_call_arg_state( by_call } +pub fn observe_call_arguments( + prepared: &SsaArtifact, + abi: &AbiProfile, +) -> BTreeMap> { + collect_call_arg_state(prepared, abi) + .into_iter() + .map(|(call_id, args)| { + let args = args + .into_iter() + .map(|arg| match arg { + SummaryOperand::Arg(idx) => CallArgObservation::Arg(idx), + SummaryOperand::Const(value) => CallArgObservation::Const(value), + SummaryOperand::Unknown => CallArgObservation::Unknown, + }) + .collect(); + (call_id, args) + }) + .collect() +} + fn merge_pred_states( in_states: &BTreeMap>, preds: &[u64], @@ -1631,6 +1698,18 @@ mod tests { arch } + fn windows_x64_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("rax", 0, 8)); + arch.add_register(RegisterDef::new("rcx", 8, 8)); + arch.add_register(RegisterDef::new("rdx", 16, 8)); + arch.add_register(RegisterDef::new("r8", 24, 8)); + arch.add_register(RegisterDef::new("r9", 32, 8)); + arch.add_register(RegisterDef::new("rip", 40, 8)); + arch + } + fn reg(offset: u64, size: u32) -> Varnode { Varnode { space: SpaceId::Register, @@ -1648,6 +1727,15 @@ mod tests { Varnode::constant(value, size) } + fn ram(offset: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Ram, + offset, + size, + meta: None, + } + } + fn block(addr: u64, ops: Vec) -> R2ILBlock { R2ILBlock { addr, @@ -1787,6 +1875,42 @@ mod tests { ); } + #[test] + fn branchind_trampoline_without_return_stays_unknown() { + let arch = x86_64_arch(); + let trampoline = SsaArtifact::for_decompile( + &[block( + 0x3500, + vec![R2ILOp::BranchInd { + target: ram(0x406050, 8), + }], + )], + Some(&arch), + ) + .expect("trampoline ssa") + .with_name("sym.imp.setlocale"); + + let set = solve_interproc_summary_set( + &[InterprocFunctionInput { + id: InterprocFunctionId(0x3500), + name: Some("sym.imp.setlocale".to_string()), + prepared: &trampoline, + }], + Some(&arch), + Some(InterprocFunctionId(0x3500)), + &BTreeMap::new(), + InterprocSolveConfig::default(), + ); + + assert_eq!( + set.summaries + .get(&InterprocFunctionId(0x3500)) + .expect("trampoline summary") + .return_relation, + SummaryReturnRelation::Unknown + ); + } + #[test] fn direct_pointer_load_marks_argument_read() { let arch = x86_64_arch(); @@ -1943,4 +2067,204 @@ mod tests { ) })); } + + #[test] + fn symbolic_store_plus_constant_preserves_arg_offset_range() { + let arch = x86_64_arch(); + let prepared = SsaArtifact::for_symbolic( + &[block( + 0x4300, + vec![ + R2ILOp::IntAdd { + dst: reg(0x80, 8), + a: reg(8, 8), + b: c(2, 8), + }, + R2ILOp::Store { + addr: reg(0x80, 8), + val: reg(16, 2), + space: SpaceId::Ram, + }, + R2ILOp::Return { target: c(0, 8) }, + ], + )], + Some(&arch), + ) + .expect("ssa"); + let abi = AbiProfile::from_arch(Some(&arch)); + let block = prepared.function().get_block(0x4300).expect("block"); + let SSAOp::Store { addr, val, .. } = &block.ops[1] else { + panic!("expected store"); + }; + let addr_id = prepared + .graph() + .value_id_for_var(addr) + .expect("store addr value id"); + let def_inst = prepared.graph().def_inst(addr_id).expect("store addr def"); + let inst = prepared.graph().inst(def_inst).expect("store addr inst"); + let [left_id, right_id] = match inst.inputs.as_slice() { + [left, right] => [*left, *right], + _ => panic!("expected additive store addr inputs"), + }; + let InstPayload::Op(op) = &inst.payload else { + panic!("expected op payload"); + }; + assert_eq!(summary_const_value(&prepared, &abi, right_id, 0), Some(2)); + assert_eq!( + classify_memory_access_location_value(&prepared, &abi, left_id, val.size, 0), + SummaryMemoryLocation { + region: SummaryMemoryRegion::Arg { index: 0 }, + range: exact_range(0, val.size), + } + ); + assert_eq!( + classify_memory_additive_location( + &prepared, + &abi, + left_id, + right_id, + AdditiveLocationCtx::new(val.size, 1, 1, op), + ), + SummaryMemoryLocation { + region: SummaryMemoryRegion::Arg { index: 0 }, + range: exact_range(2, val.size), + } + ); + assert_eq!( + classify_memory_access_location_value(&prepared, &abi, addr_id, val.size, 0), + SummaryMemoryLocation { + region: SummaryMemoryRegion::Arg { index: 0 }, + range: exact_range(2, val.size), + } + ); + let location = classify_memory_access_location(&prepared, &abi, addr, val.size); + assert_eq!( + location, + SummaryMemoryLocation { + region: SummaryMemoryRegion::Arg { index: 0 }, + range: exact_range(2, val.size), + }, + "store address should resolve to arg0+2, got {location:?}; addr={addr:?}; ops={:?}", + block.ops + ); + } + + #[test] + fn symbolic_store_minus_constant_preserves_arg_offset_range() { + let arch = x86_64_arch(); + let prepared = SsaArtifact::for_symbolic( + &[block( + 0x4310, + vec![ + R2ILOp::IntSub { + dst: reg(0x80, 8), + a: reg(8, 8), + b: c(1, 8), + }, + R2ILOp::Store { + addr: reg(0x80, 8), + val: reg(16, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { target: c(0, 8) }, + ], + )], + Some(&arch), + ) + .expect("ssa"); + let abi = AbiProfile::from_arch(Some(&arch)); + let block = prepared.function().get_block(0x4310).expect("block"); + let SSAOp::Store { addr, val, .. } = &block.ops[1] else { + panic!("expected store"); + }; + let addr_id = prepared + .graph() + .value_id_for_var(addr) + .expect("store addr value id"); + let def_inst = prepared.graph().def_inst(addr_id).expect("store addr def"); + let inst = prepared.graph().inst(def_inst).expect("store addr inst"); + let [left_id, right_id] = match inst.inputs.as_slice() { + [left, right] => [*left, *right], + _ => panic!("expected additive store addr inputs"), + }; + let InstPayload::Op(op) = &inst.payload else { + panic!("expected op payload"); + }; + assert_eq!(summary_const_value(&prepared, &abi, right_id, 0), Some(1)); + assert_eq!( + classify_memory_access_location_value(&prepared, &abi, left_id, val.size, 0), + SummaryMemoryLocation { + region: SummaryMemoryRegion::Arg { index: 0 }, + range: exact_range(0, val.size), + } + ); + assert_eq!( + classify_memory_additive_location( + &prepared, + &abi, + left_id, + right_id, + AdditiveLocationCtx::new(val.size, 1, -1, op), + ), + SummaryMemoryLocation { + region: SummaryMemoryRegion::Arg { index: 0 }, + range: exact_range(-1, val.size), + } + ); + assert_eq!( + classify_memory_access_location_value(&prepared, &abi, addr_id, val.size, 0), + SummaryMemoryLocation { + region: SummaryMemoryRegion::Arg { index: 0 }, + range: exact_range(-1, val.size), + } + ); + let location = classify_memory_access_location(&prepared, &abi, addr, val.size); + assert_eq!( + location, + SummaryMemoryLocation { + region: SummaryMemoryRegion::Arg { index: 0 }, + range: exact_range(-1, val.size), + }, + "store address should resolve to arg0-1, got {location:?}; addr={addr:?}; ops={:?}", + block.ops + ); + } + + #[test] + fn windows_x64_call_arg_observer_tracks_registration_handler_constant() { + let arch = windows_x64_arch(); + let prepared = SsaArtifact::for_symbolic( + &[block( + 0x5000, + vec![ + R2ILOp::Copy { + dst: reg(8, 8), + src: c(1, 8), + }, + R2ILOp::Copy { + dst: reg(16, 8), + src: c(0x1400_3d0f, 8), + }, + R2ILOp::Call { + target: c(0x1800_1000, 8), + }, + R2ILOp::Return { target: c(0, 8) }, + ], + )], + Some(&arch), + ) + .expect("ssa"); + + let observations = observe_call_arguments(&prepared, &AbiProfile::windows_x64()); + let call_id = prepared + .call_sites() + .by_id + .keys() + .next() + .copied() + .expect("callsite"); + let args = observations.get(&call_id).expect("call args"); + assert_eq!(args.first(), Some(&CallArgObservation::Const(1))); + assert_eq!(args.get(1), Some(&CallArgObservation::Const(0x1400_3d0f))); + } } diff --git a/crates/r2ssa/src/lib.rs b/crates/r2ssa/src/lib.rs index c8d8e15..afa923d 100644 --- a/crates/r2ssa/src/lib.rs +++ b/crates/r2ssa/src/lib.rs @@ -16,6 +16,7 @@ //! - [`taint`]: Taint analysis on SSA def-use chains //! - [`var`]: SSA variable representation +pub mod assumption; pub mod block; pub mod cfg; pub mod defuse; @@ -32,6 +33,10 @@ pub mod semantic; pub mod taint; pub mod var; +pub use assumption::{ + AnalysisAssumption, AnalysisAssumptionConflict, AssumptionProvenance, AssumptionScope, + AssumptionSet, AssumptionSubject, AssumptionUsageReport, AssumptionValue, +}; pub use block::SSABlock; pub use cfg::{BasicBlock, BlockTerminator, CFG, CFGEdge}; pub use defuse::{ @@ -46,10 +51,11 @@ pub use graph::{ BlockId, GraphBlock, GraphInst, GraphValue, InstId, InstPayload, SsaGraph, UseSite, ValueId, }; pub use interproc::{ - AbiProfile, FunctionSemanticSummary, InterprocFunctionId, InterprocFunctionInput, - InterprocSolveConfig, InterprocSummaryDiagnostics, InterprocSummarySet, SummaryArgEffect, - SummaryMemoryEffect, SummaryMemoryEffectKind, SummaryMemoryLocation, SummaryMemoryRange, - SummaryMemoryRegion, SummaryReturnRelation, solve_interproc_summary_set, + AbiProfile, CallArgObservation, FunctionSemanticSummary, InterprocFunctionId, + InterprocFunctionInput, InterprocSolveConfig, InterprocSummaryDiagnostics, InterprocSummarySet, + SummaryArgEffect, SummaryMemoryEffect, SummaryMemoryEffectKind, SummaryMemoryLocation, + SummaryMemoryRange, SummaryMemoryRegion, SummaryReturnRelation, observe_call_arguments, + solve_interproc_summary_set, }; pub use op::SSAOp; pub use optimize::{DecompilePrepConfig, OptimizationConfig, OptimizationStats, optimize_function}; @@ -57,7 +63,8 @@ pub use semantic::{ BlockAssumption, CallMemoryEffect, CallSiteFact, CallSiteFacts, CallSiteId, CompareKind, CompareProvenance, GlobalObjectKey, MemoryDefFact, MemoryLocation, MemoryPhiFact, MemorySSAFacts, MemoryUseFact, MemoryVersion, ObjectFact, ObjectId, ObjectKind, ObjectModel, - PredicateFact, PredicateFacts, PredicateId, PreparedFunctionFacts, SwitchPredicateFact, + PredicateFact, PredicateFacts, PredicateId, PreparedAssumptionBinding, + PreparedAssumptionBindingKind, PreparedFunctionFacts, SwitchPredicateFact, }; pub use taint::{DefaultTaintPolicy, TaintAnalysis, TaintLabel, TaintPolicy, TaintResult}; pub use var::SSAVar; diff --git a/crates/r2ssa/src/semantic.rs b/crates/r2ssa/src/semantic.rs index 579fe97..e2ecb3c 100644 --- a/crates/r2ssa/src/semantic.rs +++ b/crates/r2ssa/src/semantic.rs @@ -5,19 +5,22 @@ use std::collections::BTreeMap; +use serde::{Deserialize, Serialize}; + +use crate::assumption::{AssumptionSet, AssumptionSubject, AssumptionUsageReport, AssumptionValue}; use crate::cfg::BlockTerminator; use crate::function::{DecompilePrepFacts, SSAFunction, StackAddressBase, StackAddressRoot}; use crate::graph::{InstId, SsaGraph, ValueId}; use crate::op::SSAOp; use crate::var::SSAVar; -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct ObjectId(pub u32); -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct PredicateId(pub u32); -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct CallSiteId(pub u32); #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -185,26 +188,229 @@ pub struct CallSiteFacts { pub by_inst: BTreeMap, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PreparedAssumptionBindingKind { + Predicate { + predicate: PredicateId, + block_addr: u64, + predecessor: Option, + truth: bool, + }, + Register { + name: String, + state_name: String, + symbol_name: String, + bits: u32, + }, + StackSlot { + base: StackAddressBase, + offset: i64, + object: ObjectId, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreparedAssumptionBinding { + pub assumption: crate::AnalysisAssumption, + pub binding: PreparedAssumptionBindingKind, +} + #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct PreparedFunctionFacts { pub objects: ObjectModel, pub memory: MemorySSAFacts, pub predicates: PredicateFacts, pub call_sites: CallSiteFacts, + pub assumptions: AssumptionSet, + pub applied_assumption_bindings: Vec, + pub assumption_usage: AssumptionUsageReport, } impl PreparedFunctionFacts { pub fn collect(function: &SSAFunction, graph: &SsaGraph) -> Self { - let call_sites = collect_call_sites(function, graph); + Self::collect_with_assumptions(function, graph, &AssumptionSet::default()) + } + + pub fn collect_with_assumptions( + function: &SSAFunction, + graph: &SsaGraph, + assumptions: &AssumptionSet, + ) -> Self { + let call_sites = collect_call_sites(function, graph, function.decompile_prep_facts()); let (objects, memory) = collect_object_and_memory_facts(function, graph, &call_sites); - let predicates = collect_predicate_facts(function, graph); + let predicates = apply_assumptions_to_predicate_facts( + collect_predicate_facts(function, graph), + assumptions, + ); + let (applied_assumption_bindings, assumption_usage) = + collect_prepared_assumption_usage(graph, &objects, &predicates, assumptions); Self { objects, memory, predicates, call_sites, + assumptions: assumptions.clone(), + applied_assumption_bindings, + assumption_usage, + } + } +} + +fn apply_assumptions_to_predicate_facts( + mut predicates: PredicateFacts, + assumptions: &AssumptionSet, +) -> PredicateFacts { + for assumption in assumptions.iter() { + let (predicate_id, block_addr, predecessor, truth) = + match (&assumption.subject, &assumption.value) { + ( + AssumptionSubject::Predicate { + predicate, + block_addr, + predecessor, + }, + AssumptionValue::Branch { truth }, + ) => (*predicate, *block_addr, *predecessor, *truth), + _ => continue, + }; + if !predicates.predicates.contains_key(&predicate_id) { + continue; + } + let entry = predicates.block_assumptions.entry(block_addr).or_default(); + if entry.iter().any(|existing| { + existing.predicate == predicate_id + && existing.predecessor == predecessor.unwrap_or(existing.predecessor) + && existing.truth == truth + }) { + continue; + } + entry.push(BlockAssumption { + predecessor: predecessor.unwrap_or(block_addr), + predicate: predicate_id, + truth, + }); + } + predicates +} + +fn collect_prepared_assumption_usage( + graph: &SsaGraph, + objects: &ObjectModel, + predicates: &PredicateFacts, + assumptions: &AssumptionSet, +) -> (Vec, AssumptionUsageReport) { + let mut bindings = Vec::new(); + let mut usage = AssumptionUsageReport::default(); + + for assumption in assumptions.iter() { + match (&assumption.subject, &assumption.value) { + ( + AssumptionSubject::Predicate { + predicate, + block_addr, + predecessor, + }, + AssumptionValue::Branch { truth }, + ) => { + let Some(fact) = predicates.predicates.get(predicate) else { + usage.mark_ignored(assumption); + continue; + }; + if fact.block_addr != *block_addr { + usage.mark_conflict( + assumption, + format!( + "predicate block mismatch (expected 0x{block_addr:x}, observed 0x{:x})", + fact.block_addr + ), + ); + continue; + } + if let Some(pred) = predecessor { + let expected = if *truth { + fact.true_target + } else { + fact.false_target + }; + if *pred != expected { + usage.mark_conflict( + assumption, + format!( + "branch predecessor 0x{pred:x} does not match selected edge 0x{expected:x}" + ), + ); + continue; + } + } + usage.mark_applied(assumption); + bindings.push(PreparedAssumptionBinding { + assumption: assumption.clone(), + binding: PreparedAssumptionBindingKind::Predicate { + predicate: *predicate, + block_addr: *block_addr, + predecessor: *predecessor, + truth: *truth, + }, + }); + } + (AssumptionSubject::Register { name }, _) => { + let Some(value) = graph.values.iter().find(|value| { + value.var.version == 0 + && value.var.is_register() + && value.var.name.eq_ignore_ascii_case(name) + }) else { + usage.mark_ignored(assumption); + continue; + }; + usage.mark_applied(assumption); + bindings.push(PreparedAssumptionBinding { + assumption: assumption.clone(), + binding: PreparedAssumptionBindingKind::Register { + name: value.var.name.clone(), + state_name: value.var.display_name(), + symbol_name: value + .var + .name + .strip_prefix("reg:") + .unwrap_or(&value.var.name) + .to_ascii_lowercase(), + bits: value.var.size.saturating_mul(8), + }, + }); + } + (AssumptionSubject::StackSlot { base, offset }, _) => { + let Some((root, object)) = + objects.stack_objects.iter().find_map(|(root, object)| { + let matches_base = matches!( + (base.as_str(), root.base), + ("bp", StackAddressBase::FramePointer) + | ("frame", StackAddressBase::FramePointer) + | ("rbp", StackAddressBase::FramePointer) + | ("sp", StackAddressBase::StackPointer) + | ("stack", StackAddressBase::StackPointer) + | ("rsp", StackAddressBase::StackPointer) + ); + (matches_base && root.offset == *offset).then_some((*root, *object)) + }) + else { + usage.mark_ignored(assumption); + continue; + }; + usage.mark_applied(assumption); + bindings.push(PreparedAssumptionBinding { + assumption: assumption.clone(), + binding: PreparedAssumptionBindingKind::StackSlot { + base: root.base, + offset: root.offset, + object, + }, + }); + } + _ => usage.mark_ignored(assumption), } } + + (bindings, usage) } #[derive(Debug, Clone)] @@ -731,7 +937,11 @@ fn collect_predicate_facts(function: &SSAFunction, graph: &SsaGraph) -> Predicat } } -fn collect_call_sites(function: &SSAFunction, graph: &SsaGraph) -> CallSiteFacts { +fn collect_call_sites( + function: &SSAFunction, + graph: &SsaGraph, + prep_facts: Option<&DecompilePrepFacts>, +) -> CallSiteFacts { let mut by_id = BTreeMap::new(); let mut by_inst = BTreeMap::new(); let mut next_id = 0u32; @@ -763,7 +973,7 @@ fn collect_call_sites(function: &SSAFunction, graph: &SsaGraph) -> CallSiteFacts }; let id = CallSiteId(next_id); next_id = next_id.saturating_add(1); - let direct_target = resolve_const_value(None, &target); + let direct_target = resolve_const_value(prep_facts, &target); by_inst.insert(inst_id, id); by_id.insert( id, diff --git a/crates/r2sym/Cargo.toml b/crates/r2sym/Cargo.toml index 73c7730..fff3912 100644 --- a/crates/r2sym/Cargo.toml +++ b/crates/r2sym/Cargo.toml @@ -5,6 +5,10 @@ edition.workspace = true license.workspace = true description = "Symbolic execution engine for r2sleigh" +[[bench]] +name = "symex_hotpaths" +harness = false + [dependencies] r2il = { path = "../r2il" } r2ssa = { path = "../r2ssa" } @@ -19,4 +23,6 @@ default = [] r2 = ["r2pipe", "serde_json"] [dev-dependencies] -sleigh-config = { version = "1.0", features = ["x86"] } \ No newline at end of file +criterion = "0.5" +proptest = "1.6" +sleigh-config = { version = "1.0", features = ["x86"] } diff --git a/crates/r2sym/benches/symex_hotpaths.rs b/crates/r2sym/benches/symex_hotpaths.rs new file mode 100644 index 0000000..2b85c74 --- /dev/null +++ b/crates/r2sym/benches/symex_hotpaths.rs @@ -0,0 +1,732 @@ +use criterion::{BatchSize, Criterion, black_box, criterion_group, criterion_main}; +use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; +use r2ssa::SsaArtifact; +use r2sym::path::ExploreStrategy; +use r2sym::{ + AddressValue, ExplorationSpec, ExploreConfig, InputSpec, PathExplorer, PredicateSpec, + SummaryRegistry, SymSolver, SymState, SymValue, +}; +use z3::Context; +use z3::ast::BV; + +const RAX: u64 = 0; +const RBX: u64 = 8; +const RCX: u64 = 16; +const RDI: u64 = 56; +const RSI: u64 = 64; +const RDX: u64 = 72; +const TMP0: u64 = 0x80; +const TMP1: u64 = 0x88; + +fn leaked_ctx() -> &'static Context { + Box::leak(Box::new(Context::thread_local())) +} + +fn make_reg(offset: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Register, + offset, + size, + meta: None, + } +} + +fn make_const(val: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: val, + size, + meta: None, + } +} + +fn make_x86_64_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("RAX", RAX, 8)); + arch.add_register(RegisterDef::new("RBX", RBX, 8)); + arch.add_register(RegisterDef::new("RCX", RCX, 8)); + arch.add_register(RegisterDef::new("RDI", RDI, 8)); + arch.add_register(RegisterDef::new("RSI", RSI, 8)); + arch.add_register(RegisterDef::new("RDX", RDX, 8)); + arch +} + +fn build_branching_function() -> SsaArtifact { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(0x1337, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + + SsaArtifact::for_symbolic(&blocks, None).expect("branching benchmark SSA should build") +} + +fn build_join_heavy_function(levels: u64) -> SsaArtifact { + let mut blocks = Vec::new(); + let mut addr = 0x2000; + + for level in 0..levels { + let branch_addr = addr; + let false_addr = addr + 0x4; + let true_addr = addr + 0x8; + let join_addr = addr + 0xc; + + blocks.push(R2ILBlock { + addr: branch_addr, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(level, 8), + }, + R2ILOp::CBranch { + target: make_const(true_addr, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }); + blocks.push(R2ILBlock { + addr: false_addr, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(join_addr, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + blocks.push(R2ILBlock { + addr: true_addr, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(join_addr, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + blocks.push(R2ILBlock { + addr: join_addr, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_reg(RAX, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + + addr += 0x10; + } + + blocks.push(R2ILBlock { + addr, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0x1337, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + + blocks.sort_by_key(|block| block.addr); + SsaArtifact::for_symbolic(&blocks, None).expect("join-heavy benchmark SSA should build") +} + +fn emit_branch_tree( + blocks: &mut Vec, + next_addr: &mut u64, + depth: u32, + seed: &mut u64, +) -> u64 { + let entry = *next_addr; + *next_addr += 4; + + if depth == 0 { + let leaf_value = *seed; + *seed += 1; + blocks.push(R2ILBlock { + addr: entry, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(leaf_value, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + return entry; + } + + let false_entry = emit_branch_tree(blocks, next_addr, depth - 1, seed); + let true_entry = emit_branch_tree(blocks, next_addr, depth - 1, seed); + let compare_value = *seed; + *seed += 1; + + debug_assert_eq!(false_entry, entry + 4); + + blocks.push(R2ILBlock { + addr: entry, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(compare_value, 8), + }, + R2ILOp::CBranch { + target: make_const(true_entry, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }); + + entry +} + +fn build_branch_tree_function(levels: u32) -> SsaArtifact { + let mut blocks = Vec::new(); + let mut next_addr = 0x3000; + let mut seed = 0; + + emit_branch_tree(&mut blocks, &mut next_addr, levels, &mut seed); + blocks.sort_by_key(|block| block.addr); + SsaArtifact::for_symbolic(&blocks, None).expect("branch-tree benchmark SSA should build") +} + +fn build_fd_spec_function(arch: &ArchSpec) -> SsaArtifact { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Copy { + dst: make_reg(RDX, 8), + src: make_const(1, 8), + }, + R2ILOp::Call { + target: make_const(0x5000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x2000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(b'k' as u64, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0x1337, 8), + }], + }, + ]; + + SsaArtifact::for_symbolic(&blocks, Some(arch)).expect("fd benchmark SSA should build") +} + +fn bench_solver_sat_cache(c: &mut Criterion) { + c.bench_function("r2sym/solver_is_sat_cached_cold", |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let solver = SymSolver::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.add_true_constraint(&x.ult(&ctx, &SymValue::concrete(5, 32))); + + black_box(solver.is_sat(&state)); + black_box(solver.is_sat(&state)); + black_box(solver.stats()) + }); + }); + + c.bench_function("r2sym/solver_is_sat_cached_hot", |b| { + b.iter_batched( + || { + let ctx = leaked_ctx(); + let solver = SymSolver::new(ctx); + let mut state = SymState::new(ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.add_true_constraint(&x.ult(ctx, &SymValue::concrete(5, 32))); + black_box(solver.is_sat(&state)); + (solver, state) + }, + |(solver, state)| { + black_box(solver.is_sat(&state)); + black_box(solver.stats()) + }, + BatchSize::SmallInput, + ); + }); +} + +fn bench_solver_prefix_reuse(c: &mut Criterion) { + c.bench_function("r2sym/solver_prefix_reuse_cold", |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let solver = SymSolver::new(&ctx); + + let mut root = SymState::new(&ctx, 0x1000); + root.make_symbolic("x", 32); + root.make_symbolic("y", 32); + + let x = root.get_register("x"); + let y = root.get_register("y"); + + let mut left = root.fork(); + left.add_true_constraint(&x.ult(&ctx, &SymValue::concrete(20, 32))); + + let mut left_deep = left.fork(); + left_deep.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(7, 32))); + + let mut right = root.fork(); + right.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(99, 32))); + + black_box(solver.is_sat(&left_deep)); + black_box(solver.is_sat(&left)); + black_box(solver.is_sat(&right)); + black_box(solver.stats()) + }); + }); + + c.bench_function("r2sym/solver_prefix_reuse_hot", |b| { + b.iter_batched( + || { + let ctx = leaked_ctx(); + let solver = SymSolver::new(ctx); + + let mut root = SymState::new(ctx, 0x1000); + root.make_symbolic("x", 32); + root.make_symbolic("y", 32); + + let x = root.get_register("x"); + let y = root.get_register("y"); + + let mut left = root.fork(); + left.add_true_constraint(&x.ult(ctx, &SymValue::concrete(20, 32))); + + let mut left_deep = left.fork(); + left_deep.add_true_constraint(&y.eq(ctx, &SymValue::concrete(7, 32))); + + let mut right = root.fork(); + right.add_true_constraint(&x.eq(ctx, &SymValue::concrete(99, 32))); + (solver, left_deep, left, right) + }, + |(solver, left_deep, left, right)| { + black_box(solver.is_sat(&left_deep)); + black_box(solver.is_sat(&left)); + black_box(solver.is_sat(&right)); + black_box(solver.stats()) + }, + BatchSize::SmallInput, + ); + }); +} + +fn bench_solver_sliced_find_value(c: &mut Criterion) { + c.bench_function("r2sym/solver_sliced_find_value_cold", |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let solver = SymSolver::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + + for idx in 0..24 { + let reg = format!("u{idx}"); + let sym = format!("sym_u{idx}"); + state.make_symbolic_named(®, &sym, 32); + let value = state.get_register(®); + state.add_true_constraint(&value.eq(&ctx, &SymValue::concrete(idx as u64, 32))); + } + + state.make_symbolic("y", 32); + let y = state.get_register("y"); + let y_is_seven = y.to_bv(&ctx).eq(BV::from_u64(7, 32)); + + black_box(solver.find_value(&state, &y, &y_is_seven)) + }); + }); + + c.bench_function("r2sym/solver_sliced_find_value_hot", |b| { + b.iter_batched( + || { + let ctx = leaked_ctx(); + let solver = SymSolver::new(ctx); + let mut state = SymState::new(ctx, 0x1000); + + for idx in 0..24 { + let reg = format!("u{idx}"); + let sym = format!("sym_u{idx}"); + state.make_symbolic_named(®, &sym, 32); + let value = state.get_register(®); + state.add_true_constraint(&value.eq(ctx, &SymValue::concrete(idx as u64, 32))); + } + + state.make_symbolic("y", 32); + let y = state.get_register("y"); + let y_is_seven = y.to_bv(ctx).eq(BV::from_u64(7, 32)); + (solver, state, y, y_is_seven) + }, + |(solver, state, y, y_is_seven)| black_box(solver.find_value(&state, &y, &y_is_seven)), + BatchSize::SmallInput, + ); + }); +} + +fn bench_solver_small_connected_query(c: &mut Criterion) { + c.bench_function("r2sym/solver_small_connected_query_hot", |b| { + b.iter_batched( + || { + let ctx = leaked_ctx(); + let solver = SymSolver::new(ctx); + let mut state = SymState::new(ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.constrain_range(&x, 3, 9); + let x_is_seven = x.to_bv(ctx).eq(BV::from_u64(7, 32)); + (solver, state, x, x_is_seven) + }, + |(solver, state, x, x_is_seven)| black_box(solver.find_value(&state, &x, &x_is_seven)), + BatchSize::SmallInput, + ); + }); +} + +fn bench_solver_partitioned_is_sat(c: &mut Criterion) { + c.bench_function("r2sym/solver_partitioned_is_sat_cold", |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let solver = SymSolver::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + + for idx in 0..24 { + let reg = format!("p{idx}"); + let sym = format!("sym_p{idx}"); + state.make_symbolic_named(®, &sym, 32); + let value = state.get_register(®); + state.add_true_constraint(&value.eq(&ctx, &SymValue::concrete(idx as u64, 32))); + } + + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(1, 32))); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(2, 32))); + + black_box(solver.is_sat(&state)); + black_box(solver.stats()) + }); + }); + + c.bench_function("r2sym/solver_partitioned_is_sat_hot", |b| { + b.iter_batched( + || { + let ctx = leaked_ctx(); + let solver = SymSolver::new(ctx); + let mut state = SymState::new(ctx, 0x1000); + + for idx in 0..24 { + let reg = format!("p{idx}"); + let sym = format!("sym_p{idx}"); + state.make_symbolic_named(®, &sym, 32); + let value = state.get_register(®); + state.add_true_constraint(&value.eq(ctx, &SymValue::concrete(idx as u64, 32))); + } + + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.add_true_constraint(&x.eq(ctx, &SymValue::concrete(1, 32))); + state.add_true_constraint(&x.eq(ctx, &SymValue::concrete(2, 32))); + (solver, state) + }, + |(solver, state)| { + black_box(solver.is_sat(&state)); + black_box(solver.stats()) + }, + BatchSize::SmallInput, + ); + }); + + c.bench_function("r2sym/solver_partitioned_prefilter_unsat_hot", |b| { + b.iter_batched( + || { + let ctx = leaked_ctx(); + let solver = SymSolver::new(ctx); + let mut state = SymState::new(ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.constrain_range(&x, 3, 9); + let zero = x.to_bv(ctx).eq(BV::from_u64(0, 32)); + (solver, state, zero) + }, + |(solver, state, zero)| black_box(solver.sat_with_constraint(&state, &zero)), + BatchSize::SmallInput, + ); + }); +} + +fn bench_solver_cursor_fact_reuse(c: &mut Criterion) { + c.bench_function("r2sym/solver_cursor_fact_reuse_hot", |b| { + b.iter_batched( + || { + let ctx = leaked_ctx(); + let solver = SymSolver::new(ctx); + let mut base = SymState::new(ctx, 0x1000); + + for idx in 0..6 { + let reg = format!("p{idx}"); + let sym = format!("sym_p{idx}"); + base.make_symbolic_named(®, &sym, 32); + let value = base.get_register(®); + base.add_true_constraint(&value.eq(ctx, &SymValue::concrete(idx as u64, 32))); + } + + let prefix_value = base.get_register("p0"); + let prefix_eq = prefix_value.to_bv(ctx).eq(BV::from_u64(0, 32)); + black_box(solver.find_value(&base, &prefix_value, &prefix_eq)); + + let mut child = base.fork(); + child.make_symbolic("x", 32); + let x = child.get_register("x"); + child.constrain_range(&x, 3, 9); + let x_is_seven = x.to_bv(ctx).eq(BV::from_u64(7, 32)); + (solver, child, x, x_is_seven) + }, + |(solver, child, x, x_is_seven)| black_box(solver.find_value(&child, &x, &x_is_seven)), + BatchSize::SmallInput, + ); + }); +} + +fn bench_value_normalization_identities(c: &mut Criterion) { + c.bench_function("r2sym/value_normalization_identities", |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let x = SymValue::new_symbolic(&ctx, "x", 32); + let zero = SymValue::concrete(0, 32); + let one = SymValue::concrete(1, 32); + let all_ones = SymValue::concrete(u32::MAX as u64, 32); + + black_box(x.add(&ctx, &zero)); + black_box(x.mul(&ctx, &one)); + black_box(x.and(&ctx, &all_ones)); + black_box(x.xor(&ctx, &x)); + black_box(x.eq(&ctx, &x)); + }); + }); +} + +fn bench_explore_symbolic_branching(c: &mut Criterion) { + let func = build_branching_function(); + let config = ExploreConfig { + max_states: 64, + max_depth: 32, + timeout: None, + ..Default::default() + }; + + c.bench_function("r2sym/explore_symbolic_branching", |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let mut explorer = PathExplorer::with_config(&ctx, config.clone()); + let results = explorer.explore(black_box(&func), state); + black_box(results.len()); + black_box(explorer.solver().stats()) + }); + }); +} + +fn bench_explore_branch_tree(c: &mut Criterion) { + let func = build_branch_tree_function(5); + let config = ExploreConfig { + max_states: 256, + max_depth: 64, + timeout: None, + strategy: ExploreStrategy::Bfs, + ..Default::default() + }; + + c.bench_function("r2sym/explore_branch_tree", |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x3000); + state.make_symbolic("reg:56_0", 64); + + let mut explorer = PathExplorer::with_config(&ctx, config.clone()); + let results = explorer.explore(black_box(&func), state); + black_box(results.len()); + black_box(explorer.stats().clone()) + }); + }); +} + +fn bench_explore_same_pc_merge(c: &mut Criterion) { + let func = build_join_heavy_function(6); + let mut group = c.benchmark_group("r2sym/explore_same_pc_merge"); + + for (name, merge_states) in [("merge_off", false), ("merge_on", true)] { + let config = ExploreConfig { + max_states: 256, + max_depth: 96, + timeout: None, + strategy: ExploreStrategy::Bfs, + merge_states, + ..Default::default() + }; + + group.bench_function(name, |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x2000); + state.make_symbolic("reg:56_0", 64); + + let mut explorer = PathExplorer::with_config(&ctx, config.clone()); + let results = explorer.explore(black_box(&func), state); + black_box(results.len()); + black_box(explorer.stats().clone()) + }); + }); + } + + group.finish(); +} + +fn bench_run_spec_symbolic_fd_input(c: &mut Criterion) { + let arch = make_x86_64_arch(); + let func = build_fd_spec_function(&arch); + let config = ExploreConfig { + max_states: 64, + max_depth: 64, + timeout: None, + ..Default::default() + }; + let spec = ExplorationSpec { + find: vec![PredicateSpec::Address { + addr: AddressValue::Integer(0x1010), + }], + inputs: vec![InputSpec::Fd { + fd: 0, + len: 1, + name: Some("stdin0".to_string()), + alphabet: Some("k".to_string()), + }], + ..Default::default() + }; + + c.bench_function("r2sym/run_spec_symbolic_fd_input", |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let mut initial_state = SymState::new(&ctx, 0x1000); + spec.apply_to_state(&mut initial_state); + + let mut explorer = PathExplorer::with_config(&ctx, config.clone()); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + assert!(registry.install_for_explorer(&mut explorer, 0x5000, "read")); + + let result = explorer + .run_spec(black_box(&func), initial_state, black_box(&spec)) + .expect("fd-input benchmark should succeed"); + black_box(result.found_paths.len()); + black_box(explorer.solver().stats()) + }); + }); +} + +criterion_group!( + symex_hotpaths, + bench_solver_sat_cache, + bench_solver_prefix_reuse, + bench_solver_sliced_find_value, + bench_solver_partitioned_is_sat, + bench_value_normalization_identities, + bench_explore_symbolic_branching, + bench_explore_branch_tree, + bench_explore_same_pc_merge, + bench_run_spec_symbolic_fd_input, + bench_solver_small_connected_query, + bench_solver_cursor_fact_reuse +); +criterion_main!(symex_hotpaths); diff --git a/crates/r2sym/src/backward.rs b/crates/r2sym/src/backward.rs new file mode 100644 index 0000000..ebbf308 --- /dev/null +++ b/crates/r2sym/src/backward.rs @@ -0,0 +1,2292 @@ +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::rc::Rc; + +use r2ssa::graph::{InstPayload, ValueId}; +use r2ssa::{CallSiteId, PredicateId, SsaArtifact}; +use serde::{Deserialize, Serialize}; +use z3::Context; +use z3::ast::{Ast, BV, Bool}; +use z3::{SatResult as Z3SatResult, Solver}; + +use crate::sim::{CallConv, DerivedFunctionSummary}; +use crate::state::SymState; +use crate::value::SymValue; +use crate::{ + MemoryRegionId, MemoryRegionKind, SemanticConfidence, SemanticEvidence, + SemanticEvidenceCoverage, SemanticEvidenceProvenance, SemanticEvidenceReason, +}; + +const DEFAULT_REVERSE_PATH_LIMIT: usize = 16; +const DEFAULT_MAX_NORMALIZED_OFFSETS: usize = 8; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum BackwardConditionPrecision { + Exact, + OverApprox, + ResidualSearchRequired, + Unsupported, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BackwardConditionSummary { + pub simplified: String, + pub terms: Vec, + pub memory_terms: Vec, + pub backward_memory_substitutions: usize, + pub backward_memory_candidate_enumerations: usize, + pub backward_memory_residual_fallbacks: usize, + pub precision: BackwardConditionPrecision, + pub supported_paths: usize, + pub total_paths: usize, +} + +impl BackwardConditionSummary { + pub fn evidence(&self) -> SemanticEvidence { + match self.precision { + BackwardConditionPrecision::Exact => SemanticEvidence::exact(), + BackwardConditionPrecision::OverApprox => { + SemanticEvidence::likely(SemanticEvidenceReason::PartialPathCoverage) + .with_provenance(SemanticEvidenceProvenance::Normalized) + } + BackwardConditionPrecision::ResidualSearchRequired => { + if self.supported_paths > 0 + && !self.memory_terms.is_empty() + && self.backward_memory_residual_fallbacks == 0 + { + SemanticEvidence::likely(SemanticEvidenceReason::DerivedFromRanking) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized) + .with_reason(SemanticEvidenceReason::PartialPathCoverage) + } else if self.supported_paths > 0 { + SemanticEvidence::heuristic(SemanticEvidenceReason::ResidualSearchRequired) + .with_coverage(SemanticEvidenceCoverage::Bounded) + } else { + SemanticEvidence::residual(SemanticEvidenceReason::ResidualSearchRequired) + } + } + BackwardConditionPrecision::Unsupported => { + SemanticEvidence::residual(SemanticEvidenceReason::ValueOpaque) + } + } + } + + pub fn confidence(&self) -> SemanticConfidence { + self.evidence().tier + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BackwardMemoryCondition { + pub region: BackwardMemoryRegion, + pub offset_lo: i64, + pub offset_hi: i64, + pub size: u32, + pub exact_offset: bool, + #[serde(default, skip_serializing_if = "SemanticEvidence::is_default_exact")] + pub evidence: SemanticEvidence, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub binding: Option, + pub expr: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub value_expr: Option, + #[serde(default)] + pub exact_value: bool, +} + +impl BackwardMemoryCondition { + pub fn evidence(&self) -> SemanticEvidence { + if self.evidence.is_default_exact() && !self.exact_offset { + inferred_memory_term_evidence( + &self.region, + self.offset_lo, + self.offset_hi, + self.exact_offset, + ) + } else { + self.evidence.clone() + } + } + + pub fn confidence(&self) -> SemanticConfidence { + self.evidence().tier + } +} + +fn inferred_memory_term_evidence( + region: &BackwardMemoryRegion, + offset_lo: i64, + offset_hi: i64, + exact_offset: bool, +) -> SemanticEvidence { + if exact_offset { + SemanticEvidence::exact() + } else { + let Some(span) = (offset_hi >= offset_lo).then_some(offset_hi - offset_lo) else { + return SemanticEvidence::heuristic(SemanticEvidenceReason::AliasAmbiguity) + .with_coverage(SemanticEvidenceCoverage::Bounded); + }; + + let (likely_span, provenance, reason) = match region { + BackwardMemoryRegion::Argument { .. } => ( + 16, + SemanticEvidenceProvenance::Stable, + SemanticEvidenceReason::DerivedFromRanking, + ), + BackwardMemoryRegion::Region(region) => match region.kind { + MemoryRegionKind::Stack | MemoryRegionKind::Global | MemoryRegionKind::Input => ( + 16, + SemanticEvidenceProvenance::Stable, + SemanticEvidenceReason::DerivedFromRanking, + ), + MemoryRegionKind::Replay => ( + 16, + SemanticEvidenceProvenance::Ranked, + SemanticEvidenceReason::ReplayOverlap, + ), + MemoryRegionKind::Heap => ( + 12, + SemanticEvidenceProvenance::Ranked, + SemanticEvidenceReason::HeapIdentityWeak, + ), + MemoryRegionKind::EscapedUnknown => ( + 8, + SemanticEvidenceProvenance::Unstable, + SemanticEvidenceReason::AliasAmbiguity, + ), + }, + }; + + if span <= likely_span { + SemanticEvidence::likely(reason) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(provenance) + } else { + SemanticEvidence::heuristic(reason) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(provenance) + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct BackwardRegionRef { + pub id: MemoryRegionId, + pub kind: MemoryRegionKind, + pub name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum BackwardMemoryRegion { + Argument { index: usize }, + Region(BackwardRegionRef), +} + +fn format_backward_memory_location(region: &BackwardMemoryRegion, offset: i64) -> String { + match region { + BackwardMemoryRegion::Argument { index } => { + if offset == 0 { + format!("*arg{index}") + } else if offset > 0 { + format!("*(arg{index} + 0x{:x})", offset as u64) + } else { + format!("*(arg{index} - 0x{:x})", offset.unsigned_abs()) + } + } + BackwardMemoryRegion::Region(region) => { + let base = region.name.as_str(); + if offset == 0 { + format!("*{base}") + } else if offset > 0 { + format!("*({base} + 0x{:x})", offset as u64) + } else { + format!("*({base} - 0x{:x})", offset.unsigned_abs()) + } + } + } +} + +fn backward_memory_term_expr( + region: &BackwardMemoryRegion, + offset_lo: i64, + offset_hi: i64, + fallback: &str, +) -> String { + if offset_lo == offset_hi { + format_backward_memory_location(region, offset_lo) + } else { + fallback.to_string() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct NormalizedMemoryLocation { + region: BackwardMemoryRegion, + offset: i64, +} + +pub struct CompiledBackwardCondition { + pub predicate: Bool, + pub summary: BackwardConditionSummary, +} + +#[derive(Clone)] +pub(crate) struct DerivedCallSummaryView<'ctx> { + pub summary: Rc>, + pub callconv: CallConv, +} + +#[derive(Clone)] +struct ReversePath { + block_addr: u64, + phi_predecessors: BTreeMap, + assumptions: Vec<(PredicateId, bool)>, + visited: BTreeSet, +} + +#[derive(Debug)] +enum EvalUnsupported { + Unsupported, + Cycle, +} + +struct ValueTranslator<'a, 'ctx> { + func: &'a SsaArtifact, + state: &'a SymState<'ctx>, + phi_predecessors: &'a BTreeMap, + call_contexts: &'a HashMap>, + memo: HashMap>, + visiting: HashSet, + assumption_constraints: Vec, + memory_terms: Vec, + memory_substitutions: usize, + memory_candidate_enumerations: usize, + memory_residual_fallbacks: usize, + used_unsummarized_memory: bool, +} + +#[derive(Clone)] +struct CallTransformContext<'ctx> { + summary: Rc>, + callconv: CallConv, + args: Vec>, +} + +enum SummaryLocationMatch { + Match(Vec), + NoMatch, + Residual, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct LocationGroupRank { + region_rank: u8, + inexact_offset: bool, + span: u64, + offset_count: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct LocationGroupTieBreak { + region_discriminant: u8, + region_id: u32, + arg_index: usize, + min_offset: i64, +} + +impl<'a, 'ctx> ValueTranslator<'a, 'ctx> { + fn new( + func: &'a SsaArtifact, + state: &'a SymState<'ctx>, + phi_predecessors: &'a BTreeMap, + call_contexts: &'a HashMap>, + ) -> Self { + Self { + func, + state, + phi_predecessors, + call_contexts, + memo: HashMap::new(), + visiting: HashSet::new(), + assumption_constraints: Vec::new(), + memory_terms: Vec::new(), + memory_substitutions: 0, + memory_candidate_enumerations: 0, + memory_residual_fallbacks: 0, + used_unsummarized_memory: false, + } + } + + fn note_assumption(&mut self, constraint: Bool) { + self.assumption_constraints.push(constraint); + } + + fn eval_predicate( + &mut self, + predicate: PredicateId, + truth: bool, + ) -> Result { + let fact = self + .func + .predicates() + .predicates + .get(&predicate) + .ok_or(EvalUnsupported::Unsupported)?; + let value = self.eval_value_id(fact.condition)?; + let bool_expr = value_to_bool(self.state.context(), &value); + Ok(if truth { bool_expr } else { bool_expr.not() }) + } + + fn eval_value_id(&mut self, value_id: ValueId) -> Result, EvalUnsupported> { + if let Some(value) = self.memo.get(&value_id).cloned() { + return Ok(value); + } + if !self.visiting.insert(value_id) { + return Err(EvalUnsupported::Cycle); + } + + let var = self + .func + .value_var(value_id) + .ok_or(EvalUnsupported::Unsupported)?; + let result = if var.is_const() { + eval_const_var(var) + } else if let Some(hex) = var.name.strip_prefix("ram:") { + u64::from_str_radix(hex, 16) + .map(|value| SymValue::concrete(value, var.size * 8)) + .map_err(|_| EvalUnsupported::Unsupported) + } else if let Some(inst_id) = self.func.graph().def_inst(value_id) { + let inst = self + .func + .graph() + .inst(inst_id) + .ok_or(EvalUnsupported::Unsupported)?; + match &inst.payload { + InstPayload::Phi { .. } => { + let block_addr = self + .func + .graph() + .block(inst.block) + .map(|block| block.addr) + .ok_or(EvalUnsupported::Unsupported)?; + let selected_pred = self + .phi_predecessors + .get(&block_addr) + .copied() + .ok_or(EvalUnsupported::Unsupported)?; + let block = self + .func + .get_block(block_addr) + .ok_or(EvalUnsupported::Unsupported)?; + let phi = block + .phis + .iter() + .find(|phi| phi.dst == *var) + .ok_or(EvalUnsupported::Unsupported)?; + let source = phi + .sources + .iter() + .find(|(pred, _)| *pred == selected_pred) + .map(|(_, source)| source) + .ok_or(EvalUnsupported::Unsupported)?; + self.eval_ssa_var(source) + } + InstPayload::Op(op) => { + let block_addr = self + .func + .graph() + .block(inst.block) + .map(|block| block.addr) + .ok_or(EvalUnsupported::Unsupported)?; + self.eval_op(inst_id, block_addr, op) + } + } + } else { + Ok(read_input_var(self.state, var)) + }?; + + self.visiting.remove(&value_id); + self.memo.insert(value_id, result.clone()); + Ok(result) + } + + fn eval_ssa_var(&mut self, var: &r2ssa::SSAVar) -> Result, EvalUnsupported> { + if let Some(value_id) = self.func.graph().value_id_for_var(var) { + self.eval_value_id(value_id) + } else { + Ok(read_input_var(self.state, var)) + } + } + + fn eval_op( + &mut self, + inst_id: r2ssa::graph::InstId, + block_addr: u64, + op: &r2ssa::SSAOp, + ) -> Result, EvalUnsupported> { + let ctx = self.state.context(); + use r2ssa::SSAOp::*; + + match op { + Copy { src, .. } | Cast { src, .. } => self.eval_ssa_var(src), + Load { dst, addr, .. } => { + if let Some(local_value) = self.local_memory_value(inst_id, dst.size) { + return Ok(local_value); + } + let addr_value = self.eval_ssa_var(addr)?; + let structural_locations = + self.normalized_memory_locations(addr).unwrap_or_default(); + let resolved_locations = self + .resolved_memory_locations(&addr_value, dst.size) + .map(|locations| { + locations + .into_iter() + .filter(is_specific_memory_location) + .collect::>() + }) + .unwrap_or_default(); + let normalized = if !resolved_locations.is_empty() { + resolved_locations.clone() + } else { + structural_locations.clone() + }; + if let Some(call_ctx) = self.call_context_for_inst(inst_id, block_addr).cloned() + && let Some(summary_value) = self.summary_memory_value( + &call_ctx, + &addr_value, + &resolved_locations, + &structural_locations, + dst.size, + ) + { + return Ok(summary_value); + } + let value = self.state.mem_read(&addr_value, dst.size); + if self.record_memory_term(&normalized, dst.size, &value) { + return Ok(value); + } + self.used_unsummarized_memory = true; + self.memory_residual_fallbacks += 1; + Ok(value) + } + IntAdd { a, b, .. } => Ok(self.eval_ssa_var(a)?.add(ctx, &self.eval_ssa_var(b)?)), + IntSub { a, b, .. } => Ok(self.eval_ssa_var(a)?.sub(ctx, &self.eval_ssa_var(b)?)), + IntMult { a, b, .. } => Ok(self.eval_ssa_var(a)?.mul(ctx, &self.eval_ssa_var(b)?)), + IntDiv { a, b, .. } => Ok(self.eval_ssa_var(a)?.udiv(ctx, &self.eval_ssa_var(b)?)), + IntSDiv { a, b, .. } => Ok(self.eval_ssa_var(a)?.sdiv(ctx, &self.eval_ssa_var(b)?)), + IntRem { a, b, .. } => Ok(self.eval_ssa_var(a)?.urem(ctx, &self.eval_ssa_var(b)?)), + IntSRem { a, b, .. } => Ok(self.eval_ssa_var(a)?.srem(ctx, &self.eval_ssa_var(b)?)), + IntNegate { src, .. } => Ok(self.eval_ssa_var(src)?.neg(ctx)), + IntAnd { a, b, .. } => Ok(self.eval_ssa_var(a)?.and(ctx, &self.eval_ssa_var(b)?)), + IntOr { a, b, .. } => Ok(self.eval_ssa_var(a)?.or(ctx, &self.eval_ssa_var(b)?)), + IntXor { a, b, .. } => Ok(self.eval_ssa_var(a)?.xor(ctx, &self.eval_ssa_var(b)?)), + IntNot { src, .. } => Ok(self.eval_ssa_var(src)?.not(ctx)), + IntLeft { a, b, .. } => Ok(self.eval_ssa_var(a)?.shl(ctx, &self.eval_ssa_var(b)?)), + IntRight { a, b, .. } => Ok(self.eval_ssa_var(a)?.lshr(ctx, &self.eval_ssa_var(b)?)), + IntSRight { a, b, .. } => Ok(self.eval_ssa_var(a)?.ashr(ctx, &self.eval_ssa_var(b)?)), + IntEqual { a, b, .. } => Ok(self.eval_ssa_var(a)?.eq(ctx, &self.eval_ssa_var(b)?)), + IntNotEqual { a, b, .. } => { + let eq = self.eval_ssa_var(a)?.eq(ctx, &self.eval_ssa_var(b)?); + Ok(eq.not(ctx)) + } + IntLess { a, b, .. } => Ok(self.eval_ssa_var(a)?.ult(ctx, &self.eval_ssa_var(b)?)), + IntSLess { a, b, .. } => Ok(self.eval_ssa_var(a)?.slt(ctx, &self.eval_ssa_var(b)?)), + IntLessEqual { a, b, .. } => Ok(self.eval_ssa_var(a)?.ule(ctx, &self.eval_ssa_var(b)?)), + IntSLessEqual { a, b, .. } => { + Ok(self.eval_ssa_var(a)?.sle(ctx, &self.eval_ssa_var(b)?)) + } + IntZExt { dst, src } => Ok(self.eval_ssa_var(src)?.zero_extend(ctx, dst.size * 8)), + IntSExt { dst, src } => { + let value = self.eval_ssa_var(src)?; + let current = value.to_bv(ctx); + let extended = if dst.size * 8 > value.bits() { + current.sign_ext(dst.size * 8 - value.bits()) + } else { + current.extract(dst.size * 8 - 1, 0) + }; + Ok(SymValue::symbolic_tainted( + extended, + dst.size * 8, + value.get_taint(), + )) + } + BoolNot { src, .. } => Ok(self.eval_ssa_var(src)?.not(ctx)), + BoolAnd { a, b, .. } => Ok(self.eval_ssa_var(a)?.and(ctx, &self.eval_ssa_var(b)?)), + BoolOr { a, b, .. } => Ok(self.eval_ssa_var(a)?.or(ctx, &self.eval_ssa_var(b)?)), + BoolXor { a, b, .. } => Ok(self.eval_ssa_var(a)?.xor(ctx, &self.eval_ssa_var(b)?)), + Piece { hi, lo, .. } => Ok(self.eval_ssa_var(hi)?.concat(ctx, &self.eval_ssa_var(lo)?)), + Subpiece { dst, src, offset } => { + let value = self.eval_ssa_var(src)?; + let low = offset.saturating_mul(8); + let high = low.saturating_add(dst.size * 8).saturating_sub(1); + Ok(value.extract(ctx, high, low)) + } + CallDefine { dst } => self.eval_call_define(inst_id, block_addr, dst), + _ => Err(EvalUnsupported::Unsupported), + } + } + + fn eval_call_define( + &mut self, + inst_id: r2ssa::graph::InstId, + block_addr: u64, + dst: &r2ssa::SSAVar, + ) -> Result, EvalUnsupported> { + let Some(call_ctx) = self.call_context_for_inst(inst_id, block_addr) else { + return Err(EvalUnsupported::Unsupported); + }; + if !register_aliases(call_ctx.callconv.ret_register_name()) + .iter() + .any(|alias| dst.name.eq_ignore_ascii_case(alias)) + { + return Err(EvalUnsupported::Unsupported); + } + let (value, coverage) = summary_return_value(self.state, call_ctx)?; + if let Some(coverage) = coverage { + self.note_assumption(coverage); + } + Ok(value) + } + + fn call_context_for_inst( + &self, + inst_id: r2ssa::graph::InstId, + block_addr: u64, + ) -> Option<&CallTransformContext<'ctx>> { + let (inst_block_addr, op_idx) = self.func.inst_op_site(inst_id)?; + debug_assert_eq!(inst_block_addr, block_addr); + self.func.get_block(block_addr)?; + for scan_idx in (0..op_idx).rev() { + let scan_inst = self + .func + .graph() + .inst_id_for_op_site(block_addr, scan_idx)?; + let Some(call_id) = self.func.call_sites().by_inst.get(&scan_inst).copied() else { + continue; + }; + if let Some(context) = self.call_contexts.get(&call_id) { + return Some(context); + } + } + let predecessors = + if let Some(predecessor) = self.phi_predecessors.get(&block_addr).copied() { + vec![predecessor] + } else { + let preds = self.func.predecessors(block_addr); + if preds.len() == 1 { preds } else { Vec::new() } + }; + for predecessor in predecessors { + let predecessor_block = self.func.get_block(predecessor)?; + for scan_idx in (0..predecessor_block.ops.len()).rev() { + let scan_inst = self + .func + .graph() + .inst_id_for_op_site(predecessor, scan_idx)?; + let Some(call_id) = self.func.call_sites().by_inst.get(&scan_inst).copied() else { + continue; + }; + if let Some(context) = self.call_contexts.get(&call_id) { + return Some(context); + } + } + } + None + } + + fn summary_memory_value( + &mut self, + call_ctx: &CallTransformContext<'ctx>, + addr: &SymValue<'ctx>, + resolved: &[NormalizedMemoryLocation], + structural: &[NormalizedMemoryLocation], + size: u32, + ) -> Option> { + let mut actual_locations = resolved.to_vec(); + if actual_locations.is_empty() { + actual_locations.extend(structural.iter().cloned()); + } + if actual_locations.is_empty() { + actual_locations.extend(summary_memory_locations(call_ctx, addr)); + } + if actual_locations.is_empty() { + return None; + } + let fallback_summary_locations = summary_memory_locations(call_ctx, addr); + let substitutions = build_call_substitutions(self.state, call_ctx); + let location_groups = group_normalized_locations(&actual_locations); + let candidate_groups = if location_groups.len() <= 1 { + location_groups + } else if let Some(best_group) = select_best_location_group(location_groups) { + vec![best_group] + } else { + self.memory_residual_fallbacks += 1; + return None; + }; + for location_group in candidate_groups { + let summary_group = match self.summary_match_locations(call_ctx, &location_group) { + SummaryLocationMatch::Match(group) => group, + SummaryLocationMatch::NoMatch => { + if fallback_summary_locations.is_empty() { + continue; + } + fallback_summary_locations.clone() + } + SummaryLocationMatch::Residual => { + self.memory_residual_fallbacks += 1; + continue; + } + }; + let mut matches = call_ctx + .summary + .cases + .iter() + .filter_map(|case| { + let covering = + select_covering_writes(&case.memory_writes, &summary_group, size); + if covering.is_empty() || covering_writes_are_ambiguous(&covering) { + return None; + } + let (location, write) = select_best_covering_write(covering)?; + let guard = substitute_bool(&case.guard, &substitutions); + let value = substitute_value( + self.state.context(), + &slice_write_value(self.state.context(), write, location.offset, size), + &substitutions, + ); + Some((guard, value)) + }) + .collect::>(); + let alias_heavy = call_ctx.summary.cases.iter().any(|case| { + covering_writes_are_ambiguous(&select_covering_writes( + &case.memory_writes, + &summary_group, + size, + )) + }); + if alias_heavy { + self.memory_residual_fallbacks += 1; + continue; + } + if matches.is_empty() { + continue; + } + + let mut merged = self.state.mem_read(addr, size); + for (guard, value) in matches.drain(..).rev() { + merged = ite_value(self.state.context(), &guard, &value, &merged); + } + self.record_memory_term(&location_group, size, &merged); + self.memory_substitutions += 1; + return Some(merged); + } + None + } + + fn summary_match_locations( + &self, + call_ctx: &CallTransformContext<'ctx>, + actual_group: &[NormalizedMemoryLocation], + ) -> SummaryLocationMatch { + if actual_group.is_empty() { + return SummaryLocationMatch::NoMatch; + } + if actual_group + .iter() + .all(|location| matches!(location.region, BackwardMemoryRegion::Argument { .. })) + { + return SummaryLocationMatch::Match(actual_group.to_vec()); + } + let mut actual_regions = actual_group + .iter() + .filter_map(|location| match &location.region { + BackwardMemoryRegion::Region(region) => Some(region.clone()), + BackwardMemoryRegion::Argument { .. } => None, + }) + .collect::>(); + if actual_regions.len() != 1 { + return SummaryLocationMatch::Residual; + } + + let actual_region = actual_regions.pop_first().expect("single region"); + if actual_region.kind == MemoryRegionKind::EscapedUnknown { + return SummaryLocationMatch::NoMatch; + } + + let mut translated = BTreeMap::>::new(); + let pointer_args = summary_pointer_arg_indices(call_ctx); + for (arg_index, base) in call_ctx.args.iter().enumerate() { + if !pointer_args.is_empty() && !pointer_args.contains(&arg_index) { + continue; + } + let Some(base_locations) = self.resolved_memory_locations(base, 1) else { + continue; + }; + for base_location in base_locations { + let BackwardMemoryRegion::Region(base_region) = &base_location.region else { + continue; + }; + if base_region.kind == MemoryRegionKind::EscapedUnknown { + continue; + } + if *base_region != actual_region { + continue; + } + let offsets = translated.entry(arg_index).or_default(); + for actual in actual_group { + offsets.insert(actual.offset.saturating_sub(base_location.offset)); + } + } + } + + match translated.len() { + 0 => SummaryLocationMatch::NoMatch, + 1 => SummaryLocationMatch::Match(translated_arg_locations( + translated + .into_iter() + .next() + .expect("single translated arg"), + )), + _ => select_best_translated_arg(translated) + .map(translated_arg_locations) + .map(SummaryLocationMatch::Match) + .unwrap_or(SummaryLocationMatch::Residual), + } + } + + fn local_memory_value( + &mut self, + inst_id: r2ssa::graph::InstId, + size: u32, + ) -> Option> { + let value = self.local_memory_store_var(inst_id, size)?; + self.eval_ssa_var(&value).ok() + } + + fn local_memory_store_var( + &self, + inst_id: r2ssa::graph::InstId, + size: u32, + ) -> Option { + let uses = self.func.memory().uses_by_inst.get(&inst_id)?; + for use_fact in uses { + if use_fact.location.size != size { + continue; + } + for (def_inst, defs) in &self.func.memory().defs_by_inst { + for def in defs { + if def.next_version != use_fact.version || def.location != use_fact.location { + continue; + } + let inst = self.func.graph().inst(*def_inst)?; + let InstPayload::Op(r2ssa::SSAOp::Store { val, .. }) = &inst.payload else { + continue; + }; + return Some(val.clone()); + } + } + } + None + } + + fn record_memory_term( + &mut self, + locations: &[NormalizedMemoryLocation], + size: u32, + value: &SymValue<'ctx>, + ) -> bool { + if locations.is_empty() { + return false; + } + let mut grouped = BTreeMap::>::new(); + for location in locations { + grouped + .entry(location.region.clone()) + .or_default() + .insert(location.offset); + } + if grouped.len() > 1 { + self.memory_residual_fallbacks += 1; + } + for (region, offsets) in grouped { + let offset_lo = offsets.iter().copied().min().unwrap_or(0); + let offset_hi = offsets.iter().copied().max().unwrap_or(0); + let exact_offset = offset_lo == offset_hi; + let value_expr = value.to_string(); + let expr = backward_memory_term_expr(®ion, offset_lo, offset_hi, &value_expr); + let evidence = + inferred_memory_term_evidence(®ion, offset_lo, offset_hi, exact_offset); + self.memory_terms.push(BackwardMemoryCondition { + region, + offset_lo, + offset_hi, + size, + exact_offset, + evidence, + binding: None, + expr, + value_expr: Some(value_expr), + exact_value: value.is_concrete(), + }); + } + true + } + + fn normalized_memory_locations( + &mut self, + addr: &r2ssa::SSAVar, + ) -> Option> { + if let Some(arg_index) = ssa_var_arg_index(addr) { + return Some(vec![NormalizedMemoryLocation { + region: BackwardMemoryRegion::Argument { index: arg_index }, + offset: 0, + }]); + } + let value_id = self.func.graph().value_id_for_var(addr)?; + self.normalized_memory_location_value_id(value_id) + } + + fn resolved_memory_locations( + &self, + addr: &SymValue<'ctx>, + size: u32, + ) -> Option> { + let mut constraints = self.state.constraints().to_vec(); + constraints.extend(self.assumption_constraints.iter().cloned()); + let resolved = self.state.memory.resolve_pointer(addr, size, &constraints); + let mut locations = Vec::new(); + for pointer in resolved.pointers { + let Some(region) = self.region_for_pointer(pointer.region_id) else { + continue; + }; + let Ok(offset) = i64::try_from(pointer.offset) else { + continue; + }; + locations.push(NormalizedMemoryLocation { region, offset }); + } + (!locations.is_empty()).then_some(locations) + } + + fn region_for_pointer(&self, region_id: MemoryRegionId) -> Option { + let def = self.state.memory.region_def(region_id)?; + Some(BackwardMemoryRegion::Region(BackwardRegionRef { + id: def.id, + kind: def.kind.clone(), + name: def.name.clone(), + })) + } + + fn normalized_memory_location_value_id( + &mut self, + value_id: ValueId, + ) -> Option> { + let inst_id = self.func.graph().def_inst(value_id)?; + let inst = self.func.graph().inst(inst_id)?; + let InstPayload::Op(op) = &inst.payload else { + return None; + }; + use r2ssa::SSAOp::*; + match op { + Copy { src, .. } | Cast { src, .. } => self.normalized_memory_locations(src), + IntAdd { a, b, .. } => { + if let Some(base) = self.normalized_memory_locations(a) + && let Some(offsets) = self.resolve_delta_offsets(b, 1) + { + return Some(apply_delta_offsets(&base, &offsets, true)); + } + if let Some(base) = self.normalized_memory_locations(b) + && let Some(offsets) = self.resolve_delta_offsets(a, 1) + { + return Some(apply_delta_offsets(&base, &offsets, true)); + } + None + } + PtrAdd { + base, + index, + element_size, + .. + } => { + if let Some(base) = self.normalized_memory_locations(base) + && let Some(offsets) = self.resolve_delta_offsets(index, *element_size as i64) + { + return Some(apply_delta_offsets(&base, &offsets, true)); + } + None + } + IntSub { a, b, .. } => { + if let Some(base) = self.normalized_memory_locations(a) + && let Some(offsets) = self.resolve_delta_offsets(b, 1) + { + return Some(apply_delta_offsets(&base, &offsets, false)); + } + None + } + PtrSub { + base, + index, + element_size, + .. + } => { + if let Some(base) = self.normalized_memory_locations(base) + && let Some(offsets) = self.resolve_delta_offsets(index, *element_size as i64) + { + return Some(apply_delta_offsets(&base, &offsets, false)); + } + None + } + IntZExt { src, .. } | IntSExt { src, .. } => self.normalized_memory_locations(src), + Subpiece { src, offset, .. } if *offset == 0 => self.normalized_memory_locations(src), + _ => None, + } + } + + fn resolve_delta_offsets(&mut self, delta: &r2ssa::SSAVar, scale: i64) -> Option> { + let delta_value = self.eval_ssa_var(delta).ok()?; + if let Some(concrete) = delta_value.as_concrete() { + return Some(vec![(concrete as i64).saturating_mul(scale)]); + } + let candidates = self.enumerate_bounded_concrete_values(&delta_value)?; + if candidates.is_empty() { + return None; + } + self.memory_candidate_enumerations += 1; + Some( + candidates + .into_iter() + .map(|value| (value as i64).saturating_mul(scale)) + .collect(), + ) + } + + fn enumerate_bounded_concrete_values(&mut self, value: &SymValue<'ctx>) -> Option> { + let ctx = self.state.context(); + let bv = value.to_bv(ctx); + let solver = Solver::new(); + for constraint in self.state.constraints() { + solver.assert(constraint); + } + for constraint in &self.assumption_constraints { + solver.assert(constraint); + } + + let mut values = BTreeSet::new(); + loop { + match solver.check() { + Z3SatResult::Sat => { + let model = solver.get_model()?; + let concrete = model.eval(&bv, true)?.as_u64()?; + values.insert(concrete); + if values.len() > DEFAULT_MAX_NORMALIZED_OFFSETS { + self.memory_residual_fallbacks += 1; + return None; + } + solver.assert(bv.eq(BV::from_u64(concrete, value.bits())).not()); + } + Z3SatResult::Unsat => break, + Z3SatResult::Unknown => { + self.memory_residual_fallbacks += 1; + return None; + } + } + } + + Some(values.into_iter().collect()) + } +} + +pub fn compile_target_precondition<'ctx>( + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + target_addr: u64, +) -> Option { + compile_target_precondition_with_summaries(func, initial_state, target_addr, &HashMap::new()) +} + +pub(crate) fn compile_target_precondition_with_summaries<'ctx>( + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + target_addr: u64, + call_summaries: &HashMap>, +) -> Option { + let reverse_paths = enumerate_reverse_paths(func, target_addr, DEFAULT_REVERSE_PATH_LIMIT)?; + compile_reverse_paths(func, initial_state, reverse_paths, None, call_summaries) +} + +pub(crate) fn compile_branch_precondition_with_summaries<'ctx>( + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + block_addr: u64, + truth: bool, + call_summaries: &HashMap>, +) -> Option { + let predicate = func + .predicates() + .predicates + .iter() + .find_map(|(id, fact)| (fact.block_addr == block_addr).then_some(*id))?; + let reverse_paths = enumerate_reverse_paths(func, block_addr, DEFAULT_REVERSE_PATH_LIMIT)?; + compile_reverse_paths( + func, + initial_state, + reverse_paths, + Some((predicate, truth)), + call_summaries, + ) +} + +pub fn compile_derived_summary_return_postcondition<'ctx, F>( + state: &SymState<'ctx>, + summary: &DerivedFunctionSummary<'ctx>, + callconv: &CallConv, + postcondition: F, +) -> Option +where + F: Fn(&SymValue<'ctx>) -> Bool, +{ + if summary.cases.is_empty() { + return None; + } + + let call = + callconv.collect_call_info(state, summary.arg_count_hint.max(callconv.arg_capacity())); + let substitutions = build_summary_substitutions(state, summary, &call); + let mut terms = Vec::new(); + for case in &summary.cases { + let Some(return_value) = &case.return_value else { + continue; + }; + let guard = substitute_bool(&case.guard, &substitutions); + let value = substitute_value(state.context(), return_value, &substitutions); + let post = postcondition(&value); + terms.push(guard & post); + } + if terms.is_empty() { + return None; + } + let predicate = or_all(state.context(), &terms); + let simplified = predicate.simplify(); + Some(CompiledBackwardCondition { + summary: BackwardConditionSummary { + simplified: simplified.to_string(), + terms: terms + .iter() + .map(|term| term.simplify().to_string()) + .collect(), + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: if matches!( + summary.completion, + crate::sim::DerivedSummaryCompletion::Exact + ) { + BackwardConditionPrecision::Exact + } else { + BackwardConditionPrecision::ResidualSearchRequired + }, + supported_paths: terms.len(), + total_paths: terms.len(), + }, + predicate: simplified, + }) +} + +pub fn compile_derived_summary_memory_postcondition<'ctx, F>( + state: &SymState<'ctx>, + summary: &DerivedFunctionSummary<'ctx>, + callconv: &CallConv, + arg_index: usize, + offset: i64, + size: u32, + postcondition: F, +) -> Option +where + F: Fn(&SymValue<'ctx>) -> Bool, +{ + if summary.cases.is_empty() { + return None; + } + + let call = + callconv.collect_call_info(state, summary.arg_count_hint.max(callconv.arg_capacity())); + let substitutions = build_call_substitutions( + state, + &CallTransformContext { + summary: Rc::new(summary.clone()), + callconv: callconv.clone(), + args: call.args.clone(), + }, + ); + let base = call.args.get(arg_index)?; + let addr = add_signed_offset(state.context(), base, offset, call.arg_bits); + let mut terms = Vec::new(); + for case in &summary.cases { + let Some((location, write)) = select_covering_write( + &case.memory_writes, + &[NormalizedMemoryLocation { + region: BackwardMemoryRegion::Argument { index: arg_index }, + offset, + }], + size, + ) else { + continue; + }; + let guard = substitute_bool(&case.guard, &substitutions); + let value = substitute_value( + state.context(), + &slice_write_value(state.context(), write, location.offset, size), + &substitutions, + ); + terms.push(guard & postcondition(&value)); + } + if terms.is_empty() { + return None; + } + let predicate = or_all(state.context(), &terms); + let simplified = predicate.simplify(); + let memory_term = state + .memory + .resolve_pointer(&addr, size, state.constraints()) + .pointers + .into_iter() + .find_map(|pointer| { + state.memory.region_def(pointer.region_id).map(|def| { + let region = BackwardMemoryRegion::Region(BackwardRegionRef { + id: def.id, + kind: def.kind.clone(), + name: def.name.clone(), + }); + let offset = i64::try_from(pointer.offset).unwrap_or(0); + BackwardMemoryCondition { + region: region.clone(), + offset_lo: offset, + offset_hi: offset, + size, + exact_offset: true, + evidence: if matches!( + summary.completion, + crate::sim::DerivedSummaryCompletion::Exact + ) { + SemanticEvidence::exact() + } else { + SemanticEvidence::likely(SemanticEvidenceReason::PartialPathCoverage) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized) + }, + binding: None, + expr: format_backward_memory_location(®ion, offset), + value_expr: None, + exact_value: false, + } + }) + }) + .unwrap_or(BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: arg_index }, + offset_lo: offset, + offset_hi: offset, + size, + exact_offset: true, + evidence: if matches!( + summary.completion, + crate::sim::DerivedSummaryCompletion::Exact + ) { + SemanticEvidence::exact() + } else { + SemanticEvidence::likely(SemanticEvidenceReason::PartialPathCoverage) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized) + }, + binding: None, + expr: format_backward_memory_location( + &BackwardMemoryRegion::Argument { index: arg_index }, + offset, + ), + value_expr: None, + exact_value: false, + }); + Some(CompiledBackwardCondition { + summary: BackwardConditionSummary { + simplified: simplified.to_string(), + terms: terms + .iter() + .map(|term| term.simplify().to_string()) + .collect(), + memory_terms: vec![memory_term], + backward_memory_substitutions: 1, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: if matches!( + summary.completion, + crate::sim::DerivedSummaryCompletion::Exact + ) { + BackwardConditionPrecision::Exact + } else { + BackwardConditionPrecision::ResidualSearchRequired + }, + supported_paths: terms.len(), + total_paths: terms.len(), + }, + predicate: simplified, + }) +} + +fn enumerate_reverse_paths( + func: &SsaArtifact, + target_addr: u64, + limit: usize, +) -> Option<(Vec, bool)> { + func.get_block(target_addr)?; + + let mut pending = vec![ReversePath { + block_addr: target_addr, + phi_predecessors: BTreeMap::new(), + assumptions: Vec::new(), + visited: BTreeSet::from([target_addr]), + }]; + let mut completed = Vec::new(); + let mut truncated = false; + + while let Some(path) = pending.pop() { + if completed.len() >= limit { + truncated = true; + break; + } + if path.block_addr == func.entry { + completed.push(path); + continue; + } + + let predecessors = func.predecessors(path.block_addr); + if predecessors.is_empty() { + continue; + } + + for predecessor in predecessors { + if path.visited.contains(&predecessor) { + truncated = true; + continue; + } + let mut next = path.clone(); + next.block_addr = predecessor; + next.visited.insert(predecessor); + next.phi_predecessors.insert(path.block_addr, predecessor); + if let Some(assumptions) = func.predicates().block_assumptions.get(&path.block_addr) { + for assumption in assumptions + .iter() + .filter(|assumption| assumption.predecessor == predecessor) + { + next.assumptions + .push((assumption.predicate, assumption.truth)); + } + } + pending.push(next); + } + } + + Some((completed, truncated)) +} + +fn compile_reverse_paths<'ctx>( + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + (paths, truncated): (Vec, bool), + extra_predicate: Option<(PredicateId, bool)>, + call_summaries: &HashMap>, +) -> Option { + compile_reverse_paths_with_extra( + func, + initial_state, + (paths, truncated), + extra_predicate, + call_summaries, + |_, _| Ok(None), + ) +} + +fn compile_reverse_paths_with_extra<'ctx, F>( + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + (paths, truncated): (Vec, bool), + extra_predicate: Option<(PredicateId, bool)>, + call_summaries: &HashMap>, + mut extra_constraint: F, +) -> Option +where + F: for<'a> FnMut( + &mut ValueTranslator<'a, 'ctx>, + &ReversePath, + ) -> Result, EvalUnsupported>, +{ + if paths.is_empty() { + return None; + } + + let mut supported_terms = Vec::new(); + let mut memory_terms = Vec::new(); + let mut unsupported_paths = 0usize; + let mut used_unsummarized_memory = false; + let mut backward_memory_substitutions = 0usize; + let mut backward_memory_candidate_enumerations = 0usize; + let mut backward_memory_residual_fallbacks = 0usize; + for path in &paths { + let call_contexts = + build_call_transform_contexts(func, initial_state, path, call_summaries); + let mut translator = + ValueTranslator::new(func, initial_state, &path.phi_predecessors, &call_contexts); + let mut conjuncts = Vec::new(); + let mut supported = true; + for (predicate, truth) in &path.assumptions { + match translator.eval_predicate(*predicate, *truth) { + Ok(condition) => { + translator.note_assumption(condition.clone()); + conjuncts.push(condition); + } + Err(_) => { + supported = false; + break; + } + } + } + if supported && let Some((predicate, truth)) = extra_predicate { + match translator.eval_predicate(predicate, truth) { + Ok(condition) => { + translator.note_assumption(condition.clone()); + conjuncts.push(condition); + } + Err(_) => supported = false, + } + } + if supported { + match extra_constraint(&mut translator, path) { + Ok(Some(condition)) => { + translator.note_assumption(condition.clone()); + conjuncts.push(condition); + } + Ok(None) => {} + Err(_) => supported = false, + } + } + if !supported { + unsupported_paths += 1; + continue; + } + conjuncts.extend(translator.assumption_constraints.iter().cloned()); + let term = and_all(initial_state.context(), &conjuncts); + used_unsummarized_memory |= translator.used_unsummarized_memory; + backward_memory_substitutions += translator.memory_substitutions; + backward_memory_candidate_enumerations += translator.memory_candidate_enumerations; + backward_memory_residual_fallbacks += translator.memory_residual_fallbacks; + supported_terms.push(term); + memory_terms.extend(translator.memory_terms); + } + + if supported_terms.is_empty() { + return None; + } + + let predicate = or_all(initial_state.context(), &supported_terms); + let simplified = predicate.simplify(); + let precision = if unsupported_paths == 0 + && !truncated + && !used_unsummarized_memory + && backward_memory_residual_fallbacks == 0 + { + BackwardConditionPrecision::Exact + } else if unsupported_paths > 0 + || truncated + || used_unsummarized_memory + || backward_memory_residual_fallbacks > 0 + { + BackwardConditionPrecision::ResidualSearchRequired + } else { + BackwardConditionPrecision::OverApprox + }; + Some(CompiledBackwardCondition { + summary: BackwardConditionSummary { + simplified: simplified.to_string(), + terms: supported_terms + .iter() + .map(|term| term.simplify().to_string()) + .collect(), + memory_terms, + backward_memory_substitutions, + backward_memory_candidate_enumerations, + backward_memory_residual_fallbacks, + precision, + supported_paths: supported_terms.len(), + total_paths: paths.len(), + }, + predicate: simplified, + }) +} + +pub(crate) fn compile_value_postcondition_with_summaries<'ctx, F>( + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + block_addr: u64, + value_var: r2ssa::SSAVar, + postcondition: F, + call_summaries: &HashMap>, +) -> Option +where + F: Fn(&SymValue<'ctx>) -> Bool + Clone, +{ + let reverse_paths = enumerate_reverse_paths(func, block_addr, DEFAULT_REVERSE_PATH_LIMIT)?; + compile_reverse_paths_with_extra( + func, + initial_state, + reverse_paths, + None, + call_summaries, + move |translator, path| { + let _ = path; + let value = translator.eval_ssa_var(&value_var)?; + Ok(Some(postcondition.clone()(&value))) + }, + ) +} + +fn build_call_transform_contexts<'ctx>( + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + path: &ReversePath, + call_summaries: &HashMap>, +) -> HashMap> { + if call_summaries.is_empty() { + return HashMap::new(); + } + + let sequence = path_block_sequence(path); + let mut arg_state = BTreeMap::::new(); + let mut contexts = HashMap::new(); + + for (seq_index, block_addr) in sequence.iter().enumerate() { + let Some(block) = func.get_block(*block_addr) else { + continue; + }; + if seq_index > 0 { + let predecessor = sequence[seq_index - 1]; + for phi in &block.phis { + if let Some(arg_index) = ssa_var_arg_index(&phi.dst) + && let Some((_, source)) = + phi.sources.iter().find(|(pred, _)| *pred == predecessor) + && let Some(value_id) = func.graph().value_id_for_var(source) + { + arg_state.insert(arg_index, value_id); + } + } + } + + for (op_idx, op) in block.ops.iter().enumerate() { + let Some(inst_id) = func.graph().inst_id_for_op_site(*block_addr, op_idx) else { + continue; + }; + if let Some(call_id) = func.call_sites().by_inst.get(&inst_id).copied() + && let Some(callsite) = func.call_sites().by_id.get(&call_id) + && let Some(target) = callsite.direct_target + && let Some(view) = call_summaries.get(&target) + { + let context_snapshot = contexts.clone(); + let mut translator = ValueTranslator::new( + func, + initial_state, + &path.phi_predecessors, + &context_snapshot, + ); + let args = (0..view.callconv.arg_capacity()) + .map(|index| { + arg_state + .get(&index) + .copied() + .and_then(|value_id| translator.eval_value_id(value_id).ok()) + .unwrap_or_else(|| { + view.callconv + .arg_register_name(index) + .map(|reg| { + read_register_from_state( + initial_state, + reg, + view.callconv.arg_bits(), + ) + }) + .unwrap_or_else(|| SymValue::unknown(view.callconv.arg_bits())) + }) + }) + .collect::>(); + contexts.insert( + call_id, + CallTransformContext { + summary: view.summary.clone(), + callconv: view.callconv.clone(), + args, + }, + ); + } + + if let Some(dst) = op.dst() + && let Some(arg_index) = ssa_var_arg_index(dst) + && let Some(value_id) = func.graph().value_id_for_var(dst) + { + arg_state.insert(arg_index, value_id); + } + } + } + + contexts +} + +fn path_block_sequence(path: &ReversePath) -> Vec { + let mut blocks = vec![path.block_addr]; + let mut current = path.block_addr; + while let Some(predecessor) = path.phi_predecessors.get(¤t).copied() { + blocks.push(predecessor); + current = predecessor; + } + blocks.reverse(); + blocks +} + +fn and_all(_ctx: &Context, terms: &[Bool]) -> Bool { + match terms { + [] => Bool::from_bool(true), + [term] => term.clone(), + _ => { + let refs = terms.iter().collect::>(); + Bool::and(&refs) + } + } +} + +fn or_all(_ctx: &Context, terms: &[Bool]) -> Bool { + match terms { + [] => Bool::from_bool(false), + [term] => term.clone(), + _ => { + let refs = terms.iter().collect::>(); + Bool::or(&refs) + } + } +} + +fn eval_const_var<'ctx>(var: &r2ssa::SSAVar) -> Result, EvalUnsupported> { + if let Some(hex) = var.name.strip_prefix("const:") { + u64::from_str_radix(hex, 16) + .map(|value| SymValue::concrete(value, var.size * 8)) + .map_err(|_| EvalUnsupported::Unsupported) + } else { + Err(EvalUnsupported::Unsupported) + } +} + +fn read_input_var<'ctx>(state: &SymState<'ctx>, var: &r2ssa::SSAVar) -> SymValue<'ctx> { + let key = var.display_name(); + let value = state.get_register_sized(&key, var.size * 8); + if value.is_unknown() && var.version == 0 { + let base = var.name.strip_prefix("reg:").unwrap_or(&var.name); + state.get_register_sized(&base.to_ascii_uppercase(), var.size * 8) + } else { + value + } +} + +fn value_to_bool(ctx: &Context, value: &SymValue<'_>) -> Bool { + value.to_bv(ctx).eq(BV::from_u64(0, value.bits())).not() +} + +fn build_summary_substitutions<'ctx>( + state: &SymState<'ctx>, + summary: &DerivedFunctionSummary<'ctx>, + call: &crate::sim::CallInfo<'ctx>, +) -> Vec<(BV, BV)> { + let mut substitutions = Vec::new(); + for (index, symbol) in &summary.arg_symbols { + let Some(actual) = call.args.get(*index) else { + continue; + }; + substitutions.push((symbol.to_bv(state.context()), actual.to_bv(state.context()))); + } + for input in &summary.memory_inputs { + let Some(base) = call.args.get(input.arg_index) else { + continue; + }; + substitutions.push(( + input.symbol.to_bv(state.context()), + state.mem_read(base, input.size).to_bv(state.context()), + )); + } + substitutions +} + +fn build_call_substitutions<'ctx>( + state: &SymState<'ctx>, + call_ctx: &CallTransformContext<'ctx>, +) -> Vec<(BV, BV)> { + let mut substitutions = Vec::new(); + for (index, symbol) in &call_ctx.summary.arg_symbols { + let Some(actual) = call_ctx.args.get(*index) else { + continue; + }; + substitutions.push((symbol.to_bv(state.context()), actual.to_bv(state.context()))); + } + for input in &call_ctx.summary.memory_inputs { + let Some(base) = call_ctx.args.get(input.arg_index) else { + continue; + }; + substitutions.push(( + input.symbol.to_bv(state.context()), + state.mem_read(base, input.size).to_bv(state.context()), + )); + } + substitutions +} + +fn summary_return_value<'ctx>( + state: &SymState<'ctx>, + call_ctx: &CallTransformContext<'ctx>, +) -> Result<(SymValue<'ctx>, Option), EvalUnsupported> { + if call_ctx.summary.cases.is_empty() { + return Err(EvalUnsupported::Unsupported); + } + let substitutions = build_call_substitutions(state, call_ctx); + let mut merged = SymValue::unknown(call_ctx.callconv.ret_bits()); + let mut matched = false; + let mut guards = Vec::new(); + for case in call_ctx.summary.cases.iter().rev() { + let Some(return_value) = &case.return_value else { + continue; + }; + let guard = substitute_bool(&case.guard, &substitutions); + let value = substitute_value(state.context(), return_value, &substitutions); + merged = ite_value(state.context(), &guard, &value, &merged); + guards.push(guard); + matched = true; + } + matched + .then(|| { + let coverage = matches!( + call_ctx.summary.completion, + crate::sim::DerivedSummaryCompletion::Exact + ) + .then(|| or_all(state.context(), &guards)); + (merged, coverage) + }) + .ok_or(EvalUnsupported::Unsupported) +} + +fn summary_memory_locations<'ctx>( + call_ctx: &CallTransformContext<'ctx>, + addr: &SymValue<'ctx>, +) -> Vec { + let Some(concrete_addr) = addr.as_concrete() else { + return Vec::new(); + }; + let pointer_args = summary_pointer_arg_indices(call_ctx); + let mut locations = Vec::new(); + for (index, base) in call_ctx.args.iter().enumerate() { + if !pointer_args.is_empty() && !pointer_args.contains(&index) { + continue; + } + let Some(base_addr) = base.as_concrete() else { + continue; + }; + if let Some(offset) = signed_offset_between(base_addr, concrete_addr) { + locations.push(NormalizedMemoryLocation { + region: BackwardMemoryRegion::Argument { index }, + offset, + }); + } + } + locations +} + +fn summary_pointer_arg_indices<'ctx>(call_ctx: &CallTransformContext<'ctx>) -> BTreeSet { + let mut indices = BTreeSet::new(); + for input in &call_ctx.summary.memory_inputs { + indices.insert(input.arg_index); + } + for case in &call_ctx.summary.cases { + for write in &case.memory_writes { + indices.insert(write.arg_index); + } + } + indices +} + +fn translated_arg_locations( + (arg_index, offsets): (usize, BTreeSet), +) -> Vec { + offsets + .into_iter() + .map(|offset| NormalizedMemoryLocation { + region: BackwardMemoryRegion::Argument { index: arg_index }, + offset, + }) + .collect() +} + +fn memory_region_rank(region: &BackwardMemoryRegion) -> u8 { + match region { + BackwardMemoryRegion::Argument { .. } => 0, + BackwardMemoryRegion::Region(region) => match region.kind { + MemoryRegionKind::Stack => 1, + MemoryRegionKind::Global => 2, + MemoryRegionKind::Replay => 3, + MemoryRegionKind::Input => 4, + MemoryRegionKind::Heap => 5, + MemoryRegionKind::EscapedUnknown => 6, + }, + } +} + +fn offsets_span(offsets: &BTreeSet) -> u64 { + match (offsets.first().copied(), offsets.last().copied()) { + (Some(lo), Some(hi)) => hi.saturating_sub(lo).unsigned_abs(), + _ => 0, + } +} + +fn location_group_rank(group: &[NormalizedMemoryLocation]) -> Option { + let region = group.first()?.region.clone(); + let offsets = group + .iter() + .map(|location| location.offset) + .collect::>(); + let span = offsets_span(&offsets); + Some(LocationGroupRank { + region_rank: memory_region_rank(®ion), + inexact_offset: span != 0, + span, + offset_count: offsets.len(), + }) +} + +fn location_group_tie_break(group: &[NormalizedMemoryLocation]) -> Option { + let region = group.first()?.region.clone(); + let min_offset = group + .iter() + .map(|location| location.offset) + .min() + .unwrap_or(0); + Some(match region { + BackwardMemoryRegion::Argument { index } => LocationGroupTieBreak { + region_discriminant: 0, + region_id: 0, + arg_index: index, + min_offset, + }, + BackwardMemoryRegion::Region(region) => LocationGroupTieBreak { + region_discriminant: 1, + region_id: region.id.0, + arg_index: 0, + min_offset, + }, + }) +} + +fn select_best_location_group( + groups: Vec>, +) -> Option> { + if groups.len() <= 1 { + return groups.into_iter().next(); + } + + let mut ranked = groups + .into_iter() + .filter_map(|group| { + Some(( + location_group_rank(&group)?, + location_group_tie_break(&group)?, + group, + )) + }) + .collect::>(); + ranked.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1))); + if ranked.len() > 1 && ranked[0].0 == ranked[1].0 { + None + } else { + ranked.into_iter().next().map(|(_, _, group)| group) + } +} + +fn translated_offsets_rank(offsets: &BTreeSet) -> (bool, u64, usize) { + let span = offsets_span(offsets); + (span != 0, span, offsets.len()) +} + +fn select_best_translated_arg( + translated: BTreeMap>, +) -> Option<(usize, BTreeSet)> { + if translated.len() <= 1 { + return translated.into_iter().next(); + } + + let mut ranked = translated + .into_iter() + .map(|(arg_index, offsets)| (translated_offsets_rank(&offsets), arg_index, offsets)) + .collect::>(); + ranked.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1))); + if ranked.len() > 1 && ranked[0].0 == ranked[1].0 { + None + } else { + ranked + .into_iter() + .next() + .map(|(_, arg_index, offsets)| (arg_index, offsets)) + } +} + +fn group_normalized_locations( + locations: &[NormalizedMemoryLocation], +) -> Vec> { + let mut grouped = BTreeMap::>::new(); + for location in locations { + grouped + .entry(location.region.clone()) + .or_default() + .insert(location.offset); + } + grouped + .into_iter() + .map(|(region, offsets)| { + offsets + .into_iter() + .map(|offset| NormalizedMemoryLocation { + region: region.clone(), + offset, + }) + .collect() + }) + .collect() +} + +fn apply_delta_offsets( + base_locations: &[NormalizedMemoryLocation], + deltas: &[i64], + add: bool, +) -> Vec { + let mut merged = BTreeSet::new(); + for base in base_locations { + for delta in deltas { + let offset = if add { + base.offset.saturating_add(*delta) + } else { + base.offset.saturating_sub(*delta) + }; + merged.insert((base.region.clone(), offset)); + } + } + merged + .into_iter() + .map(|(region, offset)| NormalizedMemoryLocation { region, offset }) + .collect() +} + +fn select_covering_write<'a, 'ctx>( + writes: &'a [crate::sim::DerivedMemoryWrite<'ctx>], + locations: &[NormalizedMemoryLocation], + size: u32, +) -> Option<( + NormalizedMemoryLocation, + &'a crate::sim::DerivedMemoryWrite<'ctx>, +)> { + select_covering_writes(writes, locations, size) + .into_iter() + .min_by_key(|(location, write)| (write.size, write.offset, location.offset)) +} + +fn select_covering_writes<'a, 'ctx>( + writes: &'a [crate::sim::DerivedMemoryWrite<'ctx>], + locations: &[NormalizedMemoryLocation], + size: u32, +) -> Vec<( + NormalizedMemoryLocation, + &'a crate::sim::DerivedMemoryWrite<'ctx>, +)> { + writes + .iter() + .flat_map(|write| { + locations.iter().filter_map(move |location| { + (matches!( + location.region, + BackwardMemoryRegion::Argument { index } if index == write.arg_index + ) && { + let write_start = write.offset as i128; + let write_end = write_start.saturating_add(write.size as i128); + let read_start = location.offset as i128; + let read_end = read_start.saturating_add(size as i128); + write_start <= read_start && write_end >= read_end + }) + .then_some((location.clone(), write)) + }) + }) + .collect() +} + +fn select_best_covering_write<'a, 'ctx>( + covering: Vec<( + NormalizedMemoryLocation, + &'a crate::sim::DerivedMemoryWrite<'ctx>, + )>, +) -> Option<( + NormalizedMemoryLocation, + &'a crate::sim::DerivedMemoryWrite<'ctx>, +)> { + covering + .into_iter() + .min_by_key(|(location, write)| (write.size, write.offset, location.offset)) +} + +fn covering_writes_are_ambiguous<'a, 'ctx>( + covering: &[( + NormalizedMemoryLocation, + &'a crate::sim::DerivedMemoryWrite<'ctx>, + )], +) -> bool { + let distinct = covering + .iter() + .filter_map(|(location, write)| match location.region { + BackwardMemoryRegion::Argument { index } => { + Some((index, location.offset, write.arg_index)) + } + BackwardMemoryRegion::Region(_) => None, + }) + .collect::>(); + distinct.len() > 1 +} + +fn slice_write_value<'ctx>( + ctx: &'ctx Context, + write: &crate::sim::DerivedMemoryWrite<'ctx>, + offset: i64, + size: u32, +) -> SymValue<'ctx> { + if write.offset == offset && write.size == size { + return write.value.clone(); + } + let relative = offset.saturating_sub(write.offset) as u64; + let low_bit = (relative * 8) as u32; + let high_bit = low_bit + (size * 8) - 1; + adjust_bits(ctx, write.value.clone(), write.size * 8).extract(ctx, high_bit, low_bit) +} + +fn signed_offset_between(base: u64, addr: u64) -> Option { + if addr >= base { + i64::try_from(addr - base).ok() + } else { + i64::try_from(base - addr).ok().map(|delta| -delta) + } +} + +fn is_specific_memory_location(location: &NormalizedMemoryLocation) -> bool { + !matches!( + &location.region, + BackwardMemoryRegion::Region(BackwardRegionRef { + kind: MemoryRegionKind::EscapedUnknown, + .. + }) + ) +} + +fn ssa_var_arg_index(var: &r2ssa::SSAVar) -> Option { + let display = var.display_name(); + if let Some((prefix, _)) = split_version(&display) + && let Some(index) = callconv_arg_index(prefix) + { + return Some(index); + } + callconv_arg_index(&display).or_else(|| callconv_arg_index(&var.name)) +} + +fn callconv_arg_index(name: &str) -> Option { + let upper = name.to_ascii_uppercase(); + match upper.as_str() { + "RDI" | "EDI" => Some(0), + "RSI" | "ESI" => Some(1), + "RDX" | "EDX" => Some(2), + "RCX" | "ECX" => Some(3), + "R8" | "R8D" => Some(4), + "R9" | "R9D" => Some(5), + _ => None, + } +} + +fn register_aliases(base: &str) -> Vec<&str> { + match base { + "RAX" => vec!["RAX", "EAX"], + "RDI" => vec!["RDI", "EDI"], + "RSI" => vec!["RSI", "ESI"], + "RDX" => vec!["RDX", "EDX"], + "RCX" => vec!["RCX", "ECX"], + "R8" => vec!["R8", "R8D"], + "R9" => vec!["R9", "R9D"], + _ => vec![base], + } +} + +fn read_register_from_state<'ctx>(state: &SymState<'ctx>, base: &str, bits: u32) -> SymValue<'ctx> { + for alias in register_aliases(base) { + if let Some(key) = find_register_key(state, alias) { + return state.get_register_sized(&key, bits); + } + } + SymValue::unknown(bits) +} + +fn add_signed_offset<'ctx>( + ctx: &'ctx Context, + base: &SymValue<'ctx>, + offset: i64, + bits: u32, +) -> SymValue<'ctx> { + if offset >= 0 { + base.add(ctx, &SymValue::concrete(offset as u64, bits)) + } else { + base.sub(ctx, &SymValue::concrete(offset.unsigned_abs(), bits)) + } +} + +fn find_register_key<'ctx>(state: &SymState<'ctx>, base: &str) -> Option { + let mut best: Option<(u32, String)> = None; + for key in state.registers().keys() { + if let Some((prefix, version)) = split_version(key) { + if prefix.eq_ignore_ascii_case(base) + && best + .as_ref() + .is_none_or(|(best_version, _)| version > *best_version) + { + best = Some((version, key.clone())); + } + } else if key.eq_ignore_ascii_case(base) { + return Some(key.clone()); + } + } + best.map(|(_, key)| key) +} + +fn split_version(name: &str) -> Option<(&str, u32)> { + let (prefix, suffix) = name.rsplit_once('_')?; + if suffix.is_empty() || !suffix.chars().all(|c| c.is_ascii_digit()) { + return None; + } + let version = suffix.parse().ok()?; + Some((prefix, version)) +} + +fn substitute_value<'ctx>( + ctx: &'ctx Context, + value: &SymValue<'ctx>, + substitutions: &[(BV, BV)], +) -> SymValue<'ctx> { + let pairs = substitutions + .iter() + .map(|(from, to)| (from, to)) + .collect::>(); + SymValue::symbolic_tainted( + value.to_bv(ctx).substitute(&pairs), + value.bits(), + value.get_taint(), + ) +} + +fn substitute_bool(ast: &Bool, substitutions: &[(BV, BV)]) -> Bool { + let pairs = substitutions + .iter() + .map(|(from, to)| (from, to)) + .collect::>(); + ast.substitute(&pairs) +} + +fn ite_value<'ctx>( + ctx: &'ctx Context, + guard: &Bool, + when_true: &SymValue<'ctx>, + when_false: &SymValue<'ctx>, +) -> SymValue<'ctx> { + let bits = when_true.bits().max(when_false.bits()); + let taint = when_true.get_taint() | when_false.get_taint(); + let true_bv = adjust_bits(ctx, when_true.clone(), bits).to_bv(ctx); + let false_bv = adjust_bits(ctx, when_false.clone(), bits).to_bv(ctx); + SymValue::symbolic_tainted(guard.ite(&true_bv, &false_bv), bits, taint) +} + +fn adjust_bits<'ctx>(ctx: &'ctx Context, value: SymValue<'ctx>, bits: u32) -> SymValue<'ctx> { + if value.bits() == bits { + return value; + } + if value.bits() < bits { + value.zero_extend(ctx, bits) + } else { + value.extract(ctx, bits - 1, 0) + } +} + +#[cfg(test)] +mod tests { + use r2il::{R2ILBlock, R2ILOp, SpaceId, Varnode}; + use z3::Context; + + use super::*; + + fn make_reg(offset: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Register, + offset, + size, + meta: None, + } + } + + fn make_const(value: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: value, + size, + meta: None, + } + } + + #[test] + fn compile_target_precondition_builds_guard_for_simple_branch() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(16, 1), + a: make_reg(56, 8), + b: make_const(0x1337, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(16, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(0, 8), + src: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(0, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let compiled = compile_target_precondition(&func, &state, 0x1010).expect("compiled"); + assert_eq!( + compiled.summary.precision, + BackwardConditionPrecision::Exact + ); + assert!(compiled.summary.simplified.contains("1337") || !compiled.summary.terms.is_empty()); + } + + #[test] + fn select_best_location_group_prefers_stable_exact_region() { + let global_group = vec![NormalizedMemoryLocation { + region: BackwardMemoryRegion::Region(BackwardRegionRef { + id: MemoryRegionId(1), + kind: MemoryRegionKind::Global, + name: "global".to_string(), + }), + offset: 4, + }]; + let heap_group = vec![ + NormalizedMemoryLocation { + region: BackwardMemoryRegion::Region(BackwardRegionRef { + id: MemoryRegionId(2), + kind: MemoryRegionKind::Heap, + name: "heap".to_string(), + }), + offset: 4, + }, + NormalizedMemoryLocation { + region: BackwardMemoryRegion::Region(BackwardRegionRef { + id: MemoryRegionId(2), + kind: MemoryRegionKind::Heap, + name: "heap".to_string(), + }), + offset: 8, + }, + ]; + + let best = + select_best_location_group(vec![heap_group, global_group.clone()]).expect("best group"); + assert_eq!(best, global_group); + } + + #[test] + fn select_best_translated_arg_ties_remain_residual() { + let translated = BTreeMap::from([ + (0usize, BTreeSet::from([0i64])), + (1usize, BTreeSet::from([0i64])), + ]); + assert!(select_best_translated_arg(translated).is_none()); + } + + #[test] + fn inferred_memory_term_evidence_promotes_bounded_global_and_replay_regions_to_likely() { + let global_region = BackwardMemoryRegion::Region(BackwardRegionRef { + id: MemoryRegionId(1), + kind: MemoryRegionKind::Global, + name: "global".to_string(), + }); + let replay_region = BackwardMemoryRegion::Region(BackwardRegionRef { + id: MemoryRegionId(2), + kind: MemoryRegionKind::Replay, + name: "replay".to_string(), + }); + + let global = inferred_memory_term_evidence(&global_region, 0, 12, false); + let replay = inferred_memory_term_evidence(&replay_region, 4, 16, false); + + assert_eq!(global.tier, SemanticConfidence::Likely); + assert!( + global + .reasons + .contains(&SemanticEvidenceReason::DerivedFromRanking) + ); + assert_eq!(replay.tier, SemanticConfidence::Likely); + assert!( + replay + .reasons + .contains(&SemanticEvidenceReason::ReplayOverlap) + ); + } + + #[test] + fn inferred_memory_term_evidence_promotes_small_heap_windows_to_likely() { + let heap_region = BackwardMemoryRegion::Region(BackwardRegionRef { + id: MemoryRegionId(3), + kind: MemoryRegionKind::Heap, + name: "heap".to_string(), + }); + + let heap = inferred_memory_term_evidence(&heap_region, 8, 16, false); + let ambiguous = inferred_memory_term_evidence(&heap_region, 8, 32, false); + + assert_eq!(heap.tier, SemanticConfidence::Likely); + assert!( + heap.reasons + .contains(&SemanticEvidenceReason::HeapIdentityWeak) + ); + assert_eq!(ambiguous.tier, SemanticConfidence::Heuristic); + } +} diff --git a/crates/r2sym/src/constraints.rs b/crates/r2sym/src/constraints.rs new file mode 100644 index 0000000..4af7689 --- /dev/null +++ b/crates/r2sym/src/constraints.rs @@ -0,0 +1,1918 @@ +//! Canonical constraint graph derived from semantic evidence. +//! +//! This module deliberately records what is known and how it was derived. A +//! constraint graph can feed tactics, but verification remains responsible for +//! deciding whether a produced candidate is a proven solve. + +use std::collections::{BTreeMap, HashSet}; + +use r2ssa::SSAOp; +use r2ssa::{AssumptionSubject, AssumptionValue, CompareKind, SSAVar, SsaArtifact}; + +use crate::loops::{ + ExactLoopFoldEvidence, ExactLoopRecurrenceEvidence, LoopFoldOperation, LoopMemoryTermKind, + exact_fold_evidence_from_recurrences, +}; +use crate::path::PathResult; +use crate::solver::SymModel; +use crate::{SymState, SymValue}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FinalConstraintPrecision { + Exact, + ModelConditioned, + Residual, + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FinalConstraintSource { + TerminalCompareExact, + ExactRecurrenceAggregateModel, + MemoryWindowAssumption, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RecurrenceAggregateConstraint { + pub recurrence: ExactLoopRecurrenceEvidence, + pub target: u64, + pub bits: u32, + pub source: FinalConstraintSource, + pub precision: FinalConstraintPrecision, + pub reasons: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RecurrenceAggregateRangeConstraint { + pub recurrence: ExactLoopRecurrenceEvidence, + pub min: u64, + pub max: u64, + pub bits: u32, + pub source: FinalConstraintSource, + pub precision: FinalConstraintPrecision, + pub reasons: Vec, +} + +pub type FoldAggregateConstraint = RecurrenceAggregateConstraint; +pub type FoldAggregateRangeConstraint = RecurrenceAggregateRangeConstraint; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InputByteConstraint { + pub addr: u64, + pub allowed: Vec, + pub precision: FinalConstraintPrecision, + pub source: FinalConstraintSource, + pub reasons: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InputLengthConstraint { + pub base_addr: u64, + pub len: u32, + pub precision: FinalConstraintPrecision, + pub source: FinalConstraintSource, + pub reasons: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FinalConstraint { + RecurrenceEquals(RecurrenceAggregateConstraint), + RecurrenceRange(RecurrenceAggregateRangeConstraint), +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct FinalConstraintGraph { + pub constraints: Vec, + pub input_byte_constraints: Vec, + pub input_length_constraints: Vec, + pub refusals: Vec, +} + +impl FinalConstraintGraph { + pub fn is_empty(&self) -> bool { + self.constraints.is_empty() + } + + pub fn recurrence_aggregate_constraints( + &self, + ) -> impl Iterator { + self.constraints + .iter() + .filter_map(|constraint| match constraint { + FinalConstraint::RecurrenceEquals(constraint) => Some(constraint), + FinalConstraint::RecurrenceRange(_) => None, + }) + } + + pub fn recurrence_aggregate_range_constraints( + &self, + ) -> impl Iterator { + self.constraints + .iter() + .filter_map(|constraint| match constraint { + FinalConstraint::RecurrenceEquals(_) => None, + FinalConstraint::RecurrenceRange(constraint) => Some(constraint), + }) + } + + pub fn constraints_iter(&self) -> impl Iterator { + self.constraints.iter() + } + + pub fn exact_constraint_count(&self) -> usize { + self.constraints_iter() + .filter(|constraint| { + constraint_precision(constraint) == FinalConstraintPrecision::Exact + }) + .count() + } + + pub fn model_conditioned_constraint_count(&self) -> usize { + self.constraints_iter() + .filter(|constraint| { + constraint_precision(constraint) == FinalConstraintPrecision::ModelConditioned + }) + .count() + } + + pub fn strongest_precision(&self) -> FinalConstraintPrecision { + if self.exact_constraint_count() > 0 { + FinalConstraintPrecision::Exact + } else if self.model_conditioned_constraint_count() > 0 { + FinalConstraintPrecision::ModelConditioned + } else if self.constraints_iter().any(|constraint| { + constraint_precision(constraint) == FinalConstraintPrecision::Residual + }) { + FinalConstraintPrecision::Residual + } else { + FinalConstraintPrecision::Unknown + } + } + + pub fn has_exact_constraints(&self) -> bool { + self.exact_constraint_count() > 0 + } + + pub fn merge(&mut self, other: Self) { + self.constraints.extend(other.constraints); + self.input_byte_constraints + .extend(other.input_byte_constraints); + self.input_length_constraints + .extend(other.input_length_constraints); + for reason in other.refusals { + push_unique(&mut self.refusals, reason); + } + self.constraints + .sort_by(|lhs, rhs| constraint_sort_key(lhs).cmp(&constraint_sort_key(rhs))); + self.input_byte_constraints + .sort_by(|lhs, rhs| lhs.addr.cmp(&rhs.addr)); + self.input_byte_constraints.dedup_by(|lhs, rhs| { + lhs.addr == rhs.addr + && lhs.allowed == rhs.allowed + && lhs.precision == rhs.precision + && lhs.source == rhs.source + }); + self.input_length_constraints + .sort_by(|lhs, rhs| lhs.base_addr.cmp(&rhs.base_addr)); + self.input_length_constraints.dedup_by(|lhs, rhs| { + lhs.base_addr == rhs.base_addr + && lhs.len == rhs.len + && lhs.precision == rhs.precision + && lhs.source == rhs.source + }); + } +} + +fn constraint_precision(constraint: &FinalConstraint) -> FinalConstraintPrecision { + match constraint { + FinalConstraint::RecurrenceEquals(constraint) => constraint.precision, + FinalConstraint::RecurrenceRange(constraint) => constraint.precision, + } +} + +fn constraint_sort_key(constraint: &FinalConstraint) -> (u64, u64, &str, u64, u64, u8) { + match constraint { + FinalConstraint::RecurrenceEquals(constraint) => ( + constraint.recurrence.header, + constraint.recurrence.exit_target, + constraint.recurrence.accumulator.as_str(), + constraint.target, + constraint.target, + 0, + ), + FinalConstraint::RecurrenceRange(constraint) => ( + constraint.recurrence.header, + constraint.recurrence.exit_target, + constraint.recurrence.accumulator.as_str(), + constraint.min, + constraint.max, + 1, + ), + } +} + +pub fn exact_fold_model_bytes<'ctx>( + state: &SymState<'ctx>, + fold: &ExactLoopFoldEvidence, + model: &SymModel<'ctx>, +) -> Option> { + if fold.term.bytes != 1 { + return None; + } + let (Some(base), Some(stride)) = (fold.term.base, fold.term.stride) else { + return None; + }; + let mut bytes = Vec::with_capacity(fold.iterations as usize); + for iteration in 0..fold.iterations { + let offset = iteration.checked_mul(stride)?; + let addr = base.checked_add(offset)?; + let value = state.mem_read(&SymValue::concrete(addr, 64), 1); + bytes.push(model.eval(&value)? as u8); + } + Some(bytes) +} + +pub fn aggregate_exact_fold_bytes(fold: &ExactLoopFoldEvidence, bytes: &[u8]) -> Option { + if fold.term.bytes != 1 || bytes.len() != fold.iterations as usize { + return None; + } + let aggregate = match fold.operation { + LoopFoldOperation::Xor => bytes.iter().fold(0u64, |acc, byte| acc ^ (*byte as u64)), + LoopFoldOperation::Add => { + let sum = bytes + .iter() + .fold(0u128, |acc, byte| acc.saturating_add(*byte as u128)); + if fold.bits >= 64 { + sum as u64 + } else { + (sum % (1u128 << fold.bits.max(1))) as u64 + } + } + }; + Some(mask_to_bits(aggregate, fold.bits)) +} + +pub fn build_model_conditioned_recurrence_constraint_graph<'ctx>( + state: &SymState<'ctx>, + recurrences: &[ExactLoopRecurrenceEvidence], + model: &SymModel<'ctx>, +) -> FinalConstraintGraph { + let mut graph = FinalConstraintGraph::default(); + for recurrence in recurrences { + let Some(fold) = recurrence.as_fold() else { + push_unique( + &mut graph.refusals, + format!( + "non_fold_model_conditioned_recurrence:{}", + recurrence.accumulator + ), + ); + continue; + }; + if fold.term.kind != LoopMemoryTermKind::InputRead { + push_unique( + &mut graph.refusals, + format!("non_input_recurrence_constraint:{}", recurrence.accumulator), + ); + continue; + } + if fold.term.bytes != 1 { + push_unique( + &mut graph.refusals, + format!("unsupported_recurrence_read_width:{}", fold.term.bytes), + ); + continue; + } + let Some(bytes) = exact_fold_model_bytes(state, &fold, model) else { + push_unique( + &mut graph.refusals, + format!( + "recurrence_model_bytes_unavailable:{}", + recurrence.accumulator + ), + ); + continue; + }; + let Some(target) = aggregate_exact_fold_bytes(&fold, &bytes) else { + push_unique( + &mut graph.refusals, + format!( + "recurrence_aggregate_unavailable:{}", + recurrence.accumulator + ), + ); + continue; + }; + graph.constraints.push(FinalConstraint::RecurrenceEquals( + RecurrenceAggregateConstraint { + recurrence: recurrence.clone(), + target, + bits: recurrence.bits, + source: FinalConstraintSource::ExactRecurrenceAggregateModel, + precision: FinalConstraintPrecision::ModelConditioned, + reasons: vec![ + "target derived from selected-path exact recurrence model".to_string(), + ], + }, + )); + } + graph + .constraints + .sort_by(|lhs, rhs| constraint_sort_key(lhs).cmp(&constraint_sort_key(rhs))); + graph +} + +pub fn build_exact_fold_constraint_graph<'ctx>( + state: &SymState<'ctx>, + folds: &[ExactLoopFoldEvidence], + model: &SymModel<'ctx>, +) -> FinalConstraintGraph { + let recurrences = folds + .iter() + .cloned() + .map(ExactLoopRecurrenceEvidence::from) + .collect::>(); + build_model_conditioned_recurrence_constraint_graph(state, &recurrences, model) +} + +pub fn build_final_constraint_graph_for_path<'ctx>( + func: &SsaArtifact, + path: &PathResult<'ctx>, + recurrences: &[ExactLoopRecurrenceEvidence], + target_addr: u64, + model: Option<&SymModel<'ctx>>, +) -> FinalConstraintGraph { + let folds = exact_fold_evidence_from_recurrences(recurrences); + let mut graph = extract_terminal_recurrence_constraints(func, path, recurrences, target_addr); + graph.merge(extract_input_constraints_for_path(func, path, &folds)); + if graph.constraints.is_empty() + && let Some(model) = model + { + graph.merge(build_model_conditioned_recurrence_constraint_graph( + &path.state, + recurrences, + model, + )); + } + graph +} + +fn extract_terminal_recurrence_constraints<'ctx>( + func: &SsaArtifact, + path: &PathResult<'ctx>, + recurrences: &[ExactLoopRecurrenceEvidence], + target_addr: u64, +) -> FinalConstraintGraph { + let mut graph = FinalConstraintGraph::default(); + let def_index = build_ssa_def_index(func); + let Some(predecessor) = path.state.prev_pc() else { + push_unique(&mut graph.refusals, "final_constraint_missing_predecessor"); + return graph; + }; + + let predicates = func + .predicates() + .predicates + .values() + .filter(|predicate| { + predicate.block_addr == predecessor + && (predicate.true_target == target_addr || predicate.false_target == target_addr) + }) + .collect::>(); + + if predicates.is_empty() { + push_unique( + &mut graph.refusals, + format!("final_constraint_missing_terminal_predicate:0x{predecessor:x}"), + ); + return graph; + } + if predicates.len() > 1 { + push_unique( + &mut graph.refusals, + format!("final_constraint_ambiguous_terminal_predicate:0x{predecessor:x}"), + ); + return graph; + } + + let predicate = predicates[0]; + let Some(comparison) = &predicate.comparison else { + push_unique( + &mut graph.refusals, + format!("final_constraint_missing_compare:0x{predecessor:x}"), + ); + return graph; + }; + let branch_truth = predicate.true_target == target_addr; + let Some(lhs) = func.value_var(comparison.lhs) else { + push_unique(&mut graph.refusals, "final_constraint_missing_lhs"); + return graph; + }; + let Some(rhs) = func.value_var(comparison.rhs) else { + push_unique(&mut graph.refusals, "final_constraint_missing_rhs"); + return graph; + }; + + let lhs_const = parse_literal_var(lhs); + let rhs_const = parse_literal_var(rhs); + for recurrence in recurrences { + let linked = if let Some(expr) = + invertible_expr_for_recurrence_operand(func, &def_index, lhs, recurrence) + { + rhs_const.map(|target| TerminalCompareLink { + target, + const_name: rhs.display_name(), + recurrence_is_lhs: true, + expr, + }) + } else if let Some(expr) = + invertible_expr_for_recurrence_operand(func, &def_index, rhs, recurrence) + { + lhs_const.map(|target| TerminalCompareLink { + target, + const_name: lhs.display_name(), + recurrence_is_lhs: false, + expr, + }) + } else { + None + }; + let Some(link) = linked else { + continue; + }; + let Some(constraint) = terminal_constraint_for_compare( + predecessor, + comparison.kind, + branch_truth, + recurrence, + &link, + ) else { + continue; + }; + graph.constraints.push(constraint); + } + + if graph.constraints.is_empty() { + push_unique( + &mut graph.refusals, + format!("final_constraint_compare_not_linked_to_exact_recurrence:0x{predecessor:x}"), + ); + } + graph +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FoldTransform { + AddConst(u64), + XorConst(u64), + Scale(u64), + RotateLeft(u32), + RotateRight(u32), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct InvertibleFoldExpr { + bits: u32, + transforms: Vec, +} + +#[derive(Debug, Clone)] +struct TerminalCompareLink { + target: u64, + const_name: String, + recurrence_is_lhs: bool, + expr: InvertibleFoldExpr, +} + +struct InvertibleRecurrenceParseContext<'a> { + func: &'a SsaArtifact, + def_index: BTreeMap, + recurrence: &'a ExactLoopRecurrenceEvidence, + visited: HashSet, +} + +fn terminal_constraint_for_compare( + predecessor: u64, + kind: CompareKind, + branch_truth: bool, + recurrence: &ExactLoopRecurrenceEvidence, + link: &TerminalCompareLink, +) -> Option { + let exact_reason = || { + if invertible_expr_is_direct_identity(&link.expr, recurrence.bits, link.target) { + vec![format!( + "exact terminal compare links {} to {} at 0x{predecessor:x}", + recurrence.accumulator, link.const_name + )] + } else { + vec![format!( + "exact terminal compare links {} to {} at 0x{predecessor:x} via {}", + recurrence.accumulator, + link.const_name, + describe_invertible_expr(&link.expr) + )] + } + }; + match (kind, branch_truth) { + (CompareKind::Equal, true) | (CompareKind::NotEqual, false) => Some( + FinalConstraint::RecurrenceEquals(RecurrenceAggregateConstraint { + recurrence: recurrence.clone(), + target: invert_invertible_recurrence_equality( + &link.expr, + link.target, + recurrence.bits, + )?, + bits: recurrence.bits, + source: FinalConstraintSource::TerminalCompareExact, + precision: FinalConstraintPrecision::Exact, + reasons: exact_reason(), + }), + ), + (CompareKind::Less, truth) + if invertible_expr_is_direct_identity(&link.expr, recurrence.bits, link.target) => + { + recurrence_range_constraint( + recurrence, + if link.recurrence_is_lhs { + if truth { + FoldRangeRelation::Le(mask_to_bits( + link.target.wrapping_sub(1), + recurrence.bits, + )) + } else { + FoldRangeRelation::Ge(mask_to_bits(link.target, recurrence.bits)) + } + } else if truth { + FoldRangeRelation::Ge(mask_to_bits( + link.target.saturating_add(1), + recurrence.bits, + )) + } else { + FoldRangeRelation::Le(mask_to_bits(link.target, recurrence.bits)) + }, + exact_reason(), + ) + } + (CompareKind::LessEqual, truth) + if invertible_expr_is_direct_identity(&link.expr, recurrence.bits, link.target) => + { + recurrence_range_constraint( + recurrence, + if link.recurrence_is_lhs { + if truth { + FoldRangeRelation::Le(mask_to_bits(link.target, recurrence.bits)) + } else { + FoldRangeRelation::Ge(mask_to_bits( + link.target.saturating_add(1), + recurrence.bits, + )) + } + } else if truth { + FoldRangeRelation::Ge(mask_to_bits(link.target, recurrence.bits)) + } else { + FoldRangeRelation::Le(mask_to_bits( + link.target.wrapping_sub(1), + recurrence.bits, + )) + }, + exact_reason(), + ) + } + _ => None, + } +} + +fn invertible_expr_for_recurrence_operand( + func: &SsaArtifact, + def_index: &BTreeMap, + var: &SSAVar, + recurrence: &ExactLoopRecurrenceEvidence, +) -> Option { + let bits = var.size.saturating_mul(8).max(1); + let mut ctx = InvertibleRecurrenceParseContext { + func, + def_index: def_index.clone(), + recurrence, + visited: HashSet::new(), + }; + parse_invertible_expr_for_recurrence_operand(&mut ctx, var, bits, 8) +} + +fn parse_invertible_expr_for_recurrence_operand( + ctx: &mut InvertibleRecurrenceParseContext<'_>, + var: &SSAVar, + _bits: u32, + depth: u8, +) -> Option { + if depth == 0 { + return None; + } + if var.display_name() == ctx.recurrence.accumulator { + return Some(InvertibleFoldExpr { + bits: ctx.recurrence.bits.max(1), + transforms: Vec::new(), + }); + } + let name = var.display_name(); + let op = *ctx.def_index.get(&name)?; + if !ctx.visited.insert(name.clone()) { + return None; + } + let result = match op { + SSAOp::Copy { src, dst } | SSAOp::IntZExt { src, dst } => { + parse_invertible_expr_for_recurrence_operand( + ctx, + src, + dst.size.saturating_mul(8).max(1), + depth - 1, + ) + } + SSAOp::IntAdd { dst, a, b } => { + parse_invertible_add_like_expr(ctx, a, b, dst.size, depth - 1) + } + SSAOp::IntSub { dst, a, b } => { + parse_invertible_sub_like_expr(ctx, a, b, dst.size, depth - 1) + } + SSAOp::IntMult { dst, a, b } => { + let bits = dst.size.saturating_mul(8).max(1); + if let Some(scale) = parse_literal_var(a) { + let inner = parse_invertible_expr_for_recurrence_operand(ctx, b, bits, depth - 1)?; + Some(push_fold_transform( + inner, + FoldTransform::Scale(scale), + bits, + )) + } else if let Some(scale) = parse_literal_var(b) { + let inner = parse_invertible_expr_for_recurrence_operand(ctx, a, bits, depth - 1)?; + Some(push_fold_transform( + inner, + FoldTransform::Scale(scale), + bits, + )) + } else { + None + } + } + SSAOp::IntXor { dst, a, b } => { + parse_invertible_xor_like_expr(ctx, a, b, dst.size, depth - 1) + } + SSAOp::IntOr { dst, a, b } => parse_rotate_or_expr(ctx, a, b, dst.size, depth - 1), + _ => None, + } + .or_else(|| { + ctx.func + .function() + .decompile_prep_facts() + .and_then(|facts| facts.canonical_root_of(var)) + .filter(|root| root.display_name() == ctx.recurrence.accumulator) + .map(|_| InvertibleFoldExpr { + bits: ctx.recurrence.bits.max(1), + transforms: Vec::new(), + }) + }); + ctx.visited.remove(&name); + result +} + +fn parse_invertible_add_like_expr( + ctx: &mut InvertibleRecurrenceParseContext<'_>, + a: &SSAVar, + b: &SSAVar, + dst_size: u32, + depth: u8, +) -> Option { + if let Some(constant) = parse_literal_var(a) { + let inner = parse_invertible_expr_for_recurrence_operand( + ctx, + b, + dst_size.saturating_mul(8).max(1), + depth, + )?; + let bits = inner.bits; + return Some(push_fold_transform( + inner, + FoldTransform::AddConst(constant), + bits, + )); + } + if let Some(constant) = parse_literal_var(b) { + let inner = parse_invertible_expr_for_recurrence_operand( + ctx, + a, + dst_size.saturating_mul(8).max(1), + depth, + )?; + let bits = inner.bits; + return Some(push_fold_transform( + inner, + FoldTransform::AddConst(constant), + bits, + )); + } + None +} + +fn parse_invertible_sub_like_expr( + ctx: &mut InvertibleRecurrenceParseContext<'_>, + a: &SSAVar, + b: &SSAVar, + dst_size: u32, + depth: u8, +) -> Option { + if let Some(constant) = parse_literal_var(b) { + let inner = parse_invertible_expr_for_recurrence_operand( + ctx, + a, + dst_size.saturating_mul(8).max(1), + depth, + )?; + let bits = inner.bits; + return Some(push_fold_transform( + inner, + FoldTransform::AddConst(mask_to_bits(constant.wrapping_neg(), bits)), + bits, + )); + } + if let Some(constant) = parse_literal_var(a) { + let inner = parse_invertible_expr_for_recurrence_operand( + ctx, + b, + dst_size.saturating_mul(8).max(1), + depth, + )?; + let bits = inner.bits; + let inner = push_fold_transform( + inner, + FoldTransform::Scale(mask_to_bits(u64::MAX, bits)), + bits, + ); + return Some(push_fold_transform( + inner, + FoldTransform::AddConst(constant), + bits, + )); + } + None +} + +fn parse_invertible_xor_like_expr( + ctx: &mut InvertibleRecurrenceParseContext<'_>, + a: &SSAVar, + b: &SSAVar, + dst_size: u32, + depth: u8, +) -> Option { + if let Some(constant) = parse_literal_var(a) { + let inner = parse_invertible_expr_for_recurrence_operand( + ctx, + b, + dst_size.saturating_mul(8).max(1), + depth, + )?; + let bits = inner.bits; + return Some(push_fold_transform( + inner, + FoldTransform::XorConst(constant), + bits, + )); + } + if let Some(constant) = parse_literal_var(b) { + let inner = parse_invertible_expr_for_recurrence_operand( + ctx, + a, + dst_size.saturating_mul(8).max(1), + depth, + )?; + let bits = inner.bits; + return Some(push_fold_transform( + inner, + FoldTransform::XorConst(constant), + bits, + )); + } + None +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RotateDirection { + Left, + Right, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct RotateShiftTerm { + inner: InvertibleFoldExpr, + direction: RotateDirection, + shift: u32, + bits: u32, +} + +fn parse_rotate_or_expr( + ctx: &mut InvertibleRecurrenceParseContext<'_>, + a: &SSAVar, + b: &SSAVar, + dst_size: u32, + depth: u8, +) -> Option { + let lhs = parse_rotate_shift_term(ctx, a, dst_size.saturating_mul(8).max(1), depth)?; + let rhs = parse_rotate_shift_term(ctx, b, dst_size.saturating_mul(8).max(1), depth)?; + if lhs.inner != rhs.inner || lhs.bits != rhs.bits { + return None; + } + let bits = lhs.inner.bits; + if lhs.shift == 0 || rhs.shift == 0 || lhs.shift.saturating_add(rhs.shift) != bits { + return None; + } + if lhs.direction == RotateDirection::Left && rhs.direction == RotateDirection::Right { + return Some(push_fold_transform( + lhs.inner, + FoldTransform::RotateLeft(lhs.shift), + bits, + )); + } + if lhs.direction == RotateDirection::Right && rhs.direction == RotateDirection::Left { + return Some(push_fold_transform( + lhs.inner, + FoldTransform::RotateRight(lhs.shift), + bits, + )); + } + None +} + +fn parse_rotate_shift_term( + ctx: &mut InvertibleRecurrenceParseContext<'_>, + var: &SSAVar, + bits: u32, + depth: u8, +) -> Option { + if depth == 0 { + return None; + } + let op = *ctx.def_index.get(&var.display_name())?; + match op { + SSAOp::IntLeft { dst, a, b } => Some(RotateShiftTerm { + inner: parse_invertible_expr_for_recurrence_operand( + ctx, + a, + dst.size.saturating_mul(8).max(1), + depth - 1, + )?, + direction: RotateDirection::Left, + shift: normalize_rotate_amount(parse_literal_var(b)? as u32, bits), + bits: dst.size.saturating_mul(8).max(1), + }), + SSAOp::IntRight { dst, a, b } => Some(RotateShiftTerm { + inner: parse_invertible_expr_for_recurrence_operand( + ctx, + a, + dst.size.saturating_mul(8).max(1), + depth - 1, + )?, + direction: RotateDirection::Right, + shift: normalize_rotate_amount(parse_literal_var(b)? as u32, bits), + bits: dst.size.saturating_mul(8).max(1), + }), + _ => None, + } +} + +fn build_ssa_def_index(func: &SsaArtifact) -> BTreeMap { + let mut index = BTreeMap::new(); + for block in func.function().blocks() { + for op in &block.ops { + if let Some(dst) = op.dst() { + index.insert(dst.display_name(), op); + } + } + } + index +} + +fn push_fold_transform( + mut expr: InvertibleFoldExpr, + transform: FoldTransform, + bits: u32, +) -> InvertibleFoldExpr { + let normalized = match transform { + FoldTransform::AddConst(value) => FoldTransform::AddConst(mask_to_bits(value, bits)), + FoldTransform::XorConst(value) => FoldTransform::XorConst(mask_to_bits(value, bits)), + FoldTransform::Scale(value) => FoldTransform::Scale(mask_to_bits(value, bits)), + FoldTransform::RotateLeft(value) => { + FoldTransform::RotateLeft(normalize_rotate_amount(value, bits)) + } + FoldTransform::RotateRight(value) => { + FoldTransform::RotateRight(normalize_rotate_amount(value, bits)) + } + }; + expr.bits = bits; + expr.transforms.push(normalized); + expr +} + +fn normalize_rotate_amount(amount: u32, bits: u32) -> u32 { + if bits == 0 { 0 } else { amount % bits.min(64) } +} + +fn invertible_expr_is_direct_identity( + expr: &InvertibleFoldExpr, + recurrence_bits: u32, + target: u64, +) -> bool { + expr.transforms.is_empty() + && target <= max_value_for_bits(recurrence_bits) + && expr.bits >= recurrence_bits +} + +fn invert_invertible_recurrence_equality( + expr: &InvertibleFoldExpr, + target: u64, + recurrence_bits: u32, +) -> Option { + let mut solved = mask_to_bits(target, expr.bits); + for transform in expr.transforms.iter().rev() { + solved = match transform { + FoldTransform::AddConst(value) => mask_to_bits(solved.wrapping_sub(*value), expr.bits), + FoldTransform::XorConst(value) => mask_to_bits(solved ^ *value, expr.bits), + FoldTransform::Scale(value) => { + let inverse = modular_inverse_pow2(*value, expr.bits)?; + mask_to_bits(solved.wrapping_mul(inverse), expr.bits) + } + FoldTransform::RotateLeft(amount) => rotate_right_bits(solved, *amount, expr.bits), + FoldTransform::RotateRight(amount) => rotate_left_bits(solved, *amount, expr.bits), + }; + } + (solved <= max_value_for_bits(recurrence_bits)).then_some(mask_to_bits(solved, recurrence_bits)) +} + +fn describe_invertible_expr(expr: &InvertibleFoldExpr) -> String { + if expr.transforms.is_empty() { + return format!("identity(bits={})", expr.bits); + } + let parts = expr + .transforms + .iter() + .map(|transform| match transform { + FoldTransform::AddConst(value) => format!("add_const(0x{value:x})"), + FoldTransform::XorConst(value) => format!("xor_const(0x{value:x})"), + FoldTransform::Scale(value) => format!("scale(0x{value:x})"), + FoldTransform::RotateLeft(amount) => format!("rol({amount})"), + FoldTransform::RotateRight(amount) => format!("ror({amount})"), + }) + .collect::>(); + format!("{}; bits={}", parts.join(" -> "), expr.bits) +} + +fn rotate_left_bits(value: u64, amount: u32, bits: u32) -> u64 { + rotate_bits(value, amount, bits, true) +} + +fn rotate_right_bits(value: u64, amount: u32, bits: u32) -> u64 { + rotate_bits(value, amount, bits, false) +} + +fn rotate_bits(value: u64, amount: u32, bits: u32, left: bool) -> u64 { + let bits = bits.min(64); + if bits == 0 { + return 0; + } + let value = mask_to_bits(value, bits); + let amount = normalize_rotate_amount(amount, bits); + if amount == 0 { + return value; + } + if bits == 64 { + if left { + value.rotate_left(amount) + } else { + value.rotate_right(amount) + } + } else { + let lhs = if left { + value.wrapping_shl(amount) + } else { + value >> amount + }; + let rhs = if left { + value >> (bits - amount) + } else { + value.wrapping_shl(bits - amount) + }; + mask_to_bits(lhs | rhs, bits) + } +} + +fn modular_inverse_pow2(value: u64, bits: u32) -> Option { + if bits == 0 { + return None; + } + if value & 1 == 0 { + return None; + } + let modulus = 1i128.checked_shl(bits.min(64))?; + let value = (mask_to_bits(value, bits)) as i128; + let (gcd, x, _) = extended_gcd_i128(value, modulus); + if gcd != 1 { + return None; + } + let inverse = ((x % modulus) + modulus) % modulus; + Some(mask_to_bits(inverse as u64, bits)) +} + +fn extended_gcd_i128(a: i128, b: i128) -> (i128, i128, i128) { + if b == 0 { + (a, 1, 0) + } else { + let (gcd, x, y) = extended_gcd_i128(b, a.rem_euclid(b)); + (gcd, y, x - (a / b) * y) + } +} + +#[derive(Clone, Copy)] +enum FoldRangeRelation { + Ge(u64), + Le(u64), +} + +fn recurrence_range_constraint( + recurrence: &ExactLoopRecurrenceEvidence, + relation: FoldRangeRelation, + reasons: Vec, +) -> Option { + let max_value = max_value_for_bits(recurrence.bits); + let (min, max) = match relation { + FoldRangeRelation::Ge(min) if min <= max_value => (min, max_value), + FoldRangeRelation::Le(max) => (0, max.min(max_value)), + _ => return None, + }; + if min > max { + return None; + } + Some(FinalConstraint::RecurrenceRange( + RecurrenceAggregateRangeConstraint { + recurrence: recurrence.clone(), + min, + max, + bits: recurrence.bits, + source: FinalConstraintSource::TerminalCompareExact, + precision: FinalConstraintPrecision::Exact, + reasons, + }, + )) +} + +fn extract_input_constraints_for_path<'ctx>( + func: &SsaArtifact, + path: &PathResult<'ctx>, + folds: &[ExactLoopFoldEvidence], +) -> FinalConstraintGraph { + let mut graph = FinalConstraintGraph::default(); + let interesting_ranges = folds + .iter() + .filter_map(|fold| { + let (Some(base), Some(stride)) = (fold.term.base, fold.term.stride) else { + return None; + }; + Some(( + base, + fold.iterations, + stride, + fold.term.region.as_deref(), + fold.term.region_base, + )) + }) + .collect::>(); + + for assumption in func.facts().assumptions.iter() { + let AssumptionSubject::MemoryWindow { addr, size } = assumption.subject else { + continue; + }; + if !interesting_ranges + .iter() + .any(|(base, iterations, stride, region, region_base)| { + memory_window_overlaps_fold( + addr, + size, + *base, + *iterations, + *stride, + *region, + *region_base, + ) + }) + { + continue; + } + let Some(region) = path + .state + .symbolic_memory() + .iter() + .find(|region| memory_window_within_region(addr, size, region.addr, region.size)) + else { + push_unique( + &mut graph.refusals, + format!("input_assumption_missing_region:0x{addr:x}/{size}"), + ); + continue; + }; + graph.merge(memory_window_constraints_from_assumption( + addr, + size, + region.addr, + region.size, + &assumption.value, + )); + } + + for region in path.state.symbolic_memory() { + graph.input_length_constraints.push(InputLengthConstraint { + base_addr: region.addr, + len: region.size, + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: vec![format!( + "symbolic input region length {} @ 0x{:x}", + region.size, region.addr + )], + }); + } + + graph +} + +fn memory_window_constraints_from_assumption( + addr: u64, + size: u32, + region_addr: u64, + region_size: u32, + value: &AssumptionValue, +) -> FinalConstraintGraph { + let mut graph = FinalConstraintGraph::default(); + match value { + AssumptionValue::Constant { value } if size <= 8 => { + for index in 0..size { + let byte = ((value >> (index * 8)) & 0xff) as u8; + graph.input_byte_constraints.push(InputByteConstraint { + addr: addr + index as u64, + allowed: vec![byte], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: vec![format!( + "memory-window constant assumption at 0x{:x}", + addr + index as u64 + )], + }); + } + } + AssumptionValue::Range { min, max } if size == 1 && *min <= 0xff && *max <= 0xff => { + graph.input_byte_constraints.push(InputByteConstraint { + addr, + allowed: (*min as u8..=*max as u8).collect(), + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: vec![format!("memory-window range assumption at 0x{addr:x}")], + }); + } + AssumptionValue::FiniteSet { values } if size == 1 => { + let allowed = values + .iter() + .copied() + .filter(|value| *value <= 0xff) + .map(|value| value as u8) + .collect::>(); + if !allowed.is_empty() { + graph.input_byte_constraints.push(InputByteConstraint { + addr, + allowed, + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: vec![format!("memory-window finite-set assumption at 0x{addr:x}")], + }); + } + } + AssumptionValue::EnumDomain { values, .. } if size == 1 => { + let allowed = values + .iter() + .copied() + .filter(|value| *value >= 0 && *value <= 0xff) + .map(|value| value as u8) + .collect::>(); + if !allowed.is_empty() { + graph.input_byte_constraints.push(InputByteConstraint { + addr, + allowed, + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: vec![format!("memory-window enum assumption at 0x{addr:x}")], + }); + } + } + _ => { + push_unique( + &mut graph.refusals, + format!("input_assumption_unsupported:0x{addr:x}/{size}"), + ); + } + } + if memory_window_within_region(addr, size, region_addr, region_size) { + graph.input_length_constraints.push(InputLengthConstraint { + base_addr: region_addr, + len: region_size, + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: vec![format!( + "memory-window assumption applies within region @ 0x{region_addr:x}" + )], + }); + } + graph +} + +fn memory_window_overlaps_fold( + addr: u64, + size: u32, + base: u64, + iterations: u64, + stride: u64, + region: Option<&str>, + region_base: Option, +) -> bool { + if stride == 0 { + return false; + } + let Some(end) = addr.checked_add(size as u64) else { + return false; + }; + let Some(fold_end) = base.checked_add(iterations.saturating_mul(stride)) else { + return false; + }; + if addr < fold_end && end > base { + return true; + } + region.is_some() && region_base == Some(base) +} + +fn memory_window_within_region(addr: u64, size: u32, region_addr: u64, region_size: u32) -> bool { + let Some(region_end) = region_addr.checked_add(region_size as u64) else { + return false; + }; + let Some(end) = addr.checked_add(size as u64) else { + return false; + }; + addr >= region_addr && end <= region_end +} + +fn parse_literal_var(var: &SSAVar) -> Option { + parse_literal_value_name(&var.name) +} + +fn parse_literal_value_name(name: &str) -> Option { + let value_str = if let Some(value) = name.strip_prefix("const:") { + value + } else if let Some(value) = name.strip_prefix("ram:") { + value + } else { + return None; + }; + let value_str = value_str.split('_').next().unwrap_or(value_str); + if let Some(dec) = value_str + .strip_prefix("0d") + .or_else(|| value_str.strip_prefix("0D")) + { + return dec.parse().ok(); + } + if let Some(hex) = value_str + .strip_prefix("0x") + .or_else(|| value_str.strip_prefix("0X")) + { + return u64::from_str_radix(hex, 16).ok(); + } + u64::from_str_radix(value_str, 16) + .ok() + .or_else(|| value_str.parse().ok()) +} + +fn mask_to_bits(value: u64, bits: u32) -> u64 { + if bits >= 64 { + value + } else if bits == 0 { + 0 + } else { + value & ((1u64 << bits) - 1) + } +} + +fn max_value_for_bits(bits: u32) -> u64 { + if bits >= 64 { + u64::MAX + } else if bits == 0 { + 0 + } else { + (1u64 << bits) - 1 + } +} + +fn push_unique(reasons: &mut Vec, reason: impl Into) { + let reason = reason.into(); + if !reasons.iter().any(|existing| existing == &reason) { + reasons.push(reason); + } +} + +#[cfg(test)] +mod tests { + use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; + use r2ssa::{ + AnalysisAssumption, AssumptionScope, AssumptionSet, AssumptionSubject, AssumptionValue, + SsaArtifact, + }; + use z3::Context; + + use super::{ + FinalConstraint, FinalConstraintSource, InputByteConstraint, aggregate_exact_fold_bytes, + build_exact_fold_constraint_graph, build_final_constraint_graph_for_path, + }; + use crate::{ + ExactLoopFoldEvidence, ExactLoopRecurrenceEvidence, ExactLoopRecurrenceKind, + LoopFoldOperation, LoopMemoryTerm, LoopMemoryTermKind, LoopRotateDirection, PathResult, + SymSolver, SymState, SymValue, + }; + + const RAX: u64 = 0; + const RCX: u64 = 16; + const RDX: u64 = 24; + const RSI: u64 = 32; + + fn make_reg(offset: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Register, + offset, + size, + meta: None, + } + } + + fn make_const(val: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: val, + size, + meta: None, + } + } + + fn make_x86_64_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("RAX", RAX, 8)); + arch.add_register(RegisterDef::new("RCX", RCX, 8)); + arch.add_register(RegisterDef::new("RDX", RDX, 8)); + arch.add_register(RegisterDef::new("RSI", RSI, 8)); + arch + } + + fn input_fold( + operation: LoopFoldOperation, + bits: u32, + iterations: u64, + ) -> ExactLoopFoldEvidence { + ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations, + accumulator: "ACC_2".to_string(), + bits, + operation, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "PTR_1".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(iterations), + }, + } + } + + #[test] + fn aggregate_exact_fold_bytes_masks_to_width() { + let fold = input_fold(LoopFoldOperation::Add, 8, 3); + assert_eq!(aggregate_exact_fold_bytes(&fold, &[200, 100, 10]), Some(54)); + + let fold = input_fold(LoopFoldOperation::Xor, 8, 3); + assert_eq!( + aggregate_exact_fold_bytes(&fold, &[0xaa, 0x55, 0xff]), + Some(0) + ); + } + + #[test] + fn constraint_graph_derives_input_fold_target_from_selected_model() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic_memory(0x7000, 3, "argv1"); + let fold = input_fold(LoopFoldOperation::Add, 16, 3); + for (offset, byte) in [10u64, 20, 30].into_iter().enumerate() { + let value = state.mem_read(&crate::SymValue::concrete(0x7000 + offset as u64, 64), 1); + state.constrain_eq(&value, byte); + } + let solver = SymSolver::new(&ctx); + let model = solver.solve(&state).expect("model"); + let graph = build_exact_fold_constraint_graph(&state, &[fold], &model); + assert!(graph.refusals.is_empty()); + assert_eq!(graph.constraints.len(), 1); + match &graph.constraints[0] { + FinalConstraint::RecurrenceEquals(constraint) => { + assert_eq!(constraint.target, 60); + assert_eq!(constraint.bits, 16); + } + FinalConstraint::RecurrenceRange(_) => panic!("unexpected range constraint"), + } + } + + #[test] + fn final_constraint_graph_extracts_exact_terminal_compare_for_fold_accumulator() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RAX, 8), + b: make_const(0x55, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("function"); + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x1010, + iterations: 1, + accumulator: "RAX_0".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_0".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(1), + }, + }; + + let mut state = SymState::new(&ctx, 0x1010); + state.set_prev_pc(Some(0x1000)); + state.set_register("RAX_0", SymValue::concrete(0x55, 64)); + let path = PathResult::new(state, true); + let recurrences = vec![ExactLoopRecurrenceEvidence::from(fold)]; + let graph = build_final_constraint_graph_for_path(&func, &path, &recurrences, 0x1010, None); + assert!(graph.refusals.is_empty(), "{:?}", graph.refusals); + assert_eq!(graph.exact_constraint_count(), 1); + match &graph.constraints[0] { + FinalConstraint::RecurrenceEquals(constraint) => { + assert_eq!(constraint.target, 0x55); + assert_eq!( + constraint.source, + FinalConstraintSource::TerminalCompareExact + ); + assert_eq!(constraint.precision, super::FinalConstraintPrecision::Exact); + } + FinalConstraint::RecurrenceRange(_) => panic!("unexpected range constraint"), + } + } + + #[test] + fn final_constraint_graph_extracts_terminal_range_compare_for_fold_accumulator() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntLess { + dst: make_reg(RCX, 1), + a: make_reg(RAX, 8), + b: make_const(0x40, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("function"); + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x1010, + iterations: 1, + accumulator: "RAX_0".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_0".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(1), + }, + }; + + let mut state = SymState::new(&ctx, 0x1010); + state.set_prev_pc(Some(0x1000)); + let path = PathResult::new(state, true); + let recurrences = vec![ExactLoopRecurrenceEvidence::from(fold)]; + let graph = build_final_constraint_graph_for_path(&func, &path, &recurrences, 0x1010, None); + assert!(graph.refusals.is_empty(), "{:?}", graph.refusals); + assert_eq!(graph.exact_constraint_count(), 1); + match &graph.constraints[0] { + FinalConstraint::RecurrenceRange(constraint) => { + assert_eq!(constraint.min, 0); + assert_eq!(constraint.max, 0x3f); + } + FinalConstraint::RecurrenceEquals(_) => panic!("unexpected equality constraint"), + } + } + + #[test] + fn final_constraint_graph_inverts_affine_terminal_compare_for_fold_accumulator() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntMult { + dst: make_reg(RCX, 8), + a: make_reg(RAX, 8), + b: make_const(3, 8), + }, + R2ILOp::IntAdd { + dst: make_reg(RCX, 8), + a: make_reg(RCX, 8), + b: make_const(1, 8), + }, + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RCX, 8), + b: make_const(0x10, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("function"); + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x1010, + iterations: 1, + accumulator: "RAX_0".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_0".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(1), + }, + }; + + let mut state = SymState::new(&ctx, 0x1010); + state.set_prev_pc(Some(0x1000)); + let path = PathResult::new(state, true); + let recurrences = vec![ExactLoopRecurrenceEvidence::from(fold)]; + let graph = build_final_constraint_graph_for_path(&func, &path, &recurrences, 0x1010, None); + assert!(graph.refusals.is_empty(), "{:?}", graph.refusals); + assert_eq!(graph.exact_constraint_count(), 1); + match &graph.constraints[0] { + FinalConstraint::RecurrenceEquals(constraint) => { + assert_eq!(constraint.target, 5); + assert_eq!( + constraint.source, + FinalConstraintSource::TerminalCompareExact + ); + assert_eq!(constraint.precision, super::FinalConstraintPrecision::Exact); + } + FinalConstraint::RecurrenceRange(_) => panic!("unexpected range constraint"), + } + } + + #[test] + fn final_constraint_graph_inverts_rotate_xor_terminal_compare_for_fold_accumulator() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 6, + ops: vec![ + R2ILOp::IntLeft { + dst: make_reg(RDX, 8), + a: make_reg(RAX, 8), + b: make_const(3, 8), + }, + R2ILOp::IntRight { + dst: make_reg(RSI, 8), + a: make_reg(RAX, 8), + b: make_const(5, 8), + }, + R2ILOp::IntOr { + dst: make_reg(RCX, 8), + a: make_reg(RDX, 8), + b: make_reg(RSI, 8), + }, + R2ILOp::IntXor { + dst: make_reg(RCX, 8), + a: make_reg(RCX, 8), + b: make_const(0x55, 8), + }, + R2ILOp::IntEqual { + dst: make_reg(RDX, 1), + a: make_reg(RCX, 8), + b: make_const(0x78, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RDX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("function"); + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x1010, + iterations: 1, + accumulator: "RAX_0".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_0".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(1), + }, + }; + + let mut state = SymState::new(&ctx, 0x1010); + state.set_prev_pc(Some(0x1000)); + let path = PathResult::new(state, true); + let recurrences = vec![ExactLoopRecurrenceEvidence::from(fold)]; + let graph = build_final_constraint_graph_for_path(&func, &path, &recurrences, 0x1010, None); + assert!(graph.refusals.is_empty(), "{:?}", graph.refusals); + assert_eq!(graph.exact_constraint_count(), 1); + match &graph.constraints[0] { + FinalConstraint::RecurrenceEquals(constraint) => { + assert_eq!(constraint.target, 0xa5); + assert_eq!(constraint.precision, super::FinalConstraintPrecision::Exact); + } + FinalConstraint::RecurrenceRange(_) => panic!("unexpected range constraint"), + } + } + + #[test] + fn final_constraint_graph_links_direct_terminal_compare_to_exact_rotate_recurrence() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 2, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RAX, 8), + b: make_const(0x42, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("function"); + let recurrence = ExactLoopRecurrenceEvidence { + header: 0x1000, + exit_target: 0x1010, + iterations: 4, + accumulator: "RAX_0".to_string(), + initial: "RAX_1".to_string(), + bits: 8, + kind: ExactLoopRecurrenceKind::RotateMix { + direction: LoopRotateDirection::Left, + amount: 3, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_0".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(4), + }, + }, + }; + + let mut state = SymState::new(&ctx, 0x1010); + state.set_prev_pc(Some(0x1000)); + let path = PathResult::new(state, true); + let graph = build_final_constraint_graph_for_path( + &func, + &path, + std::slice::from_ref(&recurrence), + 0x1010, + None, + ); + assert!(graph.refusals.is_empty(), "{:?}", graph.refusals); + assert_eq!(graph.exact_constraint_count(), 1); + match &graph.constraints[0] { + FinalConstraint::RecurrenceEquals(constraint) => { + assert_eq!(constraint.target, 0x42); + assert_eq!(constraint.recurrence, recurrence); + assert_eq!(constraint.precision, super::FinalConstraintPrecision::Exact); + } + FinalConstraint::RecurrenceRange(_) => panic!("unexpected range constraint"), + } + } + + #[test] + fn final_constraint_graph_extracts_memory_window_byte_constraints() { + let ctx = Context::thread_local(); + let blocks = vec![R2ILBlock { + addr: 0x1000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let func = SsaArtifact::for_symbolic(&blocks, None) + .expect("function") + .with_assumptions(&AssumptionSet::new(vec![AnalysisAssumption { + id: Some("argv-byte".to_string()), + subject: AssumptionSubject::MemoryWindow { + addr: 0x7001, + size: 1, + }, + value: AssumptionValue::FiniteSet { + values: vec![b'A' as u64, b'B' as u64], + }, + scope: AssumptionScope::Query, + provenance: Default::default(), + }])); + let fold = input_fold(LoopFoldOperation::Xor, 8, 3); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic_memory(0x7000, 3, "argv1"); + let path = PathResult::new(state, true); + let recurrences = vec![ExactLoopRecurrenceEvidence::from(fold)]; + let graph = build_final_constraint_graph_for_path(&func, &path, &recurrences, 0x1000, None); + assert!(graph.constraints.is_empty()); + assert!( + graph + .input_length_constraints + .iter() + .any(|constraint| constraint.base_addr == 0x7000 && constraint.len == 3) + ); + assert!( + graph + .input_byte_constraints + .iter() + .any(|constraint: &InputByteConstraint| { + constraint.addr == 0x7001 && constraint.allowed == vec![b'A', b'B'] + }) + ); + } +} diff --git a/crates/r2sym/src/executor.rs b/crates/r2sym/src/executor.rs index dca1503..f134cde 100644 --- a/crates/r2sym/src/executor.rs +++ b/crates/r2sym/src/executor.rs @@ -3,16 +3,19 @@ //! This module implements the core symbolic execution logic, //! stepping through SSA operations and updating state. -use std::collections::HashMap; +use std::borrow::Cow; +use std::collections::{HashMap, HashSet}; use r2ssa::{FunctionSSABlock, SSAOp, SSAVar}; use z3::Context; use z3::ast::BV; use crate::SymResult; -use crate::state::{ExitStatus, SymState}; +use crate::state::{ExitStatus, RuntimeValueProvenance, SymState}; use crate::value::SymValue; +const X86_USEROP_RDTSC: u32 = 74; + /// Result of a call hook. #[derive(Debug, Clone, PartialEq, Eq)] pub enum CallHookResult { @@ -24,15 +27,72 @@ pub enum CallHookResult { Terminate(ExitStatus), } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CallHookTag { + WindowsAddVectoredExceptionHandler, + WindowsRaiseException, + WindowsVirtualAlloc, + WindowsVirtualProtect, + WindowsHeapAlloc, +} + /// A call hook for intercepting direct calls. pub type CallHook<'ctx> = Box) -> SymResult + 'ctx>; +fn block_execution_should_stop(op: &SSAOp, pc_before_op: u64, pc_after_op: u64) -> bool { + match op { + SSAOp::Branch { .. } | SSAOp::BranchInd { .. } | SSAOp::Return { .. } => true, + SSAOp::CBranch { .. } | SSAOp::Call { .. } | SSAOp::CallInd { .. } => { + pc_after_op != pc_before_op + } + _ => false, + } +} + +fn zero_equality_from_known_facts<'ctx>( + state: &SymState<'ctx>, + lhs: &SymValue<'ctx>, + rhs: &SymValue<'ctx>, +) -> Option { + if rhs.as_concrete() == Some(0) { + if state.value_known_zero(lhs) { + return Some(true); + } + if state.value_known_nonzero(lhs) { + return Some(false); + } + } + if lhs.as_concrete() == Some(0) { + if state.value_known_zero(rhs) { + return Some(true); + } + if state.value_known_nonzero(rhs) { + return Some(false); + } + } + None +} + +struct RegisteredCallHook<'ctx> { + tag: Option, + hook: CallHook<'ctx>, +} + /// Symbolic executor for SSA functions. pub struct SymExecutor<'ctx> { /// The Z3 context. ctx: &'ctx Context, /// Registered call hooks (address -> handler). - call_hooks: HashMap>, + call_hooks: HashMap>, + /// Direct-call targets that should be forked as interprocedural successors. + direct_call_fork_targets: Option>, +} + +struct ConcreteCopyLoopPlan<'a> { + counter_phi: &'a SSAVar, + src_base: u64, + dst_base: u64, + limit: u64, } impl<'ctx> SymExecutor<'ctx> { @@ -41,15 +101,47 @@ impl<'ctx> SymExecutor<'ctx> { Self { ctx, call_hooks: HashMap::new(), + direct_call_fork_targets: None, } } + /// Replace the direct-call fork target whitelist and return the previous value. + pub fn replace_direct_call_fork_targets( + &mut self, + targets: Option>, + ) -> Option> { + std::mem::replace(&mut self.direct_call_fork_targets, targets) + } + /// Register a call hook for a target address. pub fn register_call_hook(&mut self, addr: u64, hook: F) where F: Fn(&mut SymState<'ctx>) -> SymResult + 'ctx, { - self.call_hooks.insert(addr, Box::new(hook)); + self.call_hooks.insert( + addr, + RegisteredCallHook { + tag: None, + hook: Box::new(hook), + }, + ); + } + + pub fn register_tagged_call_hook(&mut self, addr: u64, tag: CallHookTag, hook: F) + where + F: Fn(&mut SymState<'ctx>) -> SymResult + 'ctx, + { + self.call_hooks.insert( + addr, + RegisteredCallHook { + tag: Some(tag), + hook: Box::new(hook), + }, + ); + } + + pub fn call_hook_tag(&self, addr: u64) -> Option { + self.call_hooks.get(&addr).and_then(|binding| binding.tag) } /// Execute a single SSA operation on the given state. @@ -63,6 +155,7 @@ impl<'ctx> SymExecutor<'ctx> { Copy { dst, src } => { let value = self.read_var(state, src); self.write_var(state, dst, value); + self.propagate_var_provenance(state, dst, self.read_var_provenance(state, src)); Ok(vec![]) } @@ -85,6 +178,10 @@ impl<'ctx> SymExecutor<'ctx> { let size = dst.size; let value = state.mem_read(&addr_val, size); self.write_var(state, dst, value); + let provenance = addr_val + .as_concrete() + .map(|source_addr| RuntimeValueProvenance { source_addr, size }); + self.propagate_var_provenance(state, dst, provenance); Ok(vec![]) } @@ -97,6 +194,10 @@ impl<'ctx> SymExecutor<'ctx> { let value = self.read_var(state, val); let size = val.size; state.mem_write(&addr_val, &value, size); + if let Some(store_addr) = addr_val.as_concrete() { + let provenance = self.read_var_provenance(state, val); + state.note_runtime_store_copy(store_addr, size, provenance.as_ref()); + } Ok(vec![]) } Fence { .. } => Ok(vec![]), @@ -430,7 +531,12 @@ impl<'ctx> SymExecutor<'ctx> { IntEqual { dst, a, b } => { let a_val = self.read_var(state, a); let b_val = self.read_var(state, b); - let result = a_val.eq(self.ctx, &b_val); + let result = + if let Some(equal) = zero_equality_from_known_facts(state, &a_val, &b_val) { + SymValue::concrete(if equal { 1 } else { 0 }, 1) + } else { + a_val.eq(self.ctx, &b_val) + }; self.write_var(state, dst, result); Ok(vec![]) } @@ -438,7 +544,12 @@ impl<'ctx> SymExecutor<'ctx> { IntNotEqual { dst, a, b } => { let a_val = self.read_var(state, a); let b_val = self.read_var(state, b); - let eq = a_val.eq(self.ctx, &b_val); + let eq = if let Some(equal) = zero_equality_from_known_facts(state, &a_val, &b_val) + { + SymValue::concrete(if equal { 1 } else { 0 }, 1) + } else { + a_val.eq(self.ctx, &b_val) + }; // NOT of equality let result = match eq.as_concrete() { Some(v) => SymValue::concrete(if v == 0 { 1 } else { 0 }, 1), @@ -491,6 +602,7 @@ impl<'ctx> SymExecutor<'ctx> { let val = self.read_var(state, src); let result = val.zero_extend(self.ctx, dst.size * 8); self.write_var(state, dst, result); + self.propagate_var_provenance(state, dst, self.read_var_provenance(state, src)); Ok(vec![]) } @@ -498,6 +610,7 @@ impl<'ctx> SymExecutor<'ctx> { let val = self.read_var(state, src); let result = val.sign_extend(self.ctx, dst.size * 8); self.write_var(state, dst, result); + self.propagate_var_provenance(state, dst, self.read_var_provenance(state, src)); Ok(vec![]) } @@ -564,6 +677,12 @@ impl<'ctx> SymExecutor<'ctx> { } }; self.write_var(state, dst, result); + let provenance = if *offset == 0 { + self.read_var_provenance(state, src) + } else { + None + }; + self.propagate_var_provenance(state, dst, provenance); Ok(vec![]) } @@ -608,7 +727,7 @@ impl<'ctx> SymExecutor<'ctx> { // ==================== Control Flow ==================== Branch { target } => { - let target_val = self.read_var(state, target); + let target_val = self.read_control_target_var(state, target); if let Some(addr) = target_val.as_concrete() { state.pc = addr; } @@ -616,7 +735,7 @@ impl<'ctx> SymExecutor<'ctx> { } CBranch { target, cond } => { - let target_val = self.read_var(state, target); + let target_val = self.read_control_target_var(state, target); let cond_val = self.read_var(state, cond); // Check if condition is concrete @@ -629,6 +748,13 @@ impl<'ctx> SymExecutor<'ctx> { } // If c == 0, fall through (don't change PC) Ok(vec![]) + } else if state.value_known_nonzero(&cond_val) { + if let Some(addr) = target_val.as_concrete() { + state.pc = addr; + } + Ok(vec![]) + } else if state.value_known_zero(&cond_val) { + Ok(vec![]) } else { // Symbolic condition - fork execution let target_addr = target_val.as_concrete(); @@ -648,7 +774,7 @@ impl<'ctx> SymExecutor<'ctx> { } BranchInd { target } => { - let target_val = self.read_var(state, target); + let target_val = self.read_control_target_var(state, target); if let Some(addr) = target_val.as_concrete() { state.pc = addr; } else { @@ -659,24 +785,39 @@ impl<'ctx> SymExecutor<'ctx> { } Call { target } => { - let target_val = self.read_var(state, target); - if let Some(addr) = target_val.as_concrete() - && let Some(hook) = self.call_hooks.get(&addr) - { - match hook(state)? { - CallHookResult::Fallthrough => {} - CallHookResult::Jump(new_pc) => state.pc = new_pc, - CallHookResult::Terminate(status) => state.terminate(status), + let target_val = self.read_control_target_var(state, target); + if let Some(addr) = target_val.as_concrete() { + if let Some(binding) = self.call_hooks.get(&addr) { + match (binding.hook)(state)? { + CallHookResult::Fallthrough => {} + CallHookResult::Jump(new_pc) => state.pc = new_pc, + CallHookResult::Terminate(status) => state.terminate(status), + } + } else if self + .direct_call_fork_targets + .as_ref() + .is_some_and(|targets| targets.contains(&addr)) + { + let mut call_state = state.fork(); + call_state.pc = addr; + return Ok(vec![call_state]); } } Ok(vec![]) } CallInd { target } => { - let target_val = self.read_var(state, target); + let target_val = self.read_control_target_var(state, target); if let Some(addr) = target_val.as_concrete() { - if let Some(hook) = self.call_hooks.get(&addr) { - match hook(state)? { + let provenance_addr = self + .read_var_provenance(state, target) + .map(|provenance| provenance.source_addr); + if let Some(binding) = self + .call_hooks + .get(&addr) + .or_else(|| provenance_addr.and_then(|slot| self.call_hooks.get(&slot))) + { + match (binding.hook)(state)? { CallHookResult::Fallthrough => {} CallHookResult::Jump(new_pc) => state.pc = new_pc, CallHookResult::Terminate(status) => state.terminate(status), @@ -691,7 +832,11 @@ impl<'ctx> SymExecutor<'ctx> { } Return { target: _ } => { - state.terminate(ExitStatus::Return); + match state.resume_pending_exception_continuation() { + Ok(Some(resume_pc)) => state.pc = resume_pc, + Ok(None) => state.terminate(ExitStatus::Return), + Err(reason) => state.terminate(ExitStatus::RuntimeBlocked(reason)), + } Ok(vec![]) } @@ -728,9 +873,18 @@ impl<'ctx> SymExecutor<'ctx> { CallOther { output, - userop: _, + userop, inputs: _, } => { + // x86 RDTSC is a timing source, not program data. Model it as a + // stable low-delta value so timing anti-debug checks do not dominate + // exception-continuation analysis. + if *userop == X86_USEROP_RDTSC + && let Some(dst) = output + { + self.write_var(state, dst, SymValue::concrete(0, dst.size * 8)); + return Ok(vec![]); + } // User-defined operation - return symbolic result if let Some(dst) = output { let result = SymValue::new_symbolic(self.ctx, "callother", dst.size * 8); @@ -798,6 +952,7 @@ impl<'ctx> SymExecutor<'ctx> { val }; self.write_var(state, dst, result); + self.propagate_var_provenance(state, dst, self.read_var_provenance(state, src)); Ok(vec![]) } @@ -880,22 +1035,78 @@ impl<'ctx> SymExecutor<'ctx> { } SymValue::concrete(0, var.size * 8) } else if let Some(hex) = var.name.strip_prefix("ram:") { - // Treat RAM addresses as concrete branch targets. if let Ok(value) = u64::from_str_radix(hex, 16) { - SymValue::concrete(value, var.size * 8) + state.mem_read(&SymValue::concrete(value, 64), var.size) } else { SymValue::concrete(0, var.size * 8) } } else { let key = var.display_name(); - state.get_register_sized(&key, var.size * 8) + if let Some(value) = state.registers().get(&key).cloned() { + return value; + } + if let Some(value) = resolve_alias_register_value(state, &key, self.ctx) { + return value; + } + SymValue::unknown(var.size * 8) + } + } + + fn read_control_target_var(&self, state: &SymState<'ctx>, var: &SSAVar) -> SymValue<'ctx> { + if let Some(addr) = parse_address_literal(var) { + SymValue::concrete(addr, var.size * 8) + } else { + self.read_var(state, var) } } /// Write an SSA variable to state. fn write_var(&self, state: &mut SymState<'ctx>, var: &SSAVar, value: SymValue<'ctx>) { + if let Some(addr) = parse_ram_addr(var) { + state.mem_write(&SymValue::concrete(addr, 64), &value, var.size); + return; + } + let key = var.display_name(); + state.set_register(&key, value.clone()); + state.set_value_provenance(&key, None); + if let Some((base, version)) = split_versioned_register(&key) + && let Some(spec) = x86_register_alias_spec(base) + && spec.offset_bits == 0 + && spec.width_bits == 32 + && spec.family.as_ref() != base + { + let family_key = format!("{}_{}", spec.family, version); + state.set_register(&family_key, value.zero_extend(self.ctx, 64)); + state.set_value_provenance(&family_key, None); + } + } + + fn propagate_var_provenance( + &self, + state: &mut SymState<'ctx>, + var: &SSAVar, + provenance: Option, + ) { + let key = var.display_name(); + state.set_value_provenance(&key, provenance); + } + + fn read_var_provenance( + &self, + state: &SymState<'ctx>, + var: &SSAVar, + ) -> Option { + if let Some(source_addr) = parse_ram_addr(var) { + return Some(RuntimeValueProvenance { + source_addr, + size: var.size, + }); + } let key = var.display_name(); - state.set_register(&key, value); + state + .value_provenance(&key) + .cloned() + .or_else(|| resolve_alias_value_provenance(state, &key)) } /// Execute a block of SSA operations. @@ -921,24 +1132,706 @@ impl<'ctx> SymExecutor<'ctx> { } } + if self + .apply_concrete_copy_loop_runahead(state, block) + .is_some() + { + // The runahead leaves the state poised for the final non-taken + // iteration so the normal executor can materialize live-out defs. + } + // Execute operations - for op in &block.ops { + for (op_idx, op) in block.ops.iter().enumerate() { if !state.active { break; } + if self.try_merge_local_fallthrough_cbranch(state, block, op_idx, op)? { + break; + } + + let pc_before_op = state.pc; let new_states = self.step(state, op)?; forked_states.extend(new_states); state.step(); + if block_execution_should_stop(op, pc_before_op, state.pc) { + break; + } } Ok(forked_states) } + + fn try_merge_local_fallthrough_cbranch( + &self, + state: &mut SymState<'ctx>, + block: &FunctionSSABlock, + op_idx: usize, + op: &SSAOp, + ) -> SymResult { + let SSAOp::CBranch { target, cond } = op else { + return Ok(false); + }; + let Some(target_addr) = parse_address_literal(target) else { + return Ok(false); + }; + let block_end = block.addr.saturating_add(block.size as u64); + if target_addr != block_end { + return Ok(false); + } + let tail = &block.ops[op_idx.saturating_add(1)..]; + if tail.is_empty() + || !tail + .iter() + .all(|tail_op| matches!(tail_op, SSAOp::Copy { .. })) + { + return Ok(false); + } + + let cond_val = self.read_var(state, cond); + if cond_val.as_concrete().is_some() + || state.value_known_nonzero(&cond_val) + || state.value_known_zero(&cond_val) + { + return Ok(false); + } + + let mut skipped = state.fork(); + skipped.add_true_constraint(&cond_val); + skipped.pc = target_addr; + skipped.step(); + for tail_op in tail { + if let SSAOp::Copy { dst, .. } = tail_op { + let preserved = self.read_var(state, dst); + self.write_var(&mut skipped, dst, preserved); + } + } + + state.add_false_constraint(&cond_val); + state.step(); + for tail_op in tail { + let forked = self.step(state, tail_op)?; + debug_assert!(forked.is_empty()); + state.step(); + } + state.pc = target_addr; + + let merged = state.merge_with(&skipped); + *state = merged; + Ok(true) + } + + fn apply_concrete_copy_loop_runahead( + &self, + state: &mut SymState<'ctx>, + block: &FunctionSSABlock, + ) -> Option<()> { + let plan = detect_concrete_copy_loop_plan(self, state, block)?; + let counter = self + .read_var(state, plan.counter_phi) + .as_concrete() + .filter(|counter| *counter < plan.limit)?; + let bulk_count = plan.limit.saturating_sub(counter).saturating_sub(1); + if bulk_count == 0 { + return None; + } + + let start_src = plan.src_base.checked_add(counter)?; + let start_dst = plan.dst_base.checked_add(counter)?; + for offset in 0..bulk_count { + let src_addr = start_src.checked_add(offset)?; + let dst_addr = start_dst.checked_add(offset)?; + let byte = state.mem_read(&SymValue::concrete(src_addr, 64), 1); + state.mem_write(&SymValue::concrete(dst_addr, 64), &byte, 1); + } + + let bulk_size = u32::try_from(bulk_count).ok()?; + state.note_runtime_store_copy( + start_dst, + bulk_size, + Some(&RuntimeValueProvenance { + source_addr: start_src, + size: bulk_size, + }), + ); + state.set_concrete( + &plan.counter_phi.display_name(), + plan.limit.saturating_sub(1), + plan.counter_phi.size.saturating_mul(8), + ); + state.step_by(block.ops.len().saturating_mul(bulk_count as usize)); + Some(()) + } +} + +fn detect_concrete_copy_loop_plan<'ctx, 'a>( + executor: &SymExecutor<'ctx>, + state: &SymState<'ctx>, + block: &'a FunctionSSABlock, +) -> Option> { + let SSAOp::CBranch { target, cond } = block.ops.last()? else { + return None; + }; + let target_addr = parse_var_addr(target)?; + if target_addr != block.addr { + return None; + } + + let (counter_next, limit) = find_loop_limit(block, cond)?; + let counter_phi = find_loop_counter_phi(block, counter_next)?; + let src_base = find_load_base(block, counter_phi) + .and_then(|base| resolve_var_concrete(executor, state, base, block))?; + let dst_base = find_store_base(block, counter_phi) + .and_then(|base| resolve_var_concrete(executor, state, base, block))?; + Some(ConcreteCopyLoopPlan { + counter_phi, + src_base, + dst_base, + limit, + }) +} + +fn find_loop_limit<'a>(block: &'a FunctionSSABlock, cond: &SSAVar) -> Option<(&'a SSAVar, u64)> { + let op = find_def(block, cond)?; + match op { + SSAOp::IntLess { a, b, .. } | SSAOp::IntLessEqual { a, b, .. } => { + let limit = parse_const_u64(b)?; + let counter_next = resolve_passthrough_var(a, block); + Some((counter_next, limit)) + } + _ => None, + } +} + +fn find_loop_counter_phi<'a>( + block: &'a FunctionSSABlock, + counter_next: &'a SSAVar, +) -> Option<&'a SSAVar> { + block.phis.iter().find_map(|phi| { + phi.sources + .iter() + .any(|(pred, src)| *pred == block.addr && var_equivalent(src, counter_next, block)) + .then_some(&phi.dst) + }) +} + +fn find_load_base<'a>(block: &'a FunctionSSABlock, counter_phi: &SSAVar) -> Option<&'a SSAVar> { + block.ops.iter().find_map(|op| match op { + SSAOp::Load { dst, addr, .. } if dst.size == 1 => { + match_base_plus_counter(addr, counter_phi, block) + } + _ => None, + }) +} + +fn find_store_base<'a>(block: &'a FunctionSSABlock, counter_phi: &SSAVar) -> Option<&'a SSAVar> { + block.ops.iter().find_map(|op| match op { + SSAOp::Store { addr, val, .. } if val.size == 1 => { + match_base_plus_counter(addr, counter_phi, block) + } + _ => None, + }) +} + +fn find_def<'a>(block: &'a FunctionSSABlock, dst: &SSAVar) -> Option<&'a SSAOp> { + block + .ops + .iter() + .find(|op| op_def(op).is_some_and(|candidate| candidate == dst)) +} + +fn op_def(op: &SSAOp) -> Option<&SSAVar> { + match op { + SSAOp::Phi { dst, .. } + | SSAOp::Copy { dst, .. } + | SSAOp::Load { dst, .. } + | SSAOp::LoadLinked { dst, .. } + | SSAOp::AtomicCAS { dst, .. } + | SSAOp::LoadGuarded { dst, .. } + | SSAOp::IntAdd { dst, .. } + | SSAOp::IntSub { dst, .. } + | SSAOp::IntMult { dst, .. } + | SSAOp::IntDiv { dst, .. } + | SSAOp::IntSDiv { dst, .. } + | SSAOp::IntRem { dst, .. } + | SSAOp::IntSRem { dst, .. } + | SSAOp::IntNegate { dst, .. } + | SSAOp::IntCarry { dst, .. } + | SSAOp::IntSCarry { dst, .. } + | SSAOp::IntSBorrow { dst, .. } + | SSAOp::IntAnd { dst, .. } + | SSAOp::IntOr { dst, .. } + | SSAOp::IntXor { dst, .. } + | SSAOp::IntNot { dst, .. } + | SSAOp::IntLeft { dst, .. } + | SSAOp::IntRight { dst, .. } + | SSAOp::IntSRight { dst, .. } + | SSAOp::IntEqual { dst, .. } + | SSAOp::IntNotEqual { dst, .. } + | SSAOp::IntLess { dst, .. } + | SSAOp::IntSLess { dst, .. } + | SSAOp::IntLessEqual { dst, .. } + | SSAOp::IntSLessEqual { dst, .. } + | SSAOp::IntZExt { dst, .. } + | SSAOp::IntSExt { dst, .. } + | SSAOp::BoolNot { dst, .. } + | SSAOp::BoolAnd { dst, .. } + | SSAOp::BoolOr { dst, .. } + | SSAOp::BoolXor { dst, .. } + | SSAOp::Piece { dst, .. } + | SSAOp::Subpiece { dst, .. } + | SSAOp::PopCount { dst, .. } + | SSAOp::Lzcount { dst, .. } + | SSAOp::CallDefine { dst } + | SSAOp::FloatAdd { dst, .. } + | SSAOp::FloatSub { dst, .. } + | SSAOp::FloatMult { dst, .. } + | SSAOp::FloatDiv { dst, .. } + | SSAOp::FloatNeg { dst, .. } + | SSAOp::FloatAbs { dst, .. } + | SSAOp::FloatSqrt { dst, .. } + | SSAOp::FloatCeil { dst, .. } + | SSAOp::FloatFloor { dst, .. } + | SSAOp::FloatRound { dst, .. } + | SSAOp::FloatNaN { dst, .. } => Some(dst), + SSAOp::StoreConditional { + result: Some(dst), .. + } => Some(dst), + _ => None, + } +} + +fn resolve_passthrough_var<'a>(var: &'a SSAVar, block: &'a FunctionSSABlock) -> &'a SSAVar { + match find_def(block, var) { + Some(SSAOp::Copy { src, .. }) + | Some(SSAOp::IntZExt { src, .. }) + | Some(SSAOp::IntSExt { src, .. }) => resolve_passthrough_var(src, block), + _ => var, + } +} + +fn var_equivalent(var: &SSAVar, target: &SSAVar, block: &FunctionSSABlock) -> bool { + if var == target { + return true; + } + match find_def(block, var) { + Some(SSAOp::Copy { src, .. }) + | Some(SSAOp::IntZExt { src, .. }) + | Some(SSAOp::IntSExt { src, .. }) => var_equivalent(src, target, block), + _ => false, + } +} + +fn var_matches_counter_term(var: &SSAVar, counter_phi: &SSAVar, block: &FunctionSSABlock) -> bool { + if var == counter_phi { + return true; + } + match find_def(block, var) { + Some(SSAOp::Copy { src, .. }) + | Some(SSAOp::IntZExt { src, .. }) + | Some(SSAOp::IntSExt { src, .. }) => var_matches_counter_term(src, counter_phi, block), + Some(SSAOp::IntMult { a, b, .. }) => { + (parse_const_u64(a) == Some(1) && var_matches_counter_term(b, counter_phi, block)) + || (parse_const_u64(b) == Some(1) + && var_matches_counter_term(a, counter_phi, block)) + } + _ => false, + } +} + +fn match_base_plus_counter<'a>( + addr: &'a SSAVar, + counter_phi: &SSAVar, + block: &'a FunctionSSABlock, +) -> Option<&'a SSAVar> { + let op = find_def(block, addr)?; + match op { + SSAOp::IntAdd { a, b, .. } => { + if var_matches_counter_term(a, counter_phi, block) { + Some(b) + } else if var_matches_counter_term(b, counter_phi, block) { + Some(a) + } else { + None + } + } + _ => None, + } +} + +fn resolve_var_concrete<'ctx>( + executor: &SymExecutor<'ctx>, + state: &SymState<'ctx>, + var: &SSAVar, + block: &FunctionSSABlock, +) -> Option { + if let Some(value) = parse_const_u64(var) { + return Some(value); + } + match find_def(block, var) { + Some(SSAOp::Copy { src, .. }) + | Some(SSAOp::IntZExt { src, .. }) + | Some(SSAOp::IntSExt { src, .. }) => resolve_var_concrete(executor, state, src, block), + _ => executor.read_var(state, var).as_concrete(), + } +} + +fn parse_const_u64(var: &SSAVar) -> Option { + var.name + .strip_prefix("const:") + .and_then(|value| u64::from_str_radix(value, 16).ok()) +} + +fn parse_ram_addr(var: &SSAVar) -> Option { + var.name + .strip_prefix("ram:") + .and_then(|value| u64::from_str_radix(value, 16).ok()) +} + +fn parse_address_literal(var: &SSAVar) -> Option { + parse_ram_addr(var).or_else(|| parse_const_u64(var)) +} + +fn parse_var_addr(var: &SSAVar) -> Option { + parse_address_literal(var) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct RegisterAliasSpec { + family: Cow<'static, str>, + offset_bits: u32, + width_bits: u32, +} + +fn resolve_alias_register_value<'ctx>( + state: &SymState<'ctx>, + key: &str, + ctx: &'ctx Context, +) -> Option> { + let (requested_base, _requested_version) = split_versioned_register(key)?; + let requested = x86_register_alias_spec(requested_base)?; + + let mut best: Option<(u32, RegisterAliasSpec, SymValue<'ctx>)> = None; + for (candidate_key, candidate_value) in state.registers() { + let Some((candidate_base, candidate_version)) = split_versioned_register(candidate_key) + else { + continue; + }; + let Some(candidate) = x86_register_alias_spec(candidate_base) else { + continue; + }; + if candidate.family != requested.family { + continue; + } + + let candidate_low = candidate.offset_bits; + let candidate_high = candidate.offset_bits + candidate.width_bits; + let requested_low = requested.offset_bits; + let requested_high = requested.offset_bits + requested.width_bits; + + let covers_requested = candidate_low <= requested_low && candidate_high >= requested_high; + let can_zero_extend = + candidate_low == requested_low && candidate.width_bits < requested.width_bits; + if !covers_requested && !can_zero_extend { + continue; + } + + let should_replace = best.as_ref().is_none_or(|(best_version, best_spec, _)| { + candidate_version > *best_version + || (candidate_version == *best_version + && candidate.width_bits > best_spec.width_bits) + }); + if should_replace { + best = Some((candidate_version, candidate, candidate_value.clone())); + } + } + + let (_version, candidate, value) = best?; + if candidate.offset_bits <= requested.offset_bits + && candidate.offset_bits + candidate.width_bits + >= requested.offset_bits + requested.width_bits + { + let low = requested.offset_bits - candidate.offset_bits; + let high = low + requested.width_bits - 1; + let extracted = value.extract(ctx, high, low); + if extracted.bits() == requested.width_bits { + Some(extracted) + } else if extracted.bits() < requested.width_bits { + Some(extracted.zero_extend(ctx, requested.width_bits)) + } else { + Some(extracted.extract(ctx, requested.width_bits - 1, 0)) + } + } else if candidate.offset_bits == requested.offset_bits + && candidate.width_bits < requested.width_bits + { + Some(value.zero_extend(ctx, requested.width_bits)) + } else { + None + } +} + +fn resolve_alias_value_provenance( + state: &SymState<'_>, + key: &str, +) -> Option { + let (requested_base, _requested_version) = split_versioned_register(key)?; + let requested = x86_register_alias_spec(requested_base)?; + + let mut best: Option<(u32, RegisterAliasSpec, RuntimeValueProvenance)> = None; + for (candidate_key, provenance) in &state.runtime().value_provenance { + let Some((candidate_base, candidate_version)) = split_versioned_register(candidate_key) + else { + continue; + }; + let Some(candidate) = x86_register_alias_spec(candidate_base) else { + continue; + }; + if candidate.family != requested.family { + continue; + } + let should_replace = best.as_ref().is_none_or(|(best_version, best_spec, _)| { + candidate_version > *best_version + || (candidate_version == *best_version + && candidate.width_bits > best_spec.width_bits) + }); + if should_replace { + best = Some((candidate_version, candidate, provenance.clone())); + } + } + best.map(|(_, _, provenance)| provenance) +} + +fn split_versioned_register(name: &str) -> Option<(&str, u32)> { + let (base, version) = name.rsplit_once('_')?; + if version.is_empty() || !version.chars().all(|c| c.is_ascii_digit()) { + return None; + } + Some((base, version.parse().ok()?)) +} + +fn x86_register_alias_spec(base: &str) -> Option { + let upper = base.to_ascii_uppercase(); + let base = upper.as_str(); + let fixed = match base { + "AL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 0, + width_bits: 8, + }), + "AH" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 8, + width_bits: 8, + }), + "AX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 0, + width_bits: 16, + }), + "EAX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 0, + width_bits: 32, + }), + "RAX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 0, + width_bits: 64, + }), + "BL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 0, + width_bits: 8, + }), + "BH" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 8, + width_bits: 8, + }), + "BX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 0, + width_bits: 16, + }), + "EBX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 0, + width_bits: 32, + }), + "RBX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 0, + width_bits: 64, + }), + "CL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 0, + width_bits: 8, + }), + "CH" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 8, + width_bits: 8, + }), + "CX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 0, + width_bits: 16, + }), + "ECX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 0, + width_bits: 32, + }), + "RCX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 0, + width_bits: 64, + }), + "DL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 0, + width_bits: 8, + }), + "DH" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 8, + width_bits: 8, + }), + "DX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 0, + width_bits: 16, + }), + "EDX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 0, + width_bits: 32, + }), + "RDX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 0, + width_bits: 64, + }), + "SIL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSI"), + offset_bits: 0, + width_bits: 8, + }), + "SI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSI"), + offset_bits: 0, + width_bits: 16, + }), + "ESI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSI"), + offset_bits: 0, + width_bits: 32, + }), + "RSI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSI"), + offset_bits: 0, + width_bits: 64, + }), + "DIL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDI"), + offset_bits: 0, + width_bits: 8, + }), + "DI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDI"), + offset_bits: 0, + width_bits: 16, + }), + "EDI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDI"), + offset_bits: 0, + width_bits: 32, + }), + "RDI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDI"), + offset_bits: 0, + width_bits: 64, + }), + "BPL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBP"), + offset_bits: 0, + width_bits: 8, + }), + "BP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBP"), + offset_bits: 0, + width_bits: 16, + }), + "EBP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBP"), + offset_bits: 0, + width_bits: 32, + }), + "RBP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBP"), + offset_bits: 0, + width_bits: 64, + }), + "SPL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSP"), + offset_bits: 0, + width_bits: 8, + }), + "SP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSP"), + offset_bits: 0, + width_bits: 16, + }), + "ESP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSP"), + offset_bits: 0, + width_bits: 32, + }), + "RSP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSP"), + offset_bits: 0, + width_bits: 64, + }), + _ => None, + }; + if fixed.is_some() { + return fixed; + } + + parse_numbered_x86_register_alias(base) +} + +fn parse_numbered_x86_register_alias(base: &str) -> Option { + let (family, width_bits) = if let Some(family) = base.strip_suffix('B') { + (family.to_string(), 8) + } else if let Some(family) = base.strip_suffix('W') { + (family.to_string(), 16) + } else if let Some(family) = base.strip_suffix('D') { + (family.to_string(), 32) + } else { + (base.to_string(), 64) + }; + + if !family.starts_with('R') { + return None; + } + let digits = &family[1..]; + if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) { + return None; + } + + Some(RegisterAliasSpec { + family: Cow::Owned(family), + offset_bits: 0, + width_bits, + }) } #[cfg(test)] mod tests { use super::*; + use crate::MemoryRegionKind; #[test] fn test_copy_op() { @@ -961,6 +1854,47 @@ mod tests { assert_eq!(dst_val.as_concrete(), Some(42)); } + #[test] + fn test_ram_copy_operands_use_memory_not_address_literals() { + let ctx = Context::thread_local(); + let executor = SymExecutor::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + let region = + state.define_memory_region(MemoryRegionKind::Global, "global", Some(0x2000), Some(8)); + state.seed_region_bytes(region, 0, &0x1122_3344_5566_7788u64.to_le_bytes()); + + executor + .step( + &mut state, + &SSAOp::Copy { + dst: SSAVar::new("dst", 1, 8), + src: SSAVar::new("ram:2000", 0, 8), + }, + ) + .expect("global load should execute"); + assert_eq!( + state.get_register("DST_1").as_concrete(), + Some(0x1122_3344_5566_7788) + ); + + state.set_register("SRC_0", SymValue::concrete(0xaabb_ccdd_eeff_0011, 64)); + executor + .step( + &mut state, + &SSAOp::Copy { + dst: SSAVar::new("ram:2000", 1, 8), + src: SSAVar::new("src", 0, 8), + }, + ) + .expect("global store should execute"); + assert_eq!( + state + .mem_read(&SymValue::concrete(0x2000, 64), 8) + .as_concrete(), + Some(0xaabb_ccdd_eeff_0011) + ); + } + #[test] fn test_add_op() { let ctx = Context::thread_local(); @@ -1003,6 +1937,27 @@ mod tests { assert_eq!(state.pc, 0x2000); // Branch taken } + #[test] + fn test_cbranch_ram_target_stays_address_literal() { + let ctx = Context::thread_local(); + let executor = SymExecutor::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.set_register("COND_0", SymValue::concrete(1, 1)); + + let forked = executor + .step( + &mut state, + &SSAOp::CBranch { + target: SSAVar::new("ram:3000", 0, 8), + cond: SSAVar::new("cond", 0, 1), + }, + ) + .expect("branch should execute"); + + assert!(forked.is_empty()); + assert_eq!(state.pc, 0x3000); + } + #[test] fn test_cbranch_symbolic() { let ctx = Context::thread_local(); @@ -1023,4 +1978,339 @@ mod tests { assert_eq!(forked[0].pc, 0x2000); // True branch goes to target // Original state is false branch (PC unchanged in this test) } + + #[test] + fn test_read_var_recovers_x86_subregister_alias() { + let ctx = Context::thread_local(); + let executor = SymExecutor::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + + state.set_register("EAX_4", SymValue::concrete(0x6b, 32)); + + let al = SSAVar::new("AL", 0, 1); + let value = executor.read_var(&state, &al); + assert_eq!(value.as_concrete(), Some(0x6b)); + assert_eq!(value.bits(), 8); + } + + #[test] + fn test_realistic_al_compare_path_forks_on_loaded_byte() { + let ctx = Context::thread_local(); + let executor = SymExecutor::new(&ctx); + let mut state = SymState::new(&ctx, 0x401c6d); + + let addr = SymValue::concrete(0x2ff6, 64); + let byte = SymValue::new_symbolic(&ctx, "stdin_byte", 8); + state.mem_write(&addr, &byte, 1); + state.set_register("tmp:4700_5", addr.clone()); + + let ops = vec![ + SSAOp::Load { + dst: SSAVar::new("tmp:11e00", 1, 1), + addr: SSAVar::new("tmp:4700", 5, 8), + space: "ram".to_string(), + }, + SSAOp::IntZExt { + dst: SSAVar::new("EAX", 4, 4), + src: SSAVar::new("tmp:11e00", 1, 1), + }, + SSAOp::IntZExt { + dst: SSAVar::new("RAX", 6, 8), + src: SSAVar::new("EAX", 4, 4), + }, + SSAOp::IntLess { + dst: SSAVar::new("CF", 6, 1), + a: SSAVar::new("AL", 0, 1), + b: SSAVar::constant(0x6b, 1), + }, + SSAOp::IntSBorrow { + dst: SSAVar::new("OF", 6, 1), + a: SSAVar::new("AL", 0, 1), + b: SSAVar::constant(0x6b, 1), + }, + SSAOp::IntSub { + dst: SSAVar::new("tmp:3de00", 1, 1), + a: SSAVar::new("AL", 0, 1), + b: SSAVar::constant(0x6b, 1), + }, + SSAOp::IntSLess { + dst: SSAVar::new("SF", 6, 1), + a: SSAVar::new("tmp:3de00", 1, 1), + b: SSAVar::constant(0, 1), + }, + SSAOp::IntEqual { + dst: SSAVar::new("ZF", 6, 1), + a: SSAVar::new("tmp:3de00", 1, 1), + b: SSAVar::constant(0, 1), + }, + SSAOp::BoolNot { + dst: SSAVar::new("tmp:12800", 1, 1), + src: SSAVar::new("ZF", 6, 1), + }, + SSAOp::CBranch { + target: SSAVar::constant(0x401c86, 8), + cond: SSAVar::new("tmp:12800", 1, 1), + }, + ]; + + let mut forked = Vec::new(); + for op in &ops { + let new_states = executor.step(&mut state, op).unwrap(); + forked.extend(new_states); + } + + assert_eq!(forked.len(), 1, "true branch should fork to the target"); + assert_eq!(forked[0].pc, 0x401c86); + assert!( + state.get_register("tmp:12800_1").as_concrete().is_none(), + "false branch condition should remain symbolic" + ); + } + + #[test] + fn test_callind_uses_provenance_source_for_hook_lookup() { + let ctx = Context::thread_local(); + let mut executor = SymExecutor::new(&ctx); + executor.register_call_hook(0x401000, |state| { + state.set_concrete("RAX_0", 0x99, 64); + Ok(CallHookResult::Fallthrough) + }); + + let mut state = SymState::new(&ctx, 0x1000); + state.set_register("TMP_0", SymValue::concrete(0x12345678, 64)); + state.set_value_provenance( + "TMP_0", + Some(RuntimeValueProvenance { + source_addr: 0x401000, + size: 8, + }), + ); + + let op = SSAOp::CallInd { + target: SSAVar::new("tmp", 0, 8), + }; + + executor + .step(&mut state, &op) + .expect("callind should execute"); + assert_eq!(state.get_register("RAX_0").as_concrete(), Some(0x99)); + } + + #[test] + fn test_direct_call_fork_requires_whitelist() { + let ctx = Context::thread_local(); + let mut executor = SymExecutor::new(&ctx); + let op = SSAOp::Call { + target: SSAVar::constant(0x401000, 8), + }; + + let mut state = SymState::new(&ctx, 0x1000); + let forks = executor + .step(&mut state, &op) + .expect("direct call should execute"); + assert!( + forks.is_empty(), + "direct calls should remain fallthrough-only by default" + ); + + let mut targets = HashSet::new(); + targets.insert(0x401000); + executor.replace_direct_call_fork_targets(Some(targets)); + + let mut state = SymState::new(&ctx, 0x1000); + let forks = executor + .step(&mut state, &op) + .expect("direct call should execute"); + assert_eq!(forks.len(), 1); + assert_eq!(forks[0].pc, 0x401000); + assert_eq!(state.pc, 0x1000, "caller fallthrough state is preserved"); + } + + #[test] + fn test_execute_block_skips_local_cbranch_tail_when_taken() { + let ctx = Context::thread_local(); + let executor = SymExecutor::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.set_register("COND_0", SymValue::concrete(1, 1)); + state.set_register("RAX_0", SymValue::concrete(0x11, 64)); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: Vec::new(), + ops: vec![ + SSAOp::CBranch { + target: SSAVar::new("ram:1004", 0, 8), + cond: SSAVar::new("cond", 0, 1), + }, + SSAOp::Copy { + dst: SSAVar::new("rax", 1, 8), + src: SSAVar::constant(0x22, 8), + }, + ], + }; + + let forks = executor + .execute_block(&mut state, &block) + .expect("block should execute"); + + assert!(forks.is_empty()); + assert_eq!(state.pc, 0x1004); + assert_eq!( + executor + .read_var(&state, &SSAVar::new("rax", 1, 8)) + .as_concrete(), + Some(0x11) + ); + } + + #[test] + fn test_execute_block_merges_symbolic_local_cbranch_tail() { + let ctx = Context::thread_local(); + let executor = SymExecutor::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.set_register("COND_0", SymValue::new_symbolic(&ctx, "cond", 1)); + state.set_register("RAX_0", SymValue::concrete(0x11, 64)); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: Vec::new(), + ops: vec![ + SSAOp::CBranch { + target: SSAVar::new("ram:1004", 0, 8), + cond: SSAVar::new("cond", 0, 1), + }, + SSAOp::Copy { + dst: SSAVar::new("rax", 1, 8), + src: SSAVar::constant(0x22, 8), + }, + ], + }; + + let forks = executor + .execute_block(&mut state, &block) + .expect("block should execute"); + + assert!(forks.is_empty()); + assert_eq!(state.pc, 0x1004); + assert!( + executor + .read_var(&state, &SSAVar::new("rax", 1, 8)) + .as_concrete() + .is_none(), + "symbolic local branch should become predicated dataflow, not a path fork" + ); + } + + #[test] + fn test_callind_uses_direct_ram_source_provenance_for_import_hook_lookup() { + let ctx = Context::thread_local(); + let mut executor = SymExecutor::new(&ctx); + executor.register_call_hook(0x401000, |state| { + state.set_concrete("RAX_0", 0x1234, 64); + Ok(CallHookResult::Fallthrough) + }); + + let mut state = SymState::new(&ctx, 0x1000); + executor + .step( + &mut state, + &SSAOp::Copy { + dst: SSAVar::new("tmp:import", 0, 8), + src: SSAVar::new("ram:401000", 0, 8), + }, + ) + .expect("import slot load should execute"); + executor + .step( + &mut state, + &SSAOp::CallInd { + target: SSAVar::new("tmp:import", 0, 8), + }, + ) + .expect("callind should execute through source provenance"); + + assert_eq!(state.get_register("RAX_0").as_concrete(), Some(0x1234)); + } + + #[test] + fn test_execute_block_runahead_collapses_concrete_copy_loop() { + let ctx = Context::thread_local(); + let executor = SymExecutor::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + + let source_region = + state.define_memory_region(MemoryRegionKind::Global, "blob", Some(0x2000), Some(4)); + state.seed_region_bytes(source_region, 0, &[0x41, 0x42, 0x43, 0x44]); + + let (_region_id, runtime_base) = state.allocate_heap_region("jit_blob", 4); + state.register_runtime_region_alias(runtime_base, 4, true); + state.set_concrete("RBX_0", runtime_base, 64); + state.set_prev_pc(Some(0x0)); + + let block = FunctionSSABlock { + addr: 0x1000, + size: 7, + phis: vec![r2ssa::PhiNode { + dst: SSAVar::new("RCX", 1, 8), + sources: vec![ + (0x0, SSAVar::constant(0, 8)), + (0x1000, SSAVar::new("RCX", 2, 8)), + ], + }], + ops: vec![ + SSAOp::IntAdd { + dst: SSAVar::new("tmp:src", 0, 8), + a: SSAVar::constant(0x2000, 8), + b: SSAVar::new("RCX", 1, 8), + }, + SSAOp::Load { + dst: SSAVar::new("tmp:byte", 0, 1), + addr: SSAVar::new("tmp:src", 0, 8), + space: "ram".to_string(), + }, + SSAOp::IntAdd { + dst: SSAVar::new("tmp:dst", 0, 8), + a: SSAVar::new("RBX", 0, 8), + b: SSAVar::new("RCX", 1, 8), + }, + SSAOp::Store { + space: "ram".to_string(), + addr: SSAVar::new("tmp:dst", 0, 8), + val: SSAVar::new("tmp:byte", 0, 1), + }, + SSAOp::IntAdd { + dst: SSAVar::new("RCX", 2, 8), + a: SSAVar::new("RCX", 1, 8), + b: SSAVar::constant(1, 8), + }, + SSAOp::IntLess { + dst: SSAVar::new("CF", 0, 1), + a: SSAVar::new("RCX", 2, 8), + b: SSAVar::constant(4, 8), + }, + SSAOp::CBranch { + target: SSAVar::new("ram:1000", 0, 8), + cond: SSAVar::new("CF", 0, 1), + }, + ], + }; + + let forked = executor + .execute_block(&mut state, &block) + .expect("copy loop block should execute"); + assert!(forked.is_empty()); + assert_eq!(state.pc, 0x1000); + assert_eq!(state.get_register("RCX_2").as_concrete(), Some(4)); + assert_eq!( + state + .mem_read(&SymValue::concrete(runtime_base, 64), 4) + .as_concrete(), + Some(0x4443_4241) + ); + let region = state + .runtime_region_for_pc(runtime_base) + .expect("runtime alias should remain registered"); + assert_eq!(region.source_base, Some(0x2000)); + assert!(state.depth >= block.ops.len() * 4); + } } diff --git a/crates/r2sym/src/kernel.rs b/crates/r2sym/src/kernel.rs new file mode 100644 index 0000000..b0f69f2 --- /dev/null +++ b/crates/r2sym/src/kernel.rs @@ -0,0 +1,595 @@ +//! Canonical semantic graph and fact-planning scaffolding. +//! +//! This module is intentionally small for the first architecture tranche: it +//! gives SSA, symex, runtime, and future replay backends a typed place to meet +//! without putting semantic policy in the plugin or executor. + +use std::collections::{BTreeMap, BTreeSet, VecDeque}; + +use r2ssa::InterprocFunctionId; + +use crate::PreparedFunctionScope; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FunctionId(pub u64); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct NodeId(pub u64); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct EdgeId(pub u64); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RegionId(pub u64); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum FactPrecision { + Unknown, + UnderApprox, + Residual, + OverApprox, + Exact, +} + +impl FactPrecision { + pub fn is_exact(self) -> bool { + matches!(self, Self::Exact) + } + + pub fn is_residual_or_weaker(self) -> bool { + matches!(self, Self::Unknown | Self::UnderApprox | Self::Residual) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum SemanticEvidenceKind { + Static, + Runtime, + Replay, + UserAssumption, + Inferred, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SemanticEvidenceRecord { + pub kind: SemanticEvidenceKind, + pub precision: FactPrecision, + pub reason: String, +} + +impl SemanticEvidenceRecord { + pub fn exact_static(reason: impl Into) -> Self { + Self { + kind: SemanticEvidenceKind::Static, + precision: FactPrecision::Exact, + reason: reason.into(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SemanticNodeKind { + FunctionEntry { + function: FunctionId, + }, + BasicBlock { + function: FunctionId, + addr: u64, + }, + RuntimeRegion { + region: RegionId, + base: u64, + size: u64, + }, + ExceptionContinuation { + handler: u64, + }, + ReplayCheckpoint { + checkpoint: u64, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SemanticNode { + pub id: NodeId, + pub kind: SemanticNodeKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SemanticEdgeKind { + Cfg, + Call, + Return, + Thunk, + Exception, + RuntimeAlias, + Replay, + Summary, + Refused { reason: String }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CallEdgePolicy { + FallthroughOnly, + InlineCallee, + ApplySummary, + ForkCallAndFallthrough, + Refuse, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SemanticEdgeAction { + Follow, + ApplyCallPolicy(CallEdgePolicy), + SeedContinuation, + MapRuntimeRegion, + ApplySummary, + Refuse { reason: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SemanticEdge { + pub id: EdgeId, + pub from: NodeId, + pub to: NodeId, + pub kind: SemanticEdgeKind, + pub action: SemanticEdgeAction, + pub evidence: SemanticEvidenceRecord, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RuntimeRegionFact { + pub id: RegionId, + pub base: u64, + pub size: u64, + pub source_base: Option, + pub executable: bool, + pub evidence: SemanticEvidenceRecord, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ConcreteStopReason { + Completed, + HitTarget(u64), + Exception(u64), + BudgetExceeded, + Unsupported(String), +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ConcreteTraceEvidence { + pub executed_blocks: Vec, + pub runtime_regions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConcreteMemorySeed { + pub addr: u64, + pub bytes: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConcreteRunRequest { + pub entry: u64, + pub target: Option, + pub max_instructions: usize, + pub argv: Vec>, + pub registers: BTreeMap, + pub memory: Vec, + pub runtime_regions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConcreteRunResult { + pub trace: ConcreteTraceEvidence, + pub stop_reason: ConcreteStopReason, + pub final_pc: Option, + pub diagnostics: Vec, +} + +pub trait ConcreteExecutionBackend { + fn run(&self, request: ConcreteRunRequest) -> ConcreteRunResult; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FunctionClosureBudget { + pub max_functions: usize, + pub max_blocks_per_function: usize, + pub max_total_blocks: usize, +} + +impl Default for FunctionClosureBudget { + fn default() -> Self { + Self { + max_functions: 32, + max_blocks_per_function: 96, + max_total_blocks: 512, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FunctionClosureReason { + Root, + HelperByScope, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FunctionClosureExclusionReason { + FunctionBudgetExceeded, + BlockBudgetExceeded, + TotalBlockBudgetExceeded, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FunctionClosureEntry { + pub function: FunctionId, + pub name: Option, + pub block_count: usize, + pub reason: FunctionClosureReason, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FunctionClosureExclusion { + pub function: FunctionId, + pub name: Option, + pub block_count: usize, + pub reason: FunctionClosureExclusionReason, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FunctionClosurePlan { + pub root: FunctionId, + pub budget: FunctionClosureBudget, + pub included: Vec, + pub excluded: Vec, +} + +impl FunctionClosurePlan { + pub fn from_scope(scope: &PreparedFunctionScope, budget: FunctionClosureBudget) -> Self { + let root = FunctionId(scope.root_id().0); + let mut total_blocks = 0usize; + let mut included = Vec::new(); + let mut excluded = Vec::new(); + + for function in scope.functions().values() { + let id = FunctionId(function.id.0); + let block_count = function.prepared.cfg().num_blocks(); + let is_root = function.id == InterprocFunctionId(root.0); + let exclusion = if !is_root && included.len() >= budget.max_functions { + Some(FunctionClosureExclusionReason::FunctionBudgetExceeded) + } else if !is_root && block_count > budget.max_blocks_per_function { + Some(FunctionClosureExclusionReason::BlockBudgetExceeded) + } else if !is_root && total_blocks.saturating_add(block_count) > budget.max_total_blocks + { + Some(FunctionClosureExclusionReason::TotalBlockBudgetExceeded) + } else { + None + }; + + if let Some(reason) = exclusion { + excluded.push(FunctionClosureExclusion { + function: id, + name: function.name.clone(), + block_count, + reason, + }); + continue; + } + + total_blocks = total_blocks.saturating_add(block_count); + included.push(FunctionClosureEntry { + function: id, + name: function.name.clone(), + block_count, + reason: if is_root { + FunctionClosureReason::Root + } else { + FunctionClosureReason::HelperByScope + }, + }); + } + + Self { + root, + budget, + included, + excluded, + } + } + + pub fn included_ids(&self) -> BTreeSet { + self.included + .iter() + .map(|entry| entry.function) + .collect::>() + } +} + +#[derive(Debug, Clone, Default)] +pub struct SemanticProgramGraph { + nodes: BTreeMap, + edges: BTreeMap, + block_nodes: BTreeMap<(FunctionId, u64), NodeId>, + addr_nodes: BTreeMap>, + successors: BTreeMap>, + predecessors: BTreeMap>, + next_node: u64, + next_edge: u64, +} + +impl SemanticProgramGraph { + pub fn new() -> Self { + Self::default() + } + + pub fn from_scope(scope: &PreparedFunctionScope, closure: &FunctionClosurePlan) -> Self { + let mut graph = Self::new(); + let included = closure.included_ids(); + for function in scope.functions().values() { + let function_id = FunctionId(function.id.0); + if !included.contains(&function_id) { + continue; + } + graph.add_node(SemanticNodeKind::FunctionEntry { + function: function_id, + }); + for addr in function.prepared.cfg().block_addrs() { + graph.add_block_node(function_id, addr); + } + } + + for function in scope.functions().values() { + let function_id = FunctionId(function.id.0); + if !included.contains(&function_id) { + continue; + } + for addr in function.prepared.cfg().block_addrs() { + let Some(block) = function.prepared.cfg().get_block(addr) else { + continue; + }; + for succ in block.successors() { + graph.add_block_edge( + function_id, + addr, + function_id, + succ, + SemanticEdgeKind::Cfg, + SemanticEdgeAction::Follow, + ); + } + } + for call in function.prepared.call_sites().by_id.values() { + let Some(target) = function.prepared.resolved_call_target(call) else { + continue; + }; + let Some((block_addr, _)) = function.prepared.inst_op_site(call.at) else { + continue; + }; + let callee_id = scope + .function_containing_block(target) + .map(|callee| FunctionId(callee.id.0)) + .unwrap_or(FunctionId(target)); + graph.add_block_edge( + function_id, + block_addr, + callee_id, + target, + SemanticEdgeKind::Call, + SemanticEdgeAction::ApplyCallPolicy(CallEdgePolicy::ForkCallAndFallthrough), + ); + } + } + graph + } + + pub fn nodes(&self) -> &BTreeMap { + &self.nodes + } + + pub fn edges(&self) -> &BTreeMap { + &self.edges + } + + pub fn node_for_block(&self, function: FunctionId, addr: u64) -> Option { + self.block_nodes.get(&(function, addr)).copied() + } + + pub fn nodes_for_addr(&self, addr: u64) -> Option<&BTreeSet> { + self.addr_nodes.get(&addr) + } + + pub fn reverse_reachable_nodes(&self, target: NodeId) -> BTreeSet { + let mut seen = BTreeSet::new(); + let mut queue = VecDeque::from([target]); + while let Some(node) = queue.pop_front() { + if !seen.insert(node) { + continue; + } + for pred in self.predecessors.get(&node).into_iter().flatten() { + queue.push_back(*pred); + } + } + seen + } + + fn add_node(&mut self, kind: SemanticNodeKind) -> NodeId { + let id = NodeId(self.next_node); + self.next_node = self.next_node.saturating_add(1); + if let SemanticNodeKind::BasicBlock { function, addr } = kind { + self.block_nodes.insert((function, addr), id); + self.addr_nodes.entry(addr).or_default().insert(id); + self.nodes.insert(id, SemanticNode { id, kind }); + return id; + } + self.nodes.insert(id, SemanticNode { id, kind }); + id + } + + fn add_block_node(&mut self, function: FunctionId, addr: u64) -> NodeId { + if let Some(id) = self.node_for_block(function, addr) { + return id; + } + self.add_node(SemanticNodeKind::BasicBlock { function, addr }) + } + + fn add_block_edge( + &mut self, + from_function: FunctionId, + from_addr: u64, + to_function: FunctionId, + to_addr: u64, + kind: SemanticEdgeKind, + action: SemanticEdgeAction, + ) -> EdgeId { + let from = self.add_block_node(from_function, from_addr); + let to = self.add_block_node(to_function, to_addr); + let id = EdgeId(self.next_edge); + self.next_edge = self.next_edge.saturating_add(1); + self.successors.entry(from).or_default().insert(to); + self.predecessors.entry(to).or_default().insert(from); + self.edges.insert( + id, + SemanticEdge { + id, + from, + to, + kind, + action, + evidence: SemanticEvidenceRecord::exact_static("scope graph"), + }, + ); + id + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use r2il::{R2ILBlock, R2ILOp, SpaceId, Varnode}; + use r2ssa::SsaArtifact; + + fn make_const(val: u64) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: val, + size: 8, + meta: None, + } + } + + fn leaf(addr: u64) -> SsaArtifact { + SsaArtifact::for_symbolic( + &[R2ILBlock { + addr, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0), + }], + switch_info: None, + op_metadata: Default::default(), + }], + None, + ) + .expect("leaf SSA") + } + + #[test] + fn closure_plan_is_deterministic_and_budgeted() { + let root = leaf(0x1000); + let helper_a = leaf(0x2000); + let helper_b = leaf(0x3000); + let scope = PreparedFunctionScope::new( + 0x1000, + vec![ + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x3000), + name: Some("helper_b".to_string()), + prepared: helper_b, + }, + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root, + }, + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("helper_a".to_string()), + prepared: helper_a, + }, + ], + ) + .expect("scope"); + + let plan = FunctionClosurePlan::from_scope( + &scope, + FunctionClosureBudget { + max_functions: 2, + ..FunctionClosureBudget::default() + }, + ); + + assert_eq!( + plan.included + .iter() + .map(|entry| entry.function.0) + .collect::>(), + vec![0x1000, 0x2000] + ); + assert_eq!(plan.excluded.len(), 1); + assert_eq!( + plan.excluded[0].reason, + FunctionClosureExclusionReason::FunctionBudgetExceeded + ); + } + + #[test] + fn graph_reverse_reachability_is_indexed_by_nodes() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1004), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let root = SsaArtifact::for_symbolic(&blocks, None).expect("root SSA"); + let scope = PreparedFunctionScope::new( + 0x1000, + vec![crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root, + }], + ) + .expect("scope"); + let closure = FunctionClosurePlan::from_scope(&scope, FunctionClosureBudget::default()); + let graph = SemanticProgramGraph::from_scope(&scope, &closure); + let target = graph + .node_for_block(FunctionId(0x1000), 0x1004) + .expect("target node"); + + let reachable = graph.reverse_reachable_nodes(target); + assert!(reachable.contains(&target)); + assert!( + reachable.contains( + &graph + .node_for_block(FunctionId(0x1000), 0x1000) + .expect("entry node") + ) + ); + } +} diff --git a/crates/r2sym/src/lib.rs b/crates/r2sym/src/lib.rs index af3b518..dba3b99 100644 --- a/crates/r2sym/src/lib.rs +++ b/crates/r2sym/src/lib.rs @@ -33,27 +33,131 @@ //! } //! ``` +pub mod backward; +pub mod constraints; pub mod executor; +pub mod kernel; +pub mod loops; pub mod memory; pub mod path; +pub mod query; pub mod r2api; +pub mod replay; pub mod runtime; +pub mod semantics; pub mod sim; pub mod solver; +pub mod spec; pub mod state; +pub mod tactics; pub mod value; +pub mod verification; +pub use backward::{ + BackwardConditionPrecision, BackwardConditionSummary, BackwardMemoryCondition, + BackwardMemoryRegion, BackwardRegionRef, CompiledBackwardCondition, + compile_derived_summary_return_postcondition, compile_target_precondition, +}; +pub use constraints::{ + FinalConstraint, FinalConstraintGraph, FinalConstraintPrecision, FinalConstraintSource, + FoldAggregateConstraint, FoldAggregateRangeConstraint, InputByteConstraint, + InputLengthConstraint, RecurrenceAggregateConstraint, RecurrenceAggregateRangeConstraint, + aggregate_exact_fold_bytes, build_exact_fold_constraint_graph, + build_final_constraint_graph_for_path, build_model_conditioned_recurrence_constraint_graph, + exact_fold_model_bytes, +}; pub use executor::{CallHookResult, SymExecutor}; -pub use memory::SymMemory; -pub use path::{ExploreConfig, PathExplorer, PathResult, SolvedPath}; +pub use kernel::{ + CallEdgePolicy, ConcreteExecutionBackend, ConcreteMemorySeed, ConcreteRunRequest, + ConcreteRunResult, ConcreteStopReason, ConcreteTraceEvidence, EdgeId, FactPrecision, + FunctionClosureBudget, FunctionClosureEntry, FunctionClosureExclusion, + FunctionClosureExclusionReason, FunctionClosurePlan, FunctionClosureReason, FunctionId, + NodeId as SemanticNodeId, RegionId as SemanticRegionId, RuntimeRegionFact, SemanticEdge, + SemanticEdgeAction, SemanticEdgeKind, SemanticEvidenceKind, SemanticEvidenceRecord, + SemanticNode, SemanticNodeKind, SemanticProgramGraph, +}; +pub use loops::{ + ExactLoopFoldEvidence, ExactLoopRecurrenceEvidence, ExactLoopRecurrenceKind, LoopCarriedVar, + LoopFoldOperation, LoopMemoryTerm, LoopMemoryTermKind, LoopRecurrence, LoopRecurrenceKind, + LoopRotateDirection, LoopSummary, LoopSummaryKind, LoopTransitionExpr, LoopTransitionSystem, + LoopVarRole, RuntimeLoopBranch, exact_fold_evidence_from_recurrences, +}; +pub use memory::{ + MemoryRegionId, MemoryRegionKind, RegionPointer, ResolvedPointerSet, SymMemory, + SymbolicMemoryRegionDef, +}; +pub use path::{ + ExploreConfig, PathExplorer, PathResult, SolvedPath, SolvedPathGeneration, + SolvedPathGenerationKind, +}; +pub use query::{ + PathConditionResult, PathConditionSummary, PathConditionTerm, QueryCompletion, + QueryExecutionPolicy, QueryMode, ReachabilityResult, ReachabilityStatus, SolveResult, + SymQueryConfig, SymbolicConditionSet, SymbolicFunctionSummary, apply_query_execution_policy, + install_symbolic_hooks_for_query_policy, recommended_query_max_depth, + recommended_query_max_depth_for_route, recommended_query_max_states_for_route, + recommended_query_timeout, recommended_query_timeout_for_route, + route_skips_eager_scope_summaries, selected_target_query_route_in_scope, +}; pub use r2api::{R2Api, R2Error}; -pub use runtime::seed_default_state_for_arch; +pub use replay::{ + ReplayMemoryOverlay, ReplayMemoryWindow, ReplayRegisterOverlay, ReplayRegisterValue, + ReplaySeed, apply_replay_seed_to_state, seed_replay_state_for_arch, + stable_replay_seed_fingerprint, +}; +pub use runtime::{ + install_runtime_hooks_for_scope, seed_default_state_for_arch, seed_memory_regions_for_arch, + seed_scope_state_for_arch, +}; +pub use semantics::{ + ArtifactBuildPlan, ArtifactGranularity, ControlFact, DecompilePlan, ExecutionModel, + InterpreterDispatchSummary, InterpreterKind, Judged, MemoryFact, NativeArtifactBody, + NativeFunctionSummary, QueryGuidanceMode, QueryPlan, RefinementStage, RegionKey, + ResidualReason, SEMANTIC_ARTIFACT_SCHEMA_VERSION, SemanticArtifact, SemanticArtifactBody, + SemanticArtifactDiagnostics, SemanticCompilationResult, SemanticConfidence, SemanticEvidence, + SemanticEvidenceAmbiguity, SemanticEvidenceCoverage, SemanticEvidenceProvenance, + SemanticEvidenceReason, SemanticEvidenceSoundness, SemanticPredicate, SemanticRegion, + SemanticSeedMode, SemanticTargetConditionSource, SliceClass, SymbolicReachabilityStatus, + TargetFact, TargetQueryExecutionRoute, TargetQueryPlan, TargetQueryRoutePlan, TypePlan, + VmArtifactBody, VmBinaryOp, VmGuardCondition, VmGuardedExit, VmMemoryCondition, + VmMemoryRegionRef, VmStateUpdate, VmStepSummary, VmTransferArm, VmUnaryOp, VmValueExpr, + compile_function_semantics_with_scope, compile_function_semantics_with_scope_and_replay_seed, + compile_query_semantic_artifact_with_scope, + compile_query_semantic_artifact_with_scope_and_replay_seed, + compile_semantic_artifact_default_with_scope, compile_semantic_artifact_with_scope, + compile_semantic_artifact_with_scope_and_replay_seed, stable_scope_hash, +}; pub use sim::{ - CallConv, CallInfo, FunctionSummary, SummaryEffect, SummaryInstallStats, SummaryRegistry, + CallConv, CallInfo, DerivedFunctionSummary, DerivedSummaryCase, DerivedSummaryCompletion, + DerivedSummaryDiagnostics, DerivedSummaryInput, DerivedSummarySet, FunctionSummary, + PreparedFunctionScope, ScopedPreparedFunction, SummaryEffect, SummaryInstallStats, + SummaryProfile, SummaryRegistry, +}; +pub use solver::{SatResult, SolverStats, SymModel, SymSolver}; +pub use spec::{ + AddressValue, BudgetSpec, ExplorationSpec, InputSpec, MergeSpec, PredicateSpec, RuntimeSpec, + StartSpec, StrategySpec, +}; +pub use state::{ + PendingExceptionContinuation, RuntimeBlockReason, RuntimeRegionAlias, RuntimeState, + RuntimeValueProvenance, SymState, SymbolicFdInput, SymbolicMemoryRegion, +}; +pub use tactics::{ + InputByteDomain, SolveTacticCandidate, SolveTacticConfig, TacticConstraintReport, + algebraic_preimage_candidate, algebraic_preimage_for_target, constrain_exact_fold_candidate, + constrain_exact_fold_inputs, constrain_exact_recurrence_candidate, + tactic_candidates_for_constraint_graph, }; -pub use solver::{SatResult, SymModel, SymSolver}; -pub use state::{SymState, SymbolicMemoryRegion}; pub use value::SymValue; +pub use verification::{ + CandidateReplayBackend, CandidateReplayRequest, EvidenceSummary, LiftedReplayBackend, + ModelValidation, SolveCandidateShape, SolveStatus, SolveTacticEvidence, SolveTacticKind, + SolveTacticStatus, SolveVerification, SolveVerificationRequest, SolveWitness, + UnavailableReplayBackend, VerificationRequirement, evidence_summary_for_route_and_stats, + solution_extraction_allowed, solve_tactics_for_exact_folds, + solve_tactics_for_exact_recurrences, verification_requirement_for_route_and_stats, + verify_solve_result, +}; /// Error types for symbolic execution. #[derive(Debug, thiserror::Error)] diff --git a/crates/r2sym/src/loops.rs b/crates/r2sym/src/loops.rs new file mode 100644 index 0000000..3d4ce91 --- /dev/null +++ b/crates/r2sym/src/loops.rs @@ -0,0 +1,2721 @@ +//! Runtime loop recognition and summary construction. +//! +//! This module owns loop-shape policy for symex. Path exploration executes +//! summaries, while verification decides whether summarized evidence can prove +//! a solve. + +use std::collections::{HashMap, HashSet}; + +use r2ssa::{FunctionSSABlock, SSAOp, SSAVar, SsaArtifact}; +use z3::Context; + +use crate::{SymState, SymValue}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LoopSummaryKind { + Exact, + BoundedExact, + Residual, + Refused, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LoopVarRole { + InductionCounter, + Accumulator, + Pointer, + MemoryEffect, + Unknown, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LoopCarriedVar { + pub name: String, + pub bits: u32, + pub role: LoopVarRole, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LoopTransitionExpr { + Identity(String), + AddConst { + var: String, + value: u64, + }, + AffineConst { + var: String, + multiplier: u64, + addend: u64, + }, + XorConst { + var: String, + value: u64, + }, + XorVar { + lhs: String, + rhs: String, + }, + AddVar { + lhs: String, + rhs: String, + }, + Load { + addr: String, + bytes: u32, + }, + TableRead(LoopMemoryTerm), + RotateMix { + var: String, + direction: LoopRotateDirection, + amount: u32, + operation: LoopFoldOperation, + term: LoopMemoryTerm, + }, + Store { + addr: String, + value: String, + }, + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LoopMemoryTermKind { + TableRead, + InputRead, + RuntimeBlobRead, + Unknown, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LoopMemoryTerm { + pub kind: LoopMemoryTermKind, + pub addr: String, + pub bytes: u32, + pub base: Option, + pub stride: Option, + pub region: Option, + pub region_base: Option, + pub region_size: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LoopRecurrenceKind { + Identity, + AddConst(u64), + SubConst(u64), + AffineConst { + multiplier: u64, + addend: u64, + }, + XorConst(u64), + AddMemoryFold(LoopMemoryTerm), + XorMemoryFold(LoopMemoryTerm), + RotateMix { + direction: LoopRotateDirection, + amount: u32, + operation: LoopFoldOperation, + term: LoopMemoryTerm, + }, + Unsupported(String), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LoopFoldOperation { + Add, + Xor, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LoopRotateDirection { + Left, + Right, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExactLoopRecurrenceKind { + AddConst(u64), + SubConst(u64), + AffineConst { + multiplier: u64, + addend: u64, + }, + XorConst(u64), + Fold { + operation: LoopFoldOperation, + term: LoopMemoryTerm, + }, + RotateMix { + direction: LoopRotateDirection, + amount: u32, + operation: LoopFoldOperation, + term: LoopMemoryTerm, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExactLoopRecurrenceEvidence { + pub header: u64, + pub exit_target: u64, + pub iterations: u64, + pub accumulator: String, + pub initial: String, + pub bits: u32, + pub kind: ExactLoopRecurrenceKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExactLoopFoldEvidence { + pub header: u64, + pub exit_target: u64, + pub iterations: u64, + pub accumulator: String, + pub bits: u32, + pub operation: LoopFoldOperation, + pub term: LoopMemoryTerm, +} + +impl ExactLoopRecurrenceEvidence { + pub fn as_fold(&self) -> Option { + let ExactLoopRecurrenceKind::Fold { operation, term } = &self.kind else { + return None; + }; + Some(ExactLoopFoldEvidence { + header: self.header, + exit_target: self.exit_target, + iterations: self.iterations, + accumulator: self.accumulator.clone(), + bits: self.bits, + operation: *operation, + term: term.clone(), + }) + } +} + +impl From for ExactLoopRecurrenceEvidence { + fn from(value: ExactLoopFoldEvidence) -> Self { + Self { + header: value.header, + exit_target: value.exit_target, + iterations: value.iterations, + accumulator: value.accumulator, + initial: String::new(), + bits: value.bits, + kind: ExactLoopRecurrenceKind::Fold { + operation: value.operation, + term: value.term, + }, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LoopRecurrence { + pub phi: String, + pub initial: String, + pub latch: String, + pub bits: u32, + pub role: LoopVarRole, + pub kind: LoopRecurrenceKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LoopTransitionSystem { + pub header: u64, + pub exit_target: u64, + pub iterations: u64, + pub counter: String, + pub counter_start: u64, + pub carried_state: Vec, + pub recurrences: Vec, + pub reasons: Vec, +} + +impl LoopTransitionSystem { + pub fn is_exact(&self) -> bool { + self.reasons.is_empty() + && self + .recurrences + .iter() + .all(|recurrence| !matches!(recurrence.kind, LoopRecurrenceKind::Unsupported(_))) + } +} + +#[derive(Debug)] +pub struct LoopSummary<'ctx> { + pub kind: LoopSummaryKind, + pub header: u64, + pub exit_target: Option, + pub iterations: Option, + pub carried_state: Vec, + pub transitions: Vec, + pub exact_recurrences: Vec, + pub exact_folds: Vec, + pub reasons: Vec, + pub resulting_state: Option>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RuntimeLoopBranch { + pub counter: SSAVar, + pub threshold: u64, + pub target: u64, +} + +pub fn bounded_exact_summary<'ctx>( + header: u64, + branch: &RuntimeLoopBranch, + iterations: u64, + resulting_state: SymState<'ctx>, +) -> LoopSummary<'ctx> { + LoopSummary { + kind: LoopSummaryKind::BoundedExact, + header, + exit_target: Some(branch.target), + iterations: Some(iterations), + carried_state: Vec::new(), + transitions: Vec::new(), + exact_recurrences: Vec::new(), + exact_folds: Vec::new(), + reasons: Vec::new(), + resulting_state: Some(resulting_state), + } +} + +pub fn summarize_residual_runtime_loop<'ctx>( + ctx: &'ctx Context, + block_func: &SsaArtifact, + block: &FunctionSSABlock, + state: &SymState<'ctx>, + branch: &RuntimeLoopBranch, + residual_enabled: bool, +) -> LoopSummary<'ctx> { + if state.pending_exception().is_none() { + return refused(block.addr, Some(branch.target), "no_pending_exception"); + } + if !residual_enabled { + return refused(block.addr, Some(branch.target), "residual_disabled"); + } + if state.runtime().runtime_regions.is_empty() { + return residual( + block.addr, + Some(branch.target), + "runtime_loop_missing_materialized_region", + ); + } + if block_func.get_block(branch.target).is_none() { + return residual( + block.addr, + Some(branch.target), + "runtime_loop_missing_exit_target", + ); + } + + let counter_bits = branch.counter.size.saturating_mul(8).max(1); + let counter_key = branch.counter.display_name(); + let Some(counter) = concrete_state_var_at_block_entry(state, block, &branch.counter) else { + return residual( + block.addr, + Some(branch.target), + "runtime_loop_unknown_counter", + ); + }; + if counter >= branch.threshold { + return refused( + block.addr, + Some(branch.target), + "runtime_loop_counter_past_threshold", + ); + } + let Some(iterations) = branch.threshold.checked_sub(counter) else { + return refused( + block.addr, + Some(branch.target), + "runtime_loop_counter_past_threshold", + ); + }; + if iterations < 8 { + return refused( + block.addr, + Some(branch.target), + "runtime_loop_too_small_to_summarize", + ); + } + + let transition_system = + derive_loop_transition_system(block, state, branch, counter, iterations); + if transition_system.is_exact() + && let Some(summary) = apply_exact_transition_system(ctx, state, &transition_system) + { + return summary; + } + + let carried_state = discover_loop_carried_state(block, Some(&branch.counter)); + if carried_state + .iter() + .all(|var| var.role == LoopVarRole::InductionCounter) + { + return residual( + block.addr, + Some(branch.target), + "runtime_loop_unknown_carried_state", + ); + } + if carried_state + .iter() + .any(|var| var.role == LoopVarRole::Unknown) + { + let mut summary = residual( + block.addr, + Some(branch.target), + "runtime_loop_unknown_carried_state", + ); + summary.carried_state = carried_state; + summary.transitions = discover_loop_transitions(block, &summary.carried_state); + return summary; + } + + let transitions = discover_loop_transitions(block, &carried_state); + let mut summarized = state.fork(); + summarized.set_register( + &counter_key, + SymValue::concrete(branch.threshold, counter_bits), + ); + for carried in &carried_state { + if carried.role == LoopVarRole::InductionCounter { + continue; + } + let summary_name = format!( + "runtime_loop_summary_{:x}_{:x}_{:x}_{}", + block.addr, counter, branch.threshold, carried.name + ); + summarized.set_register( + &carried.name, + SymValue::new_symbolic(ctx, &summary_name, carried.bits), + ); + } + summarized.pc = branch.target; + summarized.set_prev_pc(Some(block.addr)); + + LoopSummary { + kind: LoopSummaryKind::Residual, + header: block.addr, + exit_target: Some(branch.target), + iterations: Some(iterations), + carried_state, + transitions, + exact_recurrences: Vec::new(), + exact_folds: Vec::new(), + reasons: vec!["runtime_loop_summary_residual".to_string()], + resulting_state: Some(summarized), + } +} + +pub fn derive_loop_transition_system<'ctx>( + block: &FunctionSSABlock, + state: &SymState<'ctx>, + branch: &RuntimeLoopBranch, + counter: u64, + iterations: u64, +) -> LoopTransitionSystem { + let carried_state = discover_loop_carried_state(block, Some(&branch.counter)); + let mut reasons = Vec::new(); + let mut recurrences = Vec::new(); + + if block_has_memory_writes(block) { + push_unique_reason( + &mut reasons, + "runtime_loop_memory_write_transition_unsupported", + ); + } + + if state_value_at_block_entry(state, block, &branch.counter).is_none() { + push_unique_reason(&mut reasons, "runtime_loop_unknown_counter"); + } + + if counter.saturating_add(iterations) != branch.threshold { + push_unique_reason(&mut reasons, "runtime_loop_non_affine_exit_count"); + } + + for carried in &carried_state { + let Some(phi) = block + .phis + .iter() + .find(|phi| phi.dst.display_name() == carried.name) + else { + push_unique_reason( + &mut reasons, + format!("runtime_loop_missing_phi:{}", carried.name), + ); + continue; + }; + let Some(initial_source) = selected_phi_source(phi, state.prev_pc()) else { + push_unique_reason( + &mut reasons, + format!("runtime_loop_missing_initial_source:{}", carried.name), + ); + continue; + }; + let Some(latch_source) = latch_source_for_phi(phi, state.prev_pc()) else { + push_unique_reason( + &mut reasons, + format!("runtime_loop_missing_latch_source:{}", carried.name), + ); + continue; + }; + if state_value_at_block_entry(state, block, &phi.dst).is_none() { + push_unique_reason( + &mut reasons, + format!("runtime_loop_unknown_initial:{}", carried.name), + ); + continue; + } + let recurrence = recurrence_for_latch(block, state, &phi.dst, latch_source, carried.role); + let recurrence = LoopRecurrence { + initial: initial_source.display_name(), + ..recurrence + }; + if matches!(recurrence.kind, LoopRecurrenceKind::Unsupported(_)) { + push_unique_reason( + &mut reasons, + format!("runtime_loop_unsupported_recurrence:{}", carried.name), + ); + } + if let Some(term) = recurrence_memory_term(&recurrence.kind) { + if term.base.is_none() || term.stride.is_none() { + push_unique_reason( + &mut reasons, + format!("runtime_loop_unknown_memory_term:{}", carried.name), + ); + } + if term.kind == LoopMemoryTermKind::Unknown { + push_unique_reason( + &mut reasons, + format!("runtime_loop_unknown_memory_provenance:{}", carried.name), + ); + } + if !memory_term_has_exact_bounds(state, term, iterations) { + push_unique_reason( + &mut reasons, + format!("runtime_loop_memory_bounds_unknown:{}", carried.name), + ); + } + } + recurrences.push(recurrence); + } + + recurrences.sort_by(|lhs, rhs| lhs.phi.cmp(&rhs.phi)); + LoopTransitionSystem { + header: block.addr, + exit_target: branch.target, + iterations, + counter: branch.counter.display_name(), + counter_start: counter, + carried_state, + recurrences, + reasons, + } +} + +pub fn runtime_counter_threshold_branch(block: &FunctionSSABlock) -> Option { + let mut copies: HashMap = HashMap::new(); + let mut unsigned_less_defs: HashMap = HashMap::new(); + let mut inverted_less_defs: HashMap = HashMap::new(); + + for op in &block.ops { + match op { + SSAOp::Copy { dst, src } => { + copies.insert(dst.display_name(), resolve_copy_source(src, &copies)); + } + SSAOp::IntLess { dst, a, b } => { + if let Some(threshold) = parse_const_var(b) { + unsigned_less_defs.insert( + dst.display_name(), + (resolve_copy_source(a, &copies), threshold), + ); + } + } + SSAOp::BoolNot { dst, src } => { + if let Some((counter, threshold)) = unsigned_less_defs.get(&src.display_name()) { + inverted_less_defs.insert(dst.display_name(), (counter.clone(), *threshold)); + } + } + SSAOp::CBranch { target, cond } => { + let target = parse_address_var(target)?; + if target <= block.addr.saturating_add(block.size as u64) { + return None; + } + let (counter, threshold) = inverted_less_defs.get(&cond.display_name())?; + return Some(RuntimeLoopBranch { + counter: counter.clone(), + threshold: *threshold, + target, + }); + } + _ => {} + } + } + + None +} + +pub fn exact_recurrence_evidence_from_system( + system: &LoopTransitionSystem, +) -> Vec { + if !system.is_exact() { + return Vec::new(); + } + let mut evidence = system + .recurrences + .iter() + .filter_map(|recurrence| match &recurrence.kind { + LoopRecurrenceKind::AddConst(value) => Some(ExactLoopRecurrenceEvidence { + header: system.header, + exit_target: system.exit_target, + iterations: system.iterations, + accumulator: recurrence.phi.clone(), + initial: recurrence.initial.clone(), + bits: recurrence.bits, + kind: ExactLoopRecurrenceKind::AddConst(*value), + }), + LoopRecurrenceKind::SubConst(value) => Some(ExactLoopRecurrenceEvidence { + header: system.header, + exit_target: system.exit_target, + iterations: system.iterations, + accumulator: recurrence.phi.clone(), + initial: recurrence.initial.clone(), + bits: recurrence.bits, + kind: ExactLoopRecurrenceKind::SubConst(*value), + }), + LoopRecurrenceKind::AffineConst { multiplier, addend } => { + Some(ExactLoopRecurrenceEvidence { + header: system.header, + exit_target: system.exit_target, + iterations: system.iterations, + accumulator: recurrence.phi.clone(), + initial: recurrence.initial.clone(), + bits: recurrence.bits, + kind: ExactLoopRecurrenceKind::AffineConst { + multiplier: *multiplier, + addend: *addend, + }, + }) + } + LoopRecurrenceKind::XorConst(value) => Some(ExactLoopRecurrenceEvidence { + header: system.header, + exit_target: system.exit_target, + iterations: system.iterations, + accumulator: recurrence.phi.clone(), + initial: recurrence.initial.clone(), + bits: recurrence.bits, + kind: ExactLoopRecurrenceKind::XorConst(*value), + }), + LoopRecurrenceKind::AddMemoryFold(term) => Some(ExactLoopRecurrenceEvidence { + header: system.header, + exit_target: system.exit_target, + iterations: system.iterations, + accumulator: recurrence.phi.clone(), + initial: recurrence.initial.clone(), + bits: recurrence.bits, + kind: ExactLoopRecurrenceKind::Fold { + operation: LoopFoldOperation::Add, + term: term.clone(), + }, + }), + LoopRecurrenceKind::XorMemoryFold(term) => Some(ExactLoopRecurrenceEvidence { + header: system.header, + exit_target: system.exit_target, + iterations: system.iterations, + accumulator: recurrence.phi.clone(), + initial: recurrence.initial.clone(), + bits: recurrence.bits, + kind: ExactLoopRecurrenceKind::Fold { + operation: LoopFoldOperation::Xor, + term: term.clone(), + }, + }), + LoopRecurrenceKind::RotateMix { + direction, + amount, + operation, + term, + } => Some(ExactLoopRecurrenceEvidence { + header: system.header, + exit_target: system.exit_target, + iterations: system.iterations, + accumulator: recurrence.phi.clone(), + initial: recurrence.initial.clone(), + bits: recurrence.bits, + kind: ExactLoopRecurrenceKind::RotateMix { + direction: *direction, + amount: *amount, + operation: *operation, + term: term.clone(), + }, + }), + _ => None, + }) + .collect::>(); + evidence.sort_by(|lhs, rhs| { + ( + lhs.header, + lhs.exit_target, + lhs.accumulator.as_str(), + recurrence_sort_key(&lhs.kind), + ) + .cmp(&( + rhs.header, + rhs.exit_target, + rhs.accumulator.as_str(), + recurrence_sort_key(&rhs.kind), + )) + }); + evidence +} + +fn recurrence_sort_key(kind: &ExactLoopRecurrenceKind) -> String { + match kind { + ExactLoopRecurrenceKind::AddConst(value) => format!("add_const:{value:x}"), + ExactLoopRecurrenceKind::SubConst(value) => format!("sub_const:{value:x}"), + ExactLoopRecurrenceKind::AffineConst { multiplier, addend } => { + format!("affine:{multiplier:x}:{addend:x}") + } + ExactLoopRecurrenceKind::XorConst(value) => format!("xor_const:{value:x}"), + ExactLoopRecurrenceKind::Fold { operation, term } => { + format!("fold:{operation:?}:{}", term.addr) + } + ExactLoopRecurrenceKind::RotateMix { + direction, + amount, + operation, + term, + } => format!( + "rotate_mix:{direction:?}:{amount}:{operation:?}:{}", + term.addr + ), + } +} + +fn recurrence_memory_term(kind: &LoopRecurrenceKind) -> Option<&LoopMemoryTerm> { + match kind { + LoopRecurrenceKind::AddMemoryFold(term) + | LoopRecurrenceKind::XorMemoryFold(term) + | LoopRecurrenceKind::RotateMix { term, .. } => Some(term), + _ => None, + } +} + +pub fn exact_fold_evidence_from_recurrences( + recurrences: &[ExactLoopRecurrenceEvidence], +) -> Vec { + let mut folds = recurrences + .iter() + .filter_map(ExactLoopRecurrenceEvidence::as_fold) + .collect::>(); + folds.sort_by(|lhs, rhs| { + ( + lhs.header, + lhs.exit_target, + lhs.accumulator.as_str(), + lhs.term.addr.as_str(), + ) + .cmp(&( + rhs.header, + rhs.exit_target, + rhs.accumulator.as_str(), + rhs.term.addr.as_str(), + )) + }); + folds +} + +pub fn exact_fold_evidence_from_system( + system: &LoopTransitionSystem, +) -> Vec { + exact_fold_evidence_from_recurrences(&exact_recurrence_evidence_from_system(system)) +} + +pub fn discover_loop_carried_state( + block: &FunctionSSABlock, + counter: Option<&SSAVar>, +) -> Vec { + let mut vars = Vec::new(); + let counter_name = counter.map(SSAVar::display_name); + for phi in &block.phis { + let phi_name = phi.dst.display_name(); + let latch_sources = phi + .sources + .iter() + .map(|(_, source)| source) + .filter(|source| source.display_name() != phi_name) + .collect::>(); + if latch_sources.is_empty() { + continue; + } + let role = if counter_name.as_deref() == Some(phi_name.as_str()) + || latch_sources + .iter() + .any(|source| counter_name.as_deref() == Some(source.display_name().as_str())) + { + LoopVarRole::InductionCounter + } else if used_as_memory_value(block, &phi.dst, &latch_sources) { + LoopVarRole::MemoryEffect + } else if used_as_memory_addr(block, &phi.dst, &latch_sources) { + LoopVarRole::Pointer + } else if latch_update_depends_on_phi(block, &phi.dst, &latch_sources) { + LoopVarRole::Accumulator + } else { + LoopVarRole::Unknown + }; + vars.push(LoopCarriedVar { + name: phi_name, + bits: phi.dst.size.saturating_mul(8).max(1), + role, + }); + } + vars.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); + vars +} + +pub fn concrete_state_var_at_block_entry<'ctx>( + state: &SymState<'ctx>, + block: &FunctionSSABlock, + var: &SSAVar, +) -> Option { + let phi = block + .phis + .iter() + .find(|phi| phi.dst.display_name() == var.display_name()); + if let Some(phi) = phi { + let prev_pc = state.prev_pc()?; + return phi + .sources + .iter() + .find(|(pred, _)| *pred == prev_pc) + .and_then(|(_, source)| concrete_state_var(state, source)); + } + + concrete_state_var(state, var) +} + +fn refused<'ctx>(header: u64, exit_target: Option, reason: &str) -> LoopSummary<'ctx> { + LoopSummary { + kind: LoopSummaryKind::Refused, + header, + exit_target, + iterations: None, + carried_state: Vec::new(), + transitions: Vec::new(), + exact_recurrences: Vec::new(), + exact_folds: Vec::new(), + reasons: vec![reason.to_string()], + resulting_state: None, + } +} + +fn residual<'ctx>(header: u64, exit_target: Option, reason: &str) -> LoopSummary<'ctx> { + LoopSummary { + kind: LoopSummaryKind::Residual, + header, + exit_target, + iterations: None, + carried_state: Vec::new(), + transitions: Vec::new(), + exact_recurrences: Vec::new(), + exact_folds: Vec::new(), + reasons: vec![reason.to_string()], + resulting_state: None, + } +} + +fn apply_exact_transition_system<'ctx>( + ctx: &'ctx Context, + state: &SymState<'ctx>, + system: &LoopTransitionSystem, +) -> Option> { + let mut summarized = state.fork(); + let counter_bits = state.get_register_sized(&system.counter, 64).bits().max(1); + summarized.set_register( + &system.counter, + SymValue::concrete( + system.iterations.saturating_add(system.counter_start), + counter_bits, + ), + ); + + for recurrence in &system.recurrences { + let initial = value_for_name(state, &recurrence.initial, recurrence.bits)?; + let value = apply_recurrence(ctx, state, initial, recurrence, system.iterations)?; + summarized.set_register(&recurrence.phi, value); + summarized.set_register( + &recurrence.latch, + summarized.get_register_sized(&recurrence.phi, recurrence.bits), + ); + } + summarized.pc = system.exit_target; + summarized.set_prev_pc(Some(system.header)); + let exact_recurrences = exact_recurrence_evidence_from_system(system); + let exact_folds = exact_fold_evidence_from_recurrences(&exact_recurrences); + + Some(LoopSummary { + kind: LoopSummaryKind::Exact, + header: system.header, + exit_target: Some(system.exit_target), + iterations: Some(system.iterations), + carried_state: system.carried_state.clone(), + transitions: system + .recurrences + .iter() + .map(recurrence_to_transition) + .collect(), + exact_recurrences, + exact_folds, + reasons: Vec::new(), + resulting_state: Some(summarized), + }) +} + +fn apply_recurrence<'ctx>( + ctx: &'ctx Context, + state: &SymState<'ctx>, + initial: SymValue<'ctx>, + recurrence: &LoopRecurrence, + iterations: u64, +) -> Option> { + let bits = recurrence.bits.max(1); + match recurrence.kind { + LoopRecurrenceKind::Identity => Some(initial), + LoopRecurrenceKind::AddConst(value) => { + let delta = mul_mod_width(value, iterations, bits); + Some(initial.add(ctx, &SymValue::concrete(delta, bits))) + } + LoopRecurrenceKind::SubConst(value) => { + let delta = mul_mod_width(value, iterations, bits); + Some(initial.sub(ctx, &SymValue::concrete(delta, bits))) + } + LoopRecurrenceKind::AffineConst { multiplier, addend } => { + let (scale, bias) = affine_transform_pow(multiplier, addend, iterations, bits); + let scaled = initial.mul(ctx, &SymValue::concrete(scale, bits)); + Some(if bias == 0 { + scaled + } else { + scaled.add(ctx, &SymValue::concrete(bias, bits)) + }) + } + LoopRecurrenceKind::XorConst(value) => { + if iterations.is_multiple_of(2) { + Some(initial) + } else { + Some(initial.xor(ctx, &SymValue::concrete(value, bits))) + } + } + LoopRecurrenceKind::AddMemoryFold(ref term) => { + let folded = fold_memory_term(ctx, state, term, iterations, bits, true)?; + Some(initial.add(ctx, &folded)) + } + LoopRecurrenceKind::XorMemoryFold(ref term) => { + let folded = fold_memory_term(ctx, state, term, iterations, bits, false)?; + Some(initial.xor(ctx, &folded)) + } + LoopRecurrenceKind::RotateMix { + direction, + amount, + operation, + ref term, + } => apply_rotate_mix_recurrence( + ctx, + state, + initial, + term, + iterations, + bits, + RotateMixSpec { + direction, + amount, + operation, + }, + ), + LoopRecurrenceKind::Unsupported(_) => None, + } +} + +fn recurrence_to_transition(recurrence: &LoopRecurrence) -> LoopTransitionExpr { + match recurrence.kind { + LoopRecurrenceKind::Identity => LoopTransitionExpr::Identity(recurrence.phi.clone()), + LoopRecurrenceKind::AddConst(value) => LoopTransitionExpr::AddConst { + var: recurrence.phi.clone(), + value, + }, + LoopRecurrenceKind::SubConst(value) => LoopTransitionExpr::AddConst { + var: recurrence.phi.clone(), + value: value.wrapping_neg(), + }, + LoopRecurrenceKind::AffineConst { multiplier, addend } => LoopTransitionExpr::AffineConst { + var: recurrence.phi.clone(), + multiplier, + addend, + }, + LoopRecurrenceKind::XorConst(value) => LoopTransitionExpr::XorConst { + var: recurrence.phi.clone(), + value, + }, + LoopRecurrenceKind::AddMemoryFold(ref term) + | LoopRecurrenceKind::XorMemoryFold(ref term) => { + LoopTransitionExpr::TableRead(term.clone()) + } + LoopRecurrenceKind::RotateMix { + direction, + amount, + operation, + ref term, + } => LoopTransitionExpr::RotateMix { + var: recurrence.phi.clone(), + direction, + amount, + operation, + term: term.clone(), + }, + LoopRecurrenceKind::Unsupported(_) => LoopTransitionExpr::Unknown, + } +} + +fn discover_loop_transitions( + block: &FunctionSSABlock, + carried_state: &[LoopCarriedVar], +) -> Vec { + let carried = carried_state + .iter() + .map(|var| var.name.as_str()) + .collect::>(); + let mut transitions = Vec::new(); + for op in &block.ops { + match op { + SSAOp::IntAdd { dst, a, b } if carried.contains(dst.display_name().as_str()) => { + transitions.push(binary_transition( + LoopTransitionExpr::AddVar { + lhs: a.display_name(), + rhs: b.display_name(), + }, + a, + b, + |var, value| LoopTransitionExpr::AddConst { var, value }, + )); + } + SSAOp::IntXor { dst, a, b } if carried.contains(dst.display_name().as_str()) => { + transitions.push(binary_transition( + LoopTransitionExpr::XorVar { + lhs: a.display_name(), + rhs: b.display_name(), + }, + a, + b, + |var, value| LoopTransitionExpr::XorConst { var, value }, + )); + } + SSAOp::Load { dst, addr, .. } if carried.contains(dst.display_name().as_str()) => { + transitions.push(LoopTransitionExpr::Load { + addr: addr.display_name(), + bytes: dst.size, + }); + } + SSAOp::Store { addr, val, .. } => { + transitions.push(LoopTransitionExpr::Store { + addr: addr.display_name(), + value: val.display_name(), + }); + } + _ => {} + } + } + if transitions.is_empty() && !carried_state.is_empty() { + transitions.push(LoopTransitionExpr::Unknown); + } + transitions +} + +fn binary_transition( + fallback: LoopTransitionExpr, + a: &SSAVar, + b: &SSAVar, + make_const: F, +) -> LoopTransitionExpr +where + F: Fn(String, u64) -> LoopTransitionExpr, +{ + if let Some(value) = parse_const_var(a) { + make_const(b.display_name(), value) + } else if let Some(value) = parse_const_var(b) { + make_const(a.display_name(), value) + } else { + fallback + } +} + +fn recurrence_for_latch( + block: &FunctionSSABlock, + state: &SymState<'_>, + phi: &SSAVar, + latch: &SSAVar, + role: LoopVarRole, +) -> LoopRecurrence { + let phi_name = phi.display_name(); + let latch_name = latch.display_name(); + let bits = phi.size.saturating_mul(8).max(1); + let kind = if phi_name == latch_name { + LoopRecurrenceKind::Identity + } else { + block + .ops + .iter() + .find(|op| op.dst().is_some_and(|dst| dst.display_name() == latch_name)) + .map(|op| recurrence_kind_for_op(block, state, op, &phi_name)) + .unwrap_or_else(|| { + LoopRecurrenceKind::Unsupported("missing_latch_definition".to_string()) + }) + }; + + LoopRecurrence { + phi: phi_name, + initial: String::new(), + latch: latch_name, + bits, + role, + kind, + } +} + +fn recurrence_kind_for_op( + block: &FunctionSSABlock, + state: &SymState<'_>, + op: &SSAOp, + phi_name: &str, +) -> LoopRecurrenceKind { + let bits = op + .dst() + .map(|dst| dst.size.saturating_mul(8).max(1)) + .unwrap_or(64); + if let Some(kind) = affine_recurrence_kind_for_op(block, op, phi_name, bits) { + return kind; + } + match op { + SSAOp::Copy { src, .. } if src.display_name() == phi_name => LoopRecurrenceKind::Identity, + SSAOp::IntAdd { a, b, .. } => rotate_mix_recurrence_kind_for_op( + block, + state, + a, + b, + phi_name, + bits, + LoopFoldOperation::Add, + ) + .unwrap_or_else(|| recurrence_memory_or_const_fold(block, state, a, b, phi_name, true)), + SSAOp::IntSub { a, b, .. } if a.display_name() == phi_name => parse_const_var(b) + .map(LoopRecurrenceKind::SubConst) + .unwrap_or_else(|| LoopRecurrenceKind::Unsupported("sub_non_const".to_string())), + SSAOp::IntXor { a, b, .. } => rotate_mix_recurrence_kind_for_op( + block, + state, + a, + b, + phi_name, + bits, + LoopFoldOperation::Xor, + ) + .unwrap_or_else(|| recurrence_memory_or_const_fold(block, state, a, b, phi_name, false)), + _ => LoopRecurrenceKind::Unsupported("unsupported_latch_op".to_string()), + } +} + +fn affine_recurrence_kind_for_op( + block: &FunctionSSABlock, + op: &SSAOp, + phi_name: &str, + bits: u32, +) -> Option { + let mut visited = HashSet::new(); + let (multiplier, addend) = + parse_affine_recurrence_op(block, op, phi_name, bits, 8, &mut visited)?; + Some(affine_kind_from_parts(multiplier, addend, bits)) +} + +fn parse_affine_recurrence_op( + block: &FunctionSSABlock, + op: &SSAOp, + phi_name: &str, + bits: u32, + depth: u8, + visited: &mut HashSet, +) -> Option<(u64, u64)> { + if depth == 0 { + return None; + } + match op { + SSAOp::Copy { src, .. } => { + parse_affine_recurrence_var(block, src, phi_name, bits, depth - 1, visited) + } + SSAOp::IntAdd { a, b, .. } => { + let lhs = parse_affine_recurrence_var(block, a, phi_name, bits, depth - 1, visited)?; + let rhs = parse_affine_recurrence_var(block, b, phi_name, bits, depth - 1, visited)?; + Some(affine_add_parts(lhs, rhs, bits)) + } + SSAOp::IntSub { a, b, .. } => { + let lhs = parse_affine_recurrence_var(block, a, phi_name, bits, depth - 1, visited)?; + let rhs = parse_affine_recurrence_var(block, b, phi_name, bits, depth - 1, visited)?; + Some(affine_sub_parts(lhs, rhs, bits)) + } + SSAOp::IntMult { a, b, .. } => { + if let Some(scale) = parse_const_var(a) { + let rhs = + parse_affine_recurrence_var(block, b, phi_name, bits, depth - 1, visited)?; + return Some(affine_scale_parts(rhs, scale, bits)); + } + if let Some(scale) = parse_const_var(b) { + let lhs = + parse_affine_recurrence_var(block, a, phi_name, bits, depth - 1, visited)?; + return Some(affine_scale_parts(lhs, scale, bits)); + } + None + } + _ => None, + } +} + +fn parse_affine_recurrence_var( + block: &FunctionSSABlock, + value: &SSAVar, + phi_name: &str, + bits: u32, + depth: u8, + visited: &mut HashSet, +) -> Option<(u64, u64)> { + if depth == 0 { + return None; + } + if value.display_name() == phi_name { + return Some((1, 0)); + } + if let Some(constant) = parse_const_var(value) { + return Some((0, constant & mask_for_bits(bits))); + } + + let name = value.display_name(); + if !visited.insert(name.clone()) { + return None; + } + let result = block + .ops + .iter() + .find(|op| op.dst().is_some_and(|dst| dst.display_name() == name)) + .and_then(|op| parse_affine_recurrence_op(block, op, phi_name, bits, depth - 1, visited)); + visited.remove(&name); + result +} + +fn affine_kind_from_parts(multiplier: u64, addend: u64, bits: u32) -> LoopRecurrenceKind { + let mask = mask_for_bits(bits); + let multiplier = multiplier & mask; + let addend = addend & mask; + if multiplier == 1 { + if addend == 0 { + LoopRecurrenceKind::Identity + } else { + let subtraction = addend.wrapping_neg() & mask; + if subtraction != 0 && subtraction < addend { + LoopRecurrenceKind::SubConst(subtraction) + } else { + LoopRecurrenceKind::AddConst(addend) + } + } + } else { + LoopRecurrenceKind::AffineConst { multiplier, addend } + } +} + +fn affine_add_parts(lhs: (u64, u64), rhs: (u64, u64), bits: u32) -> (u64, u64) { + let mask = mask_for_bits(bits); + ( + lhs.0.wrapping_add(rhs.0) & mask, + lhs.1.wrapping_add(rhs.1) & mask, + ) +} + +fn affine_sub_parts(lhs: (u64, u64), rhs: (u64, u64), bits: u32) -> (u64, u64) { + let mask = mask_for_bits(bits); + ( + lhs.0.wrapping_sub(rhs.0) & mask, + lhs.1.wrapping_sub(rhs.1) & mask, + ) +} + +fn affine_scale_parts(parts: (u64, u64), scale: u64, bits: u32) -> (u64, u64) { + let mask = mask_for_bits(bits); + ( + parts.0.wrapping_mul(scale) & mask, + parts.1.wrapping_mul(scale) & mask, + ) +} + +fn rotate_mix_recurrence_kind_for_op( + block: &FunctionSSABlock, + state: &SymState<'_>, + a: &SSAVar, + b: &SSAVar, + phi_name: &str, + bits: u32, + operation: LoopFoldOperation, +) -> Option { + rotate_mix_kind_from_operands(block, state, a, b, phi_name, bits, operation) + .or_else(|| rotate_mix_kind_from_operands(block, state, b, a, phi_name, bits, operation)) +} + +fn rotate_mix_kind_from_operands( + block: &FunctionSSABlock, + state: &SymState<'_>, + rotate_operand: &SSAVar, + mix_operand: &SSAVar, + phi_name: &str, + bits: u32, + operation: LoopFoldOperation, +) -> Option { + let mut visited = HashSet::new(); + let (direction, amount) = + parse_rotate_source_for_var(block, rotate_operand, phi_name, bits, 8, &mut visited)?; + let term = memory_term_for_var(block, state, mix_operand)?; + Some(LoopRecurrenceKind::RotateMix { + direction, + amount, + operation, + term, + }) +} + +fn parse_rotate_source_for_var( + block: &FunctionSSABlock, + value: &SSAVar, + phi_name: &str, + bits: u32, + depth: u8, + visited: &mut HashSet, +) -> Option<(LoopRotateDirection, u32)> { + if depth == 0 { + return None; + } + let name = value.display_name(); + let op = lookup_def_op_by_name(block, &name)?; + if !visited.insert(name.clone()) { + return None; + } + let result = match op { + SSAOp::Copy { src, .. } => { + parse_rotate_source_for_var(block, src, phi_name, bits, depth - 1, visited) + } + SSAOp::IntOr { dst, a, b } if dst.size.saturating_mul(8).max(1) == bits => { + parse_rotate_or_term(block, a, b, phi_name, bits, depth - 1) + } + _ => None, + }; + visited.remove(&name); + result +} + +fn parse_rotate_or_term( + block: &FunctionSSABlock, + a: &SSAVar, + b: &SSAVar, + phi_name: &str, + bits: u32, + depth: u8, +) -> Option<(LoopRotateDirection, u32)> { + let lhs = parse_rotate_shift_term(block, a, phi_name, bits, depth)?; + let rhs = parse_rotate_shift_term(block, b, phi_name, bits, depth)?; + if lhs.shift == 0 || rhs.shift == 0 || lhs.shift.saturating_add(rhs.shift) != bits { + return None; + } + match (lhs.direction, rhs.direction) { + (LoopRotateDirection::Left, LoopRotateDirection::Right) => { + Some((LoopRotateDirection::Left, lhs.shift)) + } + (LoopRotateDirection::Right, LoopRotateDirection::Left) => { + Some((LoopRotateDirection::Right, lhs.shift)) + } + _ => None, + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct RotateShiftTerm { + direction: LoopRotateDirection, + shift: u32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct RotateMixSpec { + direction: LoopRotateDirection, + amount: u32, + operation: LoopFoldOperation, +} + +fn parse_rotate_shift_term( + block: &FunctionSSABlock, + value: &SSAVar, + phi_name: &str, + bits: u32, + depth: u8, +) -> Option { + if depth == 0 { + return None; + } + let op = lookup_def_op_by_name(block, &value.display_name())?; + match op { + SSAOp::IntLeft { dst, a, b } if dst.size.saturating_mul(8).max(1) == bits => { + let shift = normalize_rotate_amount(parse_const_var(b)? as u32, bits); + let mut inner_visited = HashSet::new(); + value_matches_phi_source(block, a, phi_name, depth - 1, &mut inner_visited).then_some( + RotateShiftTerm { + direction: LoopRotateDirection::Left, + shift, + }, + ) + } + SSAOp::IntRight { dst, a, b } if dst.size.saturating_mul(8).max(1) == bits => { + let shift = normalize_rotate_amount(parse_const_var(b)? as u32, bits); + let mut inner_visited = HashSet::new(); + value_matches_phi_source(block, a, phi_name, depth - 1, &mut inner_visited).then_some( + RotateShiftTerm { + direction: LoopRotateDirection::Right, + shift, + }, + ) + } + SSAOp::Copy { src, .. } => parse_rotate_shift_term(block, src, phi_name, bits, depth - 1), + _ => None, + } +} + +fn value_matches_phi_source( + block: &FunctionSSABlock, + value: &SSAVar, + phi_name: &str, + depth: u8, + visited: &mut HashSet, +) -> bool { + if depth == 0 { + return false; + } + if value.display_name() == phi_name { + return true; + } + let name = value.display_name(); + let Some(op) = lookup_def_op_by_name(block, &name) else { + return false; + }; + if !visited.insert(name.clone()) { + return false; + } + let result = match op { + SSAOp::Copy { src, .. } => { + value_matches_phi_source(block, src, phi_name, depth - 1, visited) + } + _ => false, + }; + visited.remove(&name); + result +} + +fn lookup_def_op_by_name<'a>(block: &'a FunctionSSABlock, name: &str) -> Option<&'a SSAOp> { + block + .ops + .iter() + .find(|op| op.dst().is_some_and(|dst| dst.display_name() == name)) +} + +fn recurrence_memory_or_const_fold( + block: &FunctionSSABlock, + state: &SymState<'_>, + a: &SSAVar, + b: &SSAVar, + phi_name: &str, + additive: bool, +) -> LoopRecurrenceKind { + let other = if a.display_name() == phi_name { + b + } else if b.display_name() == phi_name { + a + } else { + return LoopRecurrenceKind::Unsupported("fold_missing_accumulator".to_string()); + }; + + if let Some(value) = parse_const_var(other) { + return if additive { + LoopRecurrenceKind::AddConst(value) + } else { + LoopRecurrenceKind::XorConst(value) + }; + } + + let Some(term) = memory_term_for_var(block, state, other) else { + return LoopRecurrenceKind::Unsupported("fold_operand_not_memory_term".to_string()); + }; + if additive { + LoopRecurrenceKind::AddMemoryFold(term) + } else { + LoopRecurrenceKind::XorMemoryFold(term) + } +} + +fn memory_term_for_var( + block: &FunctionSSABlock, + state: &SymState<'_>, + value: &SSAVar, +) -> Option { + let (addr, bytes) = block.ops.iter().find_map(|op| match op { + SSAOp::Load { dst, addr, .. } if dst.display_name() == value.display_name() => { + Some((addr, dst.size)) + } + _ => None, + })?; + let base = state_value_at_block_entry(state, block, addr).and_then(|value| value.as_concrete()); + let addr_is_loop_carried = block + .phis + .iter() + .any(|phi| phi.dst.display_name() == addr.display_name()); + let stride = if addr_is_loop_carried { + memory_stride_for_addr_var(block, state, addr) + } else { + Some(bytes as u64) + }; + let provenance = base.and_then(|base| classify_memory_term_provenance(state, base)); + Some(LoopMemoryTerm { + kind: provenance + .as_ref() + .map(|provenance| provenance.kind) + .unwrap_or(LoopMemoryTermKind::Unknown), + addr: addr.display_name(), + bytes, + base, + stride, + region: provenance + .as_ref() + .map(|provenance| provenance.name.clone()), + region_base: provenance.as_ref().map(|provenance| provenance.base), + region_size: provenance.as_ref().map(|provenance| provenance.size), + }) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct LoopMemoryProvenance { + kind: LoopMemoryTermKind, + name: String, + base: u64, + size: u64, +} + +fn classify_memory_term_provenance( + state: &SymState<'_>, + base: u64, +) -> Option { + if let Some(region) = state + .symbolic_memory() + .iter() + .find(|region| base >= region.addr && base < region.addr.saturating_add(region.size as u64)) + { + let kind = if region.name.contains("argv") + || region.name.contains("stdin") + || region.name.contains("input") + { + LoopMemoryTermKind::InputRead + } else { + LoopMemoryTermKind::TableRead + }; + return Some(LoopMemoryProvenance { + kind, + name: region.name.clone(), + base: region.addr, + size: region.size as u64, + }); + }; + + if let Some(region) = state.runtime_region_for_pc(base) { + return Some(LoopMemoryProvenance { + kind: LoopMemoryTermKind::RuntimeBlobRead, + name: format!("runtime:{:x}", region.runtime_base), + base: region.runtime_base, + size: region.size, + }); + } + + if state.is_concrete_memory_range(base, 1) { + return Some(LoopMemoryProvenance { + kind: LoopMemoryTermKind::TableRead, + name: format!("concrete:{base:x}"), + base, + size: 0, + }); + } + + None +} + +fn memory_stride_for_addr_var( + block: &FunctionSSABlock, + state: &SymState<'_>, + addr: &SSAVar, +) -> Option { + let phi = block + .phis + .iter() + .find(|phi| phi.dst.display_name() == addr.display_name())?; + let latch = latch_source_for_phi(phi, state.prev_pc())?; + let latch_name = latch.display_name(); + block + .ops + .iter() + .find(|op| op.dst().is_some_and(|dst| dst.display_name() == latch_name)) + .and_then(|op| match op { + SSAOp::IntAdd { a, b, .. } if a.display_name() == addr.display_name() => { + parse_const_var(b) + } + SSAOp::IntAdd { a, b, .. } if b.display_name() == addr.display_name() => { + parse_const_var(a) + } + _ => None, + }) +} + +fn memory_term_has_exact_bounds( + state: &SymState<'_>, + term: &LoopMemoryTerm, + iterations: u64, +) -> bool { + let (Some(base), Some(stride)) = (term.base, term.stride) else { + return false; + }; + if iterations == 0 { + return true; + } + let Some(last_offset) = (iterations - 1).checked_mul(stride) else { + return false; + }; + let Some(last_addr) = base.checked_add(last_offset) else { + return false; + }; + let Some(last_end) = last_addr.checked_add(term.bytes as u64) else { + return false; + }; + + match term.kind { + LoopMemoryTermKind::InputRead | LoopMemoryTermKind::RuntimeBlobRead => { + let (Some(region_base), Some(region_size)) = (term.region_base, term.region_size) + else { + return false; + }; + let Some(region_end) = region_base.checked_add(region_size) else { + return false; + }; + base >= region_base && last_end <= region_end + } + LoopMemoryTermKind::TableRead => (0..iterations).all(|iteration| { + let Some(offset) = iteration.checked_mul(stride) else { + return false; + }; + let Some(addr) = base.checked_add(offset) else { + return false; + }; + state.is_concrete_memory_range(addr, term.bytes) + }), + LoopMemoryTermKind::Unknown => false, + } +} + +fn selected_phi_source(phi: &r2ssa::PhiNode, prev_pc: Option) -> Option<&SSAVar> { + let prev_pc = prev_pc?; + phi.sources + .iter() + .find(|(pred, _)| *pred == prev_pc) + .map(|(_, source)| source) +} + +fn latch_source_for_phi(phi: &r2ssa::PhiNode, selected_prev_pc: Option) -> Option<&SSAVar> { + phi.sources + .iter() + .rev() + .find(|(pred, source)| { + Some(*pred) != selected_prev_pc && source.display_name() != phi.dst.display_name() + }) + .map(|(_, source)| source) + .or_else(|| { + phi.sources + .iter() + .rev() + .find(|(_, source)| source.display_name() != phi.dst.display_name()) + .map(|(_, source)| source) + }) +} + +fn block_has_memory_writes(block: &FunctionSSABlock) -> bool { + block.ops.iter().any(|op| { + matches!( + op, + SSAOp::Store { .. } + | SSAOp::StoreConditional { .. } + | SSAOp::AtomicCAS { .. } + | SSAOp::StoreGuarded { .. } + ) + }) +} + +fn fold_memory_term<'ctx>( + ctx: &'ctx Context, + state: &SymState<'ctx>, + term: &LoopMemoryTerm, + iterations: u64, + bits: u32, + additive: bool, +) -> Option> { + let base = term.base?; + let stride = term.stride?; + let mut concrete: u64 = 0; + let mut symbolic: Option> = None; + for iteration in 0..iterations { + let offset = iteration.checked_mul(stride)?; + let addr = base.checked_add(offset)?; + let value = state.mem_read(&SymValue::concrete(addr, 64), term.bytes); + if let Some(item) = value.as_concrete() { + concrete = if additive { + concrete.wrapping_add(item) + } else { + concrete ^ item + }; + continue; + } + symbolic = Some(match symbolic { + Some(acc) if additive => acc.add(ctx, &value), + Some(acc) => acc.xor(ctx, &value), + None => value, + }); + } + let concrete_value = SymValue::concrete(concrete & mask_for_bits(bits), bits); + Some(match symbolic { + Some(value) if additive => value.add(ctx, &concrete_value), + Some(value) => value.xor(ctx, &concrete_value), + None => concrete_value, + }) +} + +fn apply_rotate_mix_recurrence<'ctx>( + ctx: &'ctx Context, + state: &SymState<'ctx>, + initial: SymValue<'ctx>, + term: &LoopMemoryTerm, + iterations: u64, + bits: u32, + spec: RotateMixSpec, +) -> Option> { + let base = term.base?; + let stride = term.stride?; + let bits = bits.max(1); + let mut acc = normalize_value_bits(ctx, &initial, bits); + for iteration in 0..iterations { + acc = rotate_value(ctx, &acc, spec.amount, bits, spec.direction); + let offset = iteration.checked_mul(stride)?; + let addr = base.checked_add(offset)?; + let value = state.mem_read(&SymValue::concrete(addr, 64), term.bytes); + acc = match spec.operation { + LoopFoldOperation::Add => acc.add(ctx, &value), + LoopFoldOperation::Xor => acc.xor(ctx, &value), + }; + acc = normalize_value_bits(ctx, &acc, bits); + } + Some(acc) +} + +fn normalize_value_bits<'ctx>( + ctx: &'ctx Context, + value: &SymValue<'ctx>, + bits: u32, +) -> SymValue<'ctx> { + let bits = bits.max(1); + match value.bits().cmp(&bits) { + std::cmp::Ordering::Equal => value.clone(), + std::cmp::Ordering::Less => value.zero_extend(ctx, bits), + std::cmp::Ordering::Greater => value.extract(ctx, bits - 1, 0), + } +} + +fn rotate_value<'ctx>( + ctx: &'ctx Context, + value: &SymValue<'ctx>, + amount: u32, + bits: u32, + direction: LoopRotateDirection, +) -> SymValue<'ctx> { + let bits = bits.max(1); + let amount = normalize_rotate_amount(amount, bits); + let value = normalize_value_bits(ctx, value, bits); + if amount == 0 { + return value; + } + if let Some(concrete) = value.as_concrete() { + return SymValue::concrete( + rotate_concrete_bits(concrete, amount, bits, direction), + bits, + ); + } + let head = match direction { + LoopRotateDirection::Left => value.shl(ctx, &SymValue::concrete(amount as u64, bits)), + LoopRotateDirection::Right => value.lshr(ctx, &SymValue::concrete(amount as u64, bits)), + }; + let tail_amount = bits - amount; + let tail = match direction { + LoopRotateDirection::Left => value.lshr(ctx, &SymValue::concrete(tail_amount as u64, bits)), + LoopRotateDirection::Right => value.shl(ctx, &SymValue::concrete(tail_amount as u64, bits)), + }; + normalize_value_bits(ctx, &head.or(ctx, &tail), bits) +} + +fn rotate_concrete_bits(value: u64, amount: u32, bits: u32, direction: LoopRotateDirection) -> u64 { + let bits = bits.clamp(1, 64); + let value = value & mask_for_bits(bits); + let amount = normalize_rotate_amount(amount, bits); + if amount == 0 { + return value; + } + if bits == 64 { + return match direction { + LoopRotateDirection::Left => value.rotate_left(amount), + LoopRotateDirection::Right => value.rotate_right(amount), + }; + } + let lhs = match direction { + LoopRotateDirection::Left => value.wrapping_shl(amount), + LoopRotateDirection::Right => value >> amount, + } & mask_for_bits(bits); + let rhs = match direction { + LoopRotateDirection::Left => value >> (bits - amount), + LoopRotateDirection::Right => value.wrapping_shl(bits - amount), + } & mask_for_bits(bits); + (lhs | rhs) & mask_for_bits(bits) +} + +fn value_for_name<'ctx>(state: &SymState<'ctx>, name: &str, bits: u32) -> Option> { + if let Some(value) = name + .strip_prefix("const:") + .and_then(|value| u64::from_str_radix(value, 16).ok()) + { + return Some(SymValue::concrete(value, bits.max(1))); + } + let value = state.get_register_sized(name, bits.max(1)); + if value.is_unknown() { + None + } else { + Some(value) + } +} + +fn state_value_at_block_entry<'ctx>( + state: &SymState<'ctx>, + block: &FunctionSSABlock, + var: &SSAVar, +) -> Option> { + let phi = block + .phis + .iter() + .find(|phi| phi.dst.display_name() == var.display_name()); + if let Some(phi) = phi { + return selected_phi_source(phi, state.prev_pc()) + .and_then(|source| value_for_name(state, &source.display_name(), source.size * 8)); + } + + value_for_name( + state, + &var.display_name(), + var.size.saturating_mul(8).max(1), + ) +} + +fn mul_mod_width(value: u64, iterations: u64, bits: u32) -> u64 { + let mask = mask_for_bits(bits); + value.wrapping_mul(iterations) & mask +} + +fn compose_affine_transform(outer: (u64, u64), inner: (u64, u64), bits: u32) -> (u64, u64) { + let mask = mask_for_bits(bits); + let multiplier = outer.0.wrapping_mul(inner.0) & mask; + let addend = outer.0.wrapping_mul(inner.1).wrapping_add(outer.1) & mask; + (multiplier, addend) +} + +fn affine_transform_pow(multiplier: u64, addend: u64, iterations: u64, bits: u32) -> (u64, u64) { + let mask = mask_for_bits(bits); + let mut result = (1u64, 0u64); + let mut base = (multiplier & mask, addend & mask); + let mut exp = iterations; + + while exp != 0 { + if exp & 1 == 1 { + result = compose_affine_transform(base, result, bits); + } + exp >>= 1; + if exp != 0 { + base = compose_affine_transform(base, base, bits); + } + } + + result +} + +fn normalize_rotate_amount(amount: u32, bits: u32) -> u32 { + let bits = bits.min(64); + if bits == 0 { 0 } else { amount % bits } +} + +fn mask_for_bits(bits: u32) -> u64 { + if bits >= 64 { + u64::MAX + } else { + (1u64 << bits.max(1)) - 1 + } +} + +fn push_unique_reason(reasons: &mut Vec, reason: impl Into) { + let reason = reason.into(); + if !reasons.iter().any(|existing| existing == &reason) { + reasons.push(reason); + } +} + +fn used_as_memory_addr(block: &FunctionSSABlock, phi: &SSAVar, latch_sources: &[&SSAVar]) -> bool { + let names = related_names(phi, latch_sources); + block.ops.iter().any(|op| match op { + SSAOp::Load { addr, .. } + | SSAOp::Store { addr, .. } + | SSAOp::LoadLinked { addr, .. } + | SSAOp::StoreConditional { addr, .. } + | SSAOp::LoadGuarded { addr, .. } + | SSAOp::StoreGuarded { addr, .. } => names.contains(addr.display_name().as_str()), + _ => false, + }) +} + +fn used_as_memory_value(block: &FunctionSSABlock, phi: &SSAVar, latch_sources: &[&SSAVar]) -> bool { + let names = related_names(phi, latch_sources); + block.ops.iter().any(|op| match op { + SSAOp::Store { val, .. } + | SSAOp::StoreConditional { val, .. } + | SSAOp::StoreGuarded { val, .. } => names.contains(val.display_name().as_str()), + _ => false, + }) +} + +fn latch_update_depends_on_phi( + block: &FunctionSSABlock, + phi: &SSAVar, + latch_sources: &[&SSAVar], +) -> bool { + let phi_name = phi.display_name(); + latch_sources.iter().any(|source| { + let source_name = source.display_name(); + block.ops.iter().any(|op| { + op.dst() + .is_some_and(|dst| dst.display_name() == source_name) + && op_sources_include(op, &phi_name) + }) + }) +} + +fn op_sources_include(op: &SSAOp, needle: &str) -> bool { + let mut found = false; + op.for_each_source(|source| { + if source.display_name() == needle { + found = true; + } + }); + found +} + +fn related_names<'a>(phi: &'a SSAVar, latch_sources: &[&'a SSAVar]) -> HashSet { + let mut names = HashSet::new(); + names.insert(phi.display_name()); + for source in latch_sources { + names.insert(source.display_name()); + } + names +} + +fn parse_hex_prefixed_var(var: &SSAVar, prefix: &str) -> Option { + var.name + .strip_prefix(prefix) + .and_then(|value| u64::from_str_radix(value, 16).ok()) +} + +fn parse_const_var(var: &SSAVar) -> Option { + parse_hex_prefixed_var(var, "const:") +} + +pub(crate) fn parse_address_var(var: &SSAVar) -> Option { + parse_hex_prefixed_var(var, "ram:").or_else(|| parse_const_var(var)) +} + +fn resolve_copy_source(var: &SSAVar, copies: &HashMap) -> SSAVar { + let mut current = var.clone(); + for _ in 0..8 { + let Some(next) = copies.get(¤t.display_name()) else { + break; + }; + if next.display_name() == current.display_name() { + break; + } + current = next.clone(); + } + current +} + +fn concrete_state_var<'ctx>(state: &SymState<'ctx>, var: &SSAVar) -> Option { + parse_const_var(var).or_else(|| { + state + .get_register_sized(&var.display_name(), var.size.saturating_mul(8).max(1)) + .as_concrete() + }) +} + +#[cfg(test)] +mod tests { + use r2ssa::{FunctionSSABlock, PhiNode, SSAOp, SSAVar}; + use z3::Context; + + use super::{ + ExactLoopRecurrenceKind, LoopFoldOperation, LoopMemoryTermKind, LoopRecurrenceKind, + LoopRotateDirection, LoopSummaryKind, LoopVarRole, apply_exact_transition_system, + concrete_state_var_at_block_entry, derive_loop_transition_system, + discover_loop_carried_state, exact_fold_evidence_from_system, + exact_recurrence_evidence_from_system, runtime_counter_threshold_branch, + }; + use crate::{SymState, SymValue}; + + fn var(name: &str, version: u32, size: u32) -> SSAVar { + SSAVar::new(name, version, size) + } + + fn const_var(value: u64, size: u32) -> SSAVar { + SSAVar::new(format!("const:{value:x}"), 0, size) + } + + #[test] + fn detects_counter_threshold_exit_branch() { + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: Vec::new(), + ops: vec![ + SSAOp::IntLess { + dst: var("tmp", 0, 1), + a: var("i", 1, 8), + b: const_var(0x10, 8), + }, + SSAOp::BoolNot { + dst: var("done", 0, 1), + src: var("tmp", 0, 1), + }, + SSAOp::CBranch { + target: const_var(0x2000, 8), + cond: var("done", 0, 1), + }, + ], + }; + let branch = runtime_counter_threshold_branch(&block).expect("branch"); + assert_eq!(branch.counter.display_name(), "I_1"); + assert_eq!(branch.threshold, 0x10); + assert_eq!(branch.target, 0x2000); + } + + #[test] + fn block_entry_concrete_lookup_uses_selected_phi_source() { + let counter = var("RCX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![PhiNode { + dst: counter.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }], + ops: Vec::new(), + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x1008)); + state.set_register("RCX_0", SymValue::concrete(1, 64)); + state.set_register("RCX_1", SymValue::concrete(7, 64)); + assert_eq!( + concrete_state_var_at_block_entry(&state, &block, &counter), + Some(7) + ); + } + + #[test] + fn discovers_non_rax_loop_accumulator() { + let acc_phi = var("RBX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![ + PhiNode { + dst: var("RCX", 2, 8), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }, + PhiNode { + dst: acc_phi.clone(), + sources: vec![(0x900, var("RBX", 0, 8)), (0x1008, var("RBX", 1, 8))], + }, + ], + ops: vec![SSAOp::IntXor { + dst: var("RBX", 1, 8), + a: acc_phi, + b: var("input", 0, 8), + }], + }; + let carried = discover_loop_carried_state(&block, Some(&var("RCX", 2, 8))); + assert!( + carried + .iter() + .any(|var| { var.name == "RBX_2" && var.role == LoopVarRole::Accumulator }) + ); + } + + #[test] + fn derives_exact_add_const_recurrence_summary() { + let counter_phi = var("RCX", 2, 8); + let acc_phi = var("RBX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![ + PhiNode { + dst: counter_phi.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }, + PhiNode { + dst: acc_phi.clone(), + sources: vec![(0x900, var("RBX", 0, 8)), (0x1008, var("RBX", 1, 8))], + }, + ], + ops: vec![ + SSAOp::IntAdd { + dst: var("RCX", 1, 8), + a: counter_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::IntAdd { + dst: var("RBX", 1, 8), + a: acc_phi, + b: const_var(3, 8), + }, + ], + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x900)); + state.set_register("RCX_0", SymValue::concrete(0, 64)); + state.set_register("RBX_0", SymValue::concrete(10, 64)); + let branch = super::RuntimeLoopBranch { + counter: counter_phi, + threshold: 10, + target: 0x2000, + }; + + let system = derive_loop_transition_system(&block, &state, &branch, 0, 10); + assert!(system.is_exact(), "{:?}", system.reasons); + assert!(system.recurrences.iter().any(|recurrence| { + recurrence.phi == "RBX_2" && recurrence.kind == LoopRecurrenceKind::AddConst(3) + })); + let exact_recurrences = exact_recurrence_evidence_from_system(&system); + assert!(exact_recurrences.iter().any(|recurrence| { + recurrence.accumulator == "RBX_2" + && recurrence.kind == super::ExactLoopRecurrenceKind::AddConst(3) + })); + let summary = apply_exact_transition_system(&ctx, &state, &system).expect("summary"); + assert_eq!(summary.kind, LoopSummaryKind::Exact); + assert_eq!(summary.exact_recurrences, exact_recurrences); + let summarized = summary.resulting_state.expect("state"); + assert_eq!(summarized.pc, 0x2000); + assert_eq!( + summarized.get_register_sized("RBX_2", 64).as_concrete(), + Some(40) + ); + assert_eq!( + summarized.get_register_sized("RCX_2", 64).as_concrete(), + Some(10) + ); + } + + #[test] + fn derives_exact_affine_const_recurrence_summary() { + let counter_phi = var("RCX", 2, 8); + let acc_phi = var("RBX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![ + PhiNode { + dst: counter_phi.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }, + PhiNode { + dst: acc_phi.clone(), + sources: vec![(0x900, var("RBX", 0, 8)), (0x1008, var("RBX", 1, 8))], + }, + ], + ops: vec![ + SSAOp::IntAdd { + dst: var("RCX", 1, 8), + a: counter_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::IntMult { + dst: var("TMP", 0, 8), + a: acc_phi, + b: const_var(3, 8), + }, + SSAOp::IntAdd { + dst: var("RBX", 1, 8), + a: var("TMP", 0, 8), + b: const_var(1, 8), + }, + ], + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x900)); + state.set_register("RCX_0", SymValue::concrete(0, 64)); + state.set_register("RBX_0", SymValue::concrete(2, 64)); + let branch = super::RuntimeLoopBranch { + counter: counter_phi, + threshold: 4, + target: 0x2000, + }; + + let system = derive_loop_transition_system(&block, &state, &branch, 0, 4); + assert!(system.is_exact(), "{:?}", system.reasons); + assert!(system.recurrences.iter().any(|recurrence| { + recurrence.phi == "RBX_2" + && recurrence.kind + == LoopRecurrenceKind::AffineConst { + multiplier: 3, + addend: 1, + } + })); + let exact_recurrences = exact_recurrence_evidence_from_system(&system); + assert!(exact_recurrences.iter().any(|recurrence| { + recurrence.accumulator == "RBX_2" + && recurrence.kind + == super::ExactLoopRecurrenceKind::AffineConst { + multiplier: 3, + addend: 1, + } + })); + let summary = apply_exact_transition_system(&ctx, &state, &system).expect("summary"); + assert_eq!(summary.kind, LoopSummaryKind::Exact); + let summarized = summary.resulting_state.expect("state"); + assert_eq!( + summarized.get_register_sized("RBX_2", 64).as_concrete(), + Some(202) + ); + assert_eq!( + summarized.get_register_sized("RCX_2", 64).as_concrete(), + Some(4) + ); + } + + #[test] + fn refuses_exact_recurrence_when_loop_has_memory_effects() { + let counter_phi = var("RCX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![PhiNode { + dst: counter_phi.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }], + ops: vec![ + SSAOp::IntAdd { + dst: var("RCX", 1, 8), + a: counter_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::Store { + space: "ram".to_string(), + addr: var("RAX", 0, 8), + val: var("RCX", 1, 8), + }, + ], + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x900)); + state.set_register("RCX_0", SymValue::concrete(0, 64)); + let branch = super::RuntimeLoopBranch { + counter: counter_phi, + threshold: 10, + target: 0x2000, + }; + + let system = derive_loop_transition_system(&block, &state, &branch, 0, 10); + assert!(!system.is_exact()); + assert!( + system + .reasons + .contains(&"runtime_loop_memory_write_transition_unsupported".to_string()) + ); + } + + #[test] + fn derives_exact_xor_table_fold_summary() { + let counter_phi = var("RCX", 2, 8); + let ptr_phi = var("RDI", 2, 8); + let acc_phi = var("RBX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![ + PhiNode { + dst: counter_phi.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }, + PhiNode { + dst: ptr_phi.clone(), + sources: vec![(0x900, var("RDI", 0, 8)), (0x1008, var("RDI", 1, 8))], + }, + PhiNode { + dst: acc_phi.clone(), + sources: vec![(0x900, var("RBX", 0, 8)), (0x1008, var("RBX", 1, 8))], + }, + ], + ops: vec![ + SSAOp::IntAdd { + dst: var("RCX", 1, 8), + a: counter_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::IntAdd { + dst: var("RDI", 1, 8), + a: ptr_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::Load { + dst: var("TMP", 0, 1), + space: "ram".to_string(), + addr: ptr_phi, + }, + SSAOp::IntXor { + dst: var("RBX", 1, 8), + a: acc_phi, + b: var("TMP", 0, 1), + }, + ], + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x900)); + state.set_register("RCX_0", SymValue::concrete(0, 64)); + state.set_register("RDI_0", SymValue::concrete(0x5000, 64)); + state.set_register("RBX_0", SymValue::concrete(0xaa, 64)); + state.mem_write( + &SymValue::concrete(0x5000, 64), + &SymValue::concrete(1, 8), + 1, + ); + state.mem_write( + &SymValue::concrete(0x5001, 64), + &SymValue::concrete(2, 8), + 1, + ); + state.mem_write( + &SymValue::concrete(0x5002, 64), + &SymValue::concrete(3, 8), + 1, + ); + let branch = super::RuntimeLoopBranch { + counter: counter_phi, + threshold: 3, + target: 0x2000, + }; + + let system = derive_loop_transition_system(&block, &state, &branch, 0, 3); + assert!(system.is_exact(), "{:?}", system.reasons); + let folds = exact_fold_evidence_from_system(&system); + assert_eq!(folds.len(), 1); + assert_eq!(folds[0].operation, LoopFoldOperation::Xor); + assert_eq!(folds[0].accumulator, "RBX_2"); + assert_eq!(folds[0].term.kind, LoopMemoryTermKind::TableRead); + assert!(system.recurrences.iter().any(|recurrence| { + matches!( + &recurrence.kind, + LoopRecurrenceKind::XorMemoryFold(term) + if term.kind == LoopMemoryTermKind::TableRead && term.base == Some(0x5000) + ) + })); + let summary = apply_exact_transition_system(&ctx, &state, &system).expect("summary"); + assert_eq!(summary.kind, LoopSummaryKind::Exact); + let summarized = summary.resulting_state.expect("state"); + assert_eq!( + summarized.get_register_sized("RBX_2", 64).as_concrete(), + Some(0xaa) + ); + assert_eq!( + summarized.get_register_sized("RDI_2", 64).as_concrete(), + Some(0x5003) + ); + } + + #[test] + fn derives_exact_xor_symbolic_input_fold_summary() { + let counter_phi = var("RCX", 2, 8); + let ptr_phi = var("RDI", 2, 8); + let acc_phi = var("RBX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![ + PhiNode { + dst: counter_phi.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }, + PhiNode { + dst: ptr_phi.clone(), + sources: vec![(0x900, var("RDI", 0, 8)), (0x1008, var("RDI", 1, 8))], + }, + PhiNode { + dst: acc_phi.clone(), + sources: vec![(0x900, var("RBX", 0, 8)), (0x1008, var("RBX", 1, 8))], + }, + ], + ops: vec![ + SSAOp::IntAdd { + dst: var("RCX", 1, 8), + a: counter_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::IntAdd { + dst: var("RDI", 1, 8), + a: ptr_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::Load { + dst: var("TMP", 0, 1), + space: "ram".to_string(), + addr: ptr_phi, + }, + SSAOp::IntXor { + dst: var("RBX", 1, 8), + a: acc_phi, + b: var("TMP", 0, 1), + }, + ], + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x900)); + state.set_register("RCX_0", SymValue::concrete(0, 64)); + state.set_register("RDI_0", SymValue::concrete(0x7000, 64)); + state.set_register("RBX_0", SymValue::concrete(0, 64)); + state.make_symbolic_memory(0x7000, 3, "argv1"); + let branch = super::RuntimeLoopBranch { + counter: counter_phi, + threshold: 3, + target: 0x2000, + }; + + let system = derive_loop_transition_system(&block, &state, &branch, 0, 3); + assert!(system.is_exact(), "{:?}", system.reasons); + let folds = exact_fold_evidence_from_system(&system); + assert_eq!(folds.len(), 1); + assert_eq!(folds[0].operation, LoopFoldOperation::Xor); + assert_eq!(folds[0].accumulator, "RBX_2"); + assert_eq!(folds[0].term.kind, LoopMemoryTermKind::InputRead); + assert!(system.recurrences.iter().any(|recurrence| { + matches!( + &recurrence.kind, + LoopRecurrenceKind::XorMemoryFold(term) + if term.kind == LoopMemoryTermKind::InputRead + && term.base == Some(0x7000) + && term.region_base == Some(0x7000) + && term.region_size == Some(3) + ) + })); + let summary = apply_exact_transition_system(&ctx, &state, &system).expect("summary"); + assert_eq!(summary.kind, LoopSummaryKind::Exact); + let summarized = summary.resulting_state.expect("state"); + assert!(summarized.get_register_sized("RBX_2", 64).is_symbolic()); + assert_eq!( + summarized.get_register_sized("RDI_2", 64).as_concrete(), + Some(0x7003) + ); + } + + #[test] + fn derives_exact_add_runtime_blob_fold_summary() { + let counter_phi = var("RCX", 2, 8); + let ptr_phi = var("RDI", 2, 8); + let acc_phi = var("RBX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![ + PhiNode { + dst: counter_phi.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }, + PhiNode { + dst: ptr_phi.clone(), + sources: vec![(0x900, var("RDI", 0, 8)), (0x1008, var("RDI", 1, 8))], + }, + PhiNode { + dst: acc_phi.clone(), + sources: vec![(0x900, var("RBX", 0, 8)), (0x1008, var("RBX", 1, 8))], + }, + ], + ops: vec![ + SSAOp::IntAdd { + dst: var("RCX", 1, 8), + a: counter_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::IntAdd { + dst: var("RDI", 1, 8), + a: ptr_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::Load { + dst: var("TMP", 0, 1), + space: "ram".to_string(), + addr: ptr_phi, + }, + SSAOp::IntAdd { + dst: var("RBX", 1, 8), + a: acc_phi, + b: var("TMP", 0, 1), + }, + ], + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x900)); + state.set_register("RCX_0", SymValue::concrete(0, 64)); + state.set_register("RDI_0", SymValue::concrete(0x6000_0000, 64)); + state.set_register("RBX_0", SymValue::concrete(10, 64)); + let region = state.define_runtime_region("jit_blob", 0x6000_0000, 3, true); + state.seed_region_bytes(region, 0, &[1, 2, 3]); + let branch = super::RuntimeLoopBranch { + counter: counter_phi, + threshold: 3, + target: 0x2000, + }; + + let system = derive_loop_transition_system(&block, &state, &branch, 0, 3); + assert!(system.is_exact(), "{:?}", system.reasons); + assert!(system.recurrences.iter().any(|recurrence| { + matches!( + &recurrence.kind, + LoopRecurrenceKind::AddMemoryFold(term) + if term.kind == LoopMemoryTermKind::RuntimeBlobRead + && term.base == Some(0x6000_0000) + && term.region_base == Some(0x6000_0000) + && term.region_size == Some(3) + ) + })); + let summary = apply_exact_transition_system(&ctx, &state, &system).expect("summary"); + let summarized = summary.resulting_state.expect("state"); + assert_eq!( + summarized.get_register_sized("RBX_2", 64).as_concrete(), + Some(16) + ); + } + + #[test] + fn derives_exact_rotate_xor_table_summary() { + let counter_phi = var("RCX", 2, 8); + let ptr_phi = var("RDI", 2, 8); + let acc_phi = var("RBX", 2, 1); + let block = FunctionSSABlock { + addr: 0x1000, + size: 7, + phis: vec![ + PhiNode { + dst: counter_phi.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }, + PhiNode { + dst: ptr_phi.clone(), + sources: vec![(0x900, var("RDI", 0, 8)), (0x1008, var("RDI", 1, 8))], + }, + PhiNode { + dst: acc_phi.clone(), + sources: vec![(0x900, var("RBX", 0, 1)), (0x1008, var("RBX", 1, 1))], + }, + ], + ops: vec![ + SSAOp::IntAdd { + dst: var("RCX", 1, 8), + a: counter_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::IntAdd { + dst: var("RDI", 1, 8), + a: ptr_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::Load { + dst: var("TMP", 0, 1), + space: "ram".to_string(), + addr: ptr_phi, + }, + SSAOp::IntLeft { + dst: var("ROT_L", 0, 1), + a: acc_phi.clone(), + b: const_var(3, 1), + }, + SSAOp::IntRight { + dst: var("ROT_R", 0, 1), + a: acc_phi, + b: const_var(5, 1), + }, + SSAOp::IntOr { + dst: var("ROT", 0, 1), + a: var("ROT_L", 0, 1), + b: var("ROT_R", 0, 1), + }, + SSAOp::IntXor { + dst: var("RBX", 1, 1), + a: var("ROT", 0, 1), + b: var("TMP", 0, 1), + }, + ], + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x900)); + state.set_register("RCX_0", SymValue::concrete(0, 64)); + state.set_register("RDI_0", SymValue::concrete(0x5000, 64)); + state.set_register("RBX_0", SymValue::concrete(0x12, 8)); + state.mem_write( + &SymValue::concrete(0x5000, 64), + &SymValue::concrete(1, 8), + 1, + ); + state.mem_write( + &SymValue::concrete(0x5001, 64), + &SymValue::concrete(2, 8), + 1, + ); + state.mem_write( + &SymValue::concrete(0x5002, 64), + &SymValue::concrete(3, 8), + 1, + ); + let branch = super::RuntimeLoopBranch { + counter: counter_phi, + threshold: 3, + target: 0x2000, + }; + + let system = derive_loop_transition_system(&block, &state, &branch, 0, 3); + assert!(system.is_exact(), "{:?}", system.reasons); + assert!(system.recurrences.iter().any(|recurrence| { + matches!( + &recurrence.kind, + LoopRecurrenceKind::RotateMix { + direction: LoopRotateDirection::Left, + amount: 3, + operation: LoopFoldOperation::Xor, + term, + } if term.kind == LoopMemoryTermKind::TableRead && term.base == Some(0x5000) + ) + })); + let recurrences = exact_recurrence_evidence_from_system(&system); + assert!(recurrences.iter().any(|recurrence| matches!( + &recurrence.kind, + ExactLoopRecurrenceKind::RotateMix { + direction: LoopRotateDirection::Left, + amount: 3, + operation: LoopFoldOperation::Xor, + term, + } if term.kind == LoopMemoryTermKind::TableRead + ))); + assert!(exact_fold_evidence_from_system(&system).is_empty()); + let summary = apply_exact_transition_system(&ctx, &state, &system).expect("summary"); + let summarized = summary.resulting_state.expect("state"); + assert_eq!( + summarized.get_register_sized("RBX_2", 8).as_concrete(), + Some(0x77) + ); + assert_eq!( + summarized.get_register_sized("RDI_2", 64).as_concrete(), + Some(0x5003) + ); + } + + #[test] + fn refuses_exact_symbolic_input_fold_when_bounds_are_unknown() { + let counter_phi = var("RCX", 2, 8); + let ptr_phi = var("RDI", 2, 8); + let acc_phi = var("RBX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![ + PhiNode { + dst: counter_phi.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }, + PhiNode { + dst: ptr_phi.clone(), + sources: vec![(0x900, var("RDI", 0, 8)), (0x1008, var("RDI", 1, 8))], + }, + PhiNode { + dst: acc_phi.clone(), + sources: vec![(0x900, var("RBX", 0, 8)), (0x1008, var("RBX", 1, 8))], + }, + ], + ops: vec![ + SSAOp::IntAdd { + dst: var("RCX", 1, 8), + a: counter_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::IntAdd { + dst: var("RDI", 1, 8), + a: ptr_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::Load { + dst: var("TMP", 0, 1), + space: "ram".to_string(), + addr: ptr_phi, + }, + SSAOp::IntXor { + dst: var("RBX", 1, 8), + a: acc_phi, + b: var("TMP", 0, 1), + }, + ], + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x900)); + state.set_register("RCX_0", SymValue::concrete(0, 64)); + state.set_register("RDI_0", SymValue::concrete(0x7000, 64)); + state.set_register("RBX_0", SymValue::concrete(0, 64)); + state.make_symbolic_memory(0x7000, 2, "argv1"); + let branch = super::RuntimeLoopBranch { + counter: counter_phi, + threshold: 3, + target: 0x2000, + }; + + let system = derive_loop_transition_system(&block, &state, &branch, 0, 3); + assert!(!system.is_exact()); + assert!( + system + .reasons + .contains(&"runtime_loop_memory_bounds_unknown:RBX_2".to_string()) + ); + } +} diff --git a/crates/r2sym/src/memory.rs b/crates/r2sym/src/memory.rs index 93b157d..d5a09b5 100644 --- a/crates/r2sym/src/memory.rs +++ b/crates/r2sym/src/memory.rs @@ -1,240 +1,636 @@ //! Symbolic memory model for symbolic execution. //! -//! This module provides a sparse memory model that can handle both -//! concrete and symbolic addresses and values. +//! The memory core is region-based: concrete and symbolic bytes live under +//! explicit stack/global/input/heap/replay/unknown regions. The public API +//! remains address-shaped for compatibility with the executor and summaries. -use std::collections::HashMap; -use std::collections::HashSet; +use std::collections::hash_map::DefaultHasher; +use std::collections::{BTreeMap, BTreeSet}; +use std::hash::{Hash, Hasher}; +use std::rc::Rc; -use z3::ast::{BV, Bool}; -use z3::{Context, SatResult, Solver}; +use serde::{Deserialize, Serialize}; +use z3::ast::{Ast, BV, Bool}; +use z3::{Context, DeclKind, SatResult, Solver}; use crate::value::SymValue; -/// A symbolic memory model. -/// -/// Memory is modeled as a sparse map from addresses to byte values. -/// For symbolic addresses, we concretize a bounded set of targets and -/// build an ITE chain to select the correct value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct MemoryRegionId(pub u32); + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum MemoryRegionKind { + Stack, + Global, + Input, + Heap, + Replay, + EscapedUnknown, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RegionPointer { + pub region_id: MemoryRegionId, + pub offset: u64, + pub ptr_bits: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SymbolicMemoryRegionDef { + pub id: MemoryRegionId, + pub kind: MemoryRegionKind, + pub name: String, + pub base_addr: Option, + pub extent: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvedPointerSet { + pub pointers: Vec, + pub truncated: bool, +} + +#[derive(Clone)] +struct RegionOffsetWrite<'ctx> { + offset: u64, + value: SymValue<'ctx>, + size: u32, +} + +#[derive(Clone)] +struct RegionUnresolvedWrite<'ctx> { + addr: SymValue<'ctx>, + value: SymValue<'ctx>, + size: u32, +} + +#[derive(Clone)] +struct MemoryRegion<'ctx> { + def: SymbolicMemoryRegionDef, + concrete: BTreeMap, + offset_writes: Vec>, + latest_write_by_offset: BTreeMap, + unresolved_writes: Vec>, +} + +impl<'ctx> MemoryRegion<'ctx> { + fn new( + id: MemoryRegionId, + kind: MemoryRegionKind, + name: impl Into, + base_addr: Option, + extent: Option, + ) -> Self { + Self { + def: SymbolicMemoryRegionDef { + id, + kind, + name: name.into(), + base_addr, + extent, + }, + concrete: BTreeMap::new(), + offset_writes: Vec::new(), + latest_write_by_offset: BTreeMap::new(), + unresolved_writes: Vec::new(), + } + } + + fn contains_absolute_range(&self, addr: u64, size: u32) -> bool { + let Some(base_addr) = self.def.base_addr else { + return false; + }; + let Some(offset) = addr.checked_sub(base_addr) else { + return false; + }; + self.contains_offset_range(offset, size) + } + + fn absolute_to_offset(&self, addr: u64) -> Option { + let base = self.def.base_addr?; + addr.checked_sub(base) + } + + fn contains_offset_range(&self, offset: u64, size: u32) -> bool { + match self.def.extent { + Some(extent) => offset + .checked_add(size as u64) + .is_some_and(|end| end <= extent), + None => true, + } + } + + fn ensure_extent_covers(&mut self, offset: u64, size: u32) { + let needed = offset.saturating_add(size as u64); + match self.def.extent { + Some(extent) if extent >= needed => {} + _ => self.def.extent = Some(needed), + } + } + + fn merge_offsets(&self) -> BTreeSet { + let mut offsets = BTreeSet::new(); + offsets.extend(self.concrete.keys().copied()); + offsets.extend(self.latest_write_by_offset.keys().copied()); + offsets + } + + fn unresolved_symbolic_writes(&self) -> impl Iterator> { + self.unresolved_writes.iter() + } + + fn read_byte_from_offset_write( + &self, + ctx: &'ctx Context, + byte_offset: u64, + ) -> Option> { + let write_index = *self.latest_write_by_offset.get(&byte_offset)?; + let write = self.offset_writes.get(write_index)?; + let relative = byte_offset.checked_sub(write.offset)?; + if relative >= write.size as u64 { + return None; + } + let value = adjust_bits(ctx, &write.value, write.size * 8); + let low_bit = (relative * 8) as u32; + let high_bit = low_bit + 7; + Some(value.extract(ctx, high_bit, low_bit)) + } + + fn default_read_byte( + &self, + ctx: &'ctx Context, + byte_offset: u64, + default_symbolic: bool, + default_name: &str, + ) -> SymValue<'ctx> { + if let Some(byte) = self.read_byte_from_offset_write(ctx, byte_offset) { + return byte; + } + if let Some(byte) = self.concrete.get(&byte_offset) { + return SymValue::concrete(*byte as u64, 8); + } + if default_symbolic { + SymValue::new_symbolic(ctx, &format!("{default_name}_b{byte_offset:x}"), 8) + } else { + SymValue::concrete(0, 8) + } + } + + fn read_offset( + &self, + ctx: &'ctx Context, + offset: u64, + size: u32, + default_symbolic: bool, + default_name: &str, + ) -> SymValue<'ctx> { + let mut byte_values = Vec::with_capacity(size as usize); + let mut value = 0u64; + let mut all_concrete = true; + for index in 0..size { + let byte_offset = offset.wrapping_add(index as u64); + let byte = self.default_read_byte(ctx, byte_offset, default_symbolic, default_name); + if let Some(concrete) = byte.as_concrete() { + if index < 8 { + value |= concrete << (index * 8); + } + } else { + all_concrete = false; + } + byte_values.push(byte); + } + + if all_concrete { + if size > 8 { + let mut bytes = byte_values.iter().rev().filter_map(SymValue::as_concrete); + let first = bytes.next().unwrap_or(0); + let mut ast = BV::from_u64(first, 8); + for byte in bytes { + ast = ast.concat(BV::from_u64(byte, 8)); + } + return SymValue::symbolic(ast, size * 8); + } + return SymValue::concrete(value, size * 8); + } + + let mut bytes = byte_values.iter().rev(); + let Some(first) = bytes.next() else { + return if default_symbolic { + SymValue::new_symbolic(ctx, default_name, size * 8) + } else { + SymValue::concrete(0, size * 8) + }; + }; + let mut ast = first.to_bv(ctx); + let mut taint = first.get_taint(); + for byte in bytes { + ast = ast.concat(byte.to_bv(ctx)); + taint |= byte.get_taint(); + } + SymValue::symbolic_tainted(ast, size * 8, taint) + } + + fn write_offset(&mut self, ctx: &'ctx Context, offset: u64, value: &SymValue<'ctx>, size: u32) { + let bits = size * 8; + let value = adjust_bits(ctx, value, bits); + self.ensure_extent_covers(offset, size); + if let Some(concrete_value) = value.as_concrete() { + for index in 0..size { + let byte_offset = offset.wrapping_add(index as u64); + let byte_value = if index < 8 { + ((concrete_value >> (index * 8)) & 0xff) as u8 + } else { + 0 + }; + self.concrete.insert(byte_offset, byte_value); + } + } else { + for index in 0..size { + self.concrete.remove(&offset.wrapping_add(index as u64)); + } + } + let write_index = self.offset_writes.len(); + self.offset_writes.push(RegionOffsetWrite { + offset, + value, + size, + }); + for index in 0..size { + self.latest_write_by_offset + .insert(offset.wrapping_add(index as u64), write_index); + } + } + + fn push_unresolved_write(&mut self, addr: SymValue<'ctx>, value: SymValue<'ctx>, size: u32) { + self.unresolved_writes + .push(RegionUnresolvedWrite { addr, value, size }); + } + + fn visible_offset_write_indices(&self) -> Vec { + let mut indices = self + .latest_write_by_offset + .values() + .copied() + .collect::>() + .into_iter() + .collect::>(); + indices.sort_unstable(); + indices + } +} + +/// A symbolic memory model backed by explicit regions. pub struct SymMemory<'ctx> { - /// The Z3 context. ctx: &'ctx Context, - /// Concrete memory (address -> byte value). - concrete: HashMap, - /// Symbolic memory regions (for symbolic writes). - /// Each entry represents a write: (address, value, size). - symbolic_writes: Vec<(SymValue<'ctx>, SymValue<'ctx>, u32)>, - /// Default value for uninitialized memory (0 or symbolic). + regions: Rc>>, default_symbolic: bool, - /// Maximum number of symbolic address targets to enumerate. max_symbolic_targets: usize, + next_region_id: u32, + escaped_unknown_region: MemoryRegionId, } impl<'ctx> SymMemory<'ctx> { const DEFAULT_MAX_SYMBOLIC_TARGETS: usize = 256; - /// Create a new empty memory. pub fn new(ctx: &'ctx Context) -> Self { - Self { - ctx, - concrete: HashMap::new(), - symbolic_writes: Vec::new(), - default_symbolic: false, - max_symbolic_targets: Self::DEFAULT_MAX_SYMBOLIC_TARGETS, - } + Self::new_with_default(ctx, false) } - /// Create memory with symbolic default values. pub fn new_symbolic(ctx: &'ctx Context) -> Self { + Self::new_with_default(ctx, true) + } + + fn new_with_default(ctx: &'ctx Context, default_symbolic: bool) -> Self { + let escaped_unknown_region = MemoryRegionId(0); + let mut regions = BTreeMap::new(); + regions.insert( + escaped_unknown_region, + MemoryRegion::new( + escaped_unknown_region, + MemoryRegionKind::EscapedUnknown, + "escaped_unknown", + None, + None, + ), + ); + Self { ctx, - concrete: HashMap::new(), - symbolic_writes: Vec::new(), - default_symbolic: true, + regions: Rc::new(regions), + default_symbolic, max_symbolic_targets: Self::DEFAULT_MAX_SYMBOLIC_TARGETS, + next_region_id: 1, + escaped_unknown_region, } } - /// Set the maximum number of symbolic targets to enumerate. pub fn set_max_symbolic_targets(&mut self, max: usize) { self.max_symbolic_targets = max; } - pub(crate) fn merge_addrs(&self) -> Vec { - let mut addrs = HashSet::new(); - addrs.extend(self.concrete.keys().copied()); - for (addr, _value, size) in &self.symbolic_writes { - if let Some(base) = addr.as_concrete() { - for offset in 0..*size { - addrs.insert(base.wrapping_add(offset as u64)); - } - } + pub fn escaped_unknown_region(&self) -> MemoryRegionId { + self.escaped_unknown_region + } + + pub fn define_region( + &mut self, + kind: MemoryRegionKind, + name: impl Into, + base_addr: Option, + extent: Option, + ) -> MemoryRegionId { + let name = name.into(); + if let Some(existing) = self.regions.values().find(|region| { + region.def.kind == kind + && region.def.name == name + && region.def.base_addr == base_addr + && region.def.extent == extent + }) { + return existing.def.id; } - addrs.into_iter().collect() + + let id = MemoryRegionId(self.next_region_id); + self.next_region_id += 1; + Rc::make_mut(&mut self.regions) + .insert(id, MemoryRegion::new(id, kind, name, base_addr, extent)); + id } - pub(crate) fn symbolic_writes(&self) -> &[(SymValue<'ctx>, SymValue<'ctx>, u32)] { - &self.symbolic_writes + pub fn region_defs(&self) -> Vec { + self.regions + .values() + .map(|region| region.def.clone()) + .collect() } - pub(crate) fn push_symbolic_write( - &mut self, - addr: SymValue<'ctx>, - value: SymValue<'ctx>, - size: u32, - ) { - self.symbolic_writes.push((addr, value, size)); + pub fn region_def(&self, region_id: MemoryRegionId) -> Option<&SymbolicMemoryRegionDef> { + self.regions.get(®ion_id).map(|region| ®ion.def) } - /// Read a value from memory. - /// - /// # Arguments - /// * `addr` - The address to read from - /// * `size` - The size in bytes to read - pub fn read(&self, addr: &SymValue<'ctx>, size: u32) -> SymValue<'ctx> { - self.read_with_constraints(addr, size, &[]) + pub fn seed_region_bytes(&mut self, region_id: MemoryRegionId, offset: u64, bytes: &[u8]) { + let Some(region) = Rc::make_mut(&mut self.regions).get_mut(®ion_id) else { + return; + }; + region.ensure_extent_covers(offset, bytes.len() as u32); + for (index, byte) in bytes.iter().copied().enumerate() { + region.concrete.insert(offset + index as u64, byte); + } } - /// Read a value from memory with path constraints. - pub fn read_with_constraints( + pub fn allocate_heap_region( + &mut self, + name: impl Into, + size: u64, + ) -> (MemoryRegionId, u64) { + let id = MemoryRegionId(self.next_region_id); + let base_addr = 0x6000_0000u64 + (id.0 as u64) * 0x10000; + let region_id = self.define_region( + MemoryRegionKind::Heap, + name.into(), + Some(base_addr), + Some(size), + ); + (region_id, base_addr) + } + + pub fn resolve_pointer( &self, addr: &SymValue<'ctx>, size: u32, constraints: &[Bool], - ) -> SymValue<'ctx> { - if let Some(concrete_addr) = addr.as_concrete() { - return self.read_concrete(concrete_addr, size); + ) -> ResolvedPointerSet { + if let Some(concrete_addr) = addr.as_concrete() + && let Some(pointer) = self.resolve_concrete_pointer(concrete_addr, addr.bits(), size) + { + return ResolvedPointerSet { + pointers: vec![pointer], + truncated: false, + }; } - let bits = size * 8; - let addr_taint = addr.get_taint(); - let mut result = if self.default_symbolic { - let ast = BV::fresh_const("mem_sym", bits); - SymValue::symbolic_tainted(ast, bits, addr_taint) - } else { - SymValue::concrete_tainted(0, bits, addr_taint) - }; + let (targets, truncated) = self.enumerate_symbolic_addresses(addr, constraints); + let mut seen = BTreeSet::new(); + let mut pointers = Vec::new(); + for target in targets { + if let Some(pointer) = self.resolve_concrete_pointer(target, addr.bits(), size) + && seen.insert((pointer.region_id, pointer.offset)) + { + pointers.push(pointer); + } + } - let (targets, _truncated) = self.enumerate_symbolic_addresses(addr, constraints); - if targets.is_empty() { - return result; + ResolvedPointerSet { + pointers, + truncated, } + } - let addr_bv = addr.to_bv(self.ctx); - for target in targets { - let value = self.read_concrete(target, size).with_taint(addr_taint); - let cond = addr_bv.eq(BV::from_u64(target, addr.bits())); - let taint = value.get_taint() | result.get_taint(); - let merged = SymValue::symbolic_tainted( - cond.ite(&value.to_bv(self.ctx), &result.to_bv(self.ctx)), - bits, - taint, - ); - result = merged; + pub fn region_read(&self, pointer: &RegionPointer, size: u32) -> SymValue<'ctx> { + self.read_region_pointer(pointer, size) + } + + pub fn region_write(&mut self, pointer: &RegionPointer, value: &SymValue<'ctx>, size: u32) { + self.write_region_pointer(pointer, value, size); + } + + pub(crate) fn semantic_fingerprint(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.default_symbolic.hash(&mut hasher); + self.max_symbolic_targets.hash(&mut hasher); + self.next_region_id.hash(&mut hasher); + self.escaped_unknown_region.hash(&mut hasher); + + for region in self.regions.values() { + region.def.id.hash(&mut hasher); + region.def.kind.hash(&mut hasher); + region.def.name.hash(&mut hasher); + region.def.base_addr.hash(&mut hasher); + region.def.extent.hash(&mut hasher); + + for (offset, byte) in ®ion.concrete { + offset.hash(&mut hasher); + byte.hash(&mut hasher); + } + + for index in region.visible_offset_write_indices() { + if let Some(write) = region.offset_writes.get(index) { + if write.value.as_concrete().is_some() { + continue; + } + write.offset.hash(&mut hasher); + write.size.hash(&mut hasher); + hash_sym_value(self.ctx, &write.value, &mut hasher); + } + } + + for write in ®ion.unresolved_writes { + write.size.hash(&mut hasher); + hash_sym_value(self.ctx, &write.addr, &mut hasher); + hash_sym_value(self.ctx, &write.value, &mut hasher); + } } - result + hasher.finish() } - fn enumerate_symbolic_addresses( + pub(crate) fn merge_with( &self, - addr: &SymValue<'ctx>, - constraints: &[Bool], - ) -> (Vec, bool) { - if self.max_symbolic_targets == 0 { - return (Vec::new(), true); - } + other: &Self, + constraints_self: &[Bool], + constraints_other: &[Bool], + cond_other: &Bool, + ) -> Self { + let mut merged = self.fork(); + let mut region_map = BTreeMap::new(); - let solver = Solver::new(); - for constraint in constraints { - solver.assert(constraint); + for region in other.regions.values() { + let target_id = if let Some(existing) = merged.regions.values().find(|existing| { + existing.def.kind == region.def.kind + && existing.def.name == region.def.name + && existing.def.base_addr == region.def.base_addr + && existing.def.extent == region.def.extent + }) { + existing.def.id + } else { + merged.define_region( + region.def.kind.clone(), + region.def.name.clone(), + region.def.base_addr, + region.def.extent, + ) + }; + region_map.insert(region.def.id, target_id); } - let addr_bv = addr.to_bv(self.ctx); - let mut targets = Vec::new(); - let mut truncated = false; + let all_region_ids = merged.regions.keys().copied().collect::>(); + for region_id in all_region_ids { + let merged_def = match merged.region_def(region_id).cloned() { + Some(def) => def, + None => continue, + }; + let other_region = region_map + .iter() + .find_map(|(other_id, mapped)| { + (*mapped == region_id).then(|| other.regions.get(other_id)) + }) + .flatten(); + let self_region = self + .regions + .values() + .find(|region| region.def == merged_def); + let mut offsets = BTreeSet::new(); + if let Some(region) = self_region { + offsets.extend(region.merge_offsets()); + } + if let Some(region) = other_region { + offsets.extend(region.merge_offsets()); + } - while targets.len() < self.max_symbolic_targets { - if solver.check() != SatResult::Sat { - break; + for offset in offsets { + let pointer = RegionPointer { + region_id, + offset, + ptr_bits: 64, + }; + let self_value = self_region + .map(|region| { + region.read_offset( + self.ctx, + offset, + 1, + self.default_symbolic, + &format!("merge_self_{}_{}", region_id.0, offset), + ) + }) + .unwrap_or_else(|| self.default_byte(&pointer, 1)); + let other_value = other_region + .map(|region| { + region.read_offset( + self.ctx, + offset, + 1, + other.default_symbolic, + &format!("merge_other_{}_{}", region_id.0, offset), + ) + }) + .unwrap_or_else(|| other.default_byte(&pointer, 1)); + let merged_value = merge_values(self.ctx, cond_other, &self_value, &other_value); + merged.region_write(&pointer, &merged_value, 1); } - let model = match solver.get_model() { - Some(model) => model, - None => break, + } + + for region in other.regions.values() { + let Some(mapped_region) = region_map.get(®ion.def.id).copied() else { + continue; }; - let Some(value) = model.eval(&addr_bv, true).and_then(|v| v.as_u64()) else { - truncated = true; - break; + let Some(target) = Rc::make_mut(&mut merged.regions).get_mut(&mapped_region) else { + continue; }; - - targets.push(value); - let neq = addr_bv.eq(BV::from_u64(value, addr.bits())).not(); - solver.assert(&neq); + for write in region.unresolved_symbolic_writes() { + target.push_unresolved_write(write.addr.clone(), write.value.clone(), write.size); + } } - if targets.len() == self.max_symbolic_targets && solver.check() == SatResult::Sat { - truncated = true; - } + let _ = constraints_self; + let _ = constraints_other; + merged + } - (targets, truncated) + pub fn read(&self, addr: &SymValue<'ctx>, size: u32) -> SymValue<'ctx> { + self.read_with_constraints(addr, size, &[]) } - fn read_concrete(&self, concrete_addr: u64, size: u32) -> SymValue<'ctx> { - // Check symbolic writes for this address (most recent first). - for (write_addr, write_val, write_size) in self.symbolic_writes.iter().rev() { - if let Some(wa) = write_addr.as_concrete() { - let write_end = wa.checked_add(*write_size as u64); - let read_end = concrete_addr.checked_add(size as u64); - if let (Some(write_end), Some(read_end)) = (write_end, read_end) - && wa <= concrete_addr - && write_end >= read_end - { - let offset = concrete_addr - wa; - if offset == 0 && *write_size == size { - return write_val.clone(); - } - let low_bit = (offset * 8) as u32; - let high_bit = low_bit + (size * 8) - 1; - return write_val.extract(self.ctx, high_bit, low_bit); - } - } + pub fn read_with_constraints( + &self, + addr: &SymValue<'ctx>, + size: u32, + constraints: &[Bool], + ) -> SymValue<'ctx> { + let addr_taint = addr.get_taint(); + if let Some(concrete_addr) = addr.as_concrete() { + return self + .read_concrete(concrete_addr, addr.bits(), size) + .with_taint(addr_taint); } - let mut all_concrete = true; - let mut value: u64 = 0; + let bits = size * 8; + let mut result = if self.default_symbolic { + SymValue::symbolic_tainted(BV::fresh_const("mem_sym", bits), bits, addr_taint) + } else { + SymValue::concrete_tainted(0, bits, addr_taint) + }; - for i in 0..size { - let byte_addr = concrete_addr.wrapping_add(i as u64); - if let Some(&byte) = self.concrete.get(&byte_addr) { - value |= (byte as u64) << (i * 8); - } else { - all_concrete = false; - break; - } + let resolved = self.resolve_pointer(addr, size, constraints); + if resolved.pointers.is_empty() { + return result; } - if all_concrete { - return SymValue::concrete(value, size * 8); + let addr_bv = addr.to_bv(self.ctx); + for pointer in resolved.pointers { + let Some(def) = self.region_def(pointer.region_id) else { + continue; + }; + let Some(base_addr) = def.base_addr else { + continue; + }; + let target_addr = base_addr.wrapping_add(pointer.offset); + let value = self.region_read(&pointer, size).with_taint(addr_taint); + let cond = addr_bv.eq(BV::from_u64(target_addr, addr.bits())); + let taint = value.get_taint() | result.get_taint(); + result = SymValue::symbolic_tainted( + cond.ite(&value.to_bv(self.ctx), &result.to_bv(self.ctx)), + bits, + taint, + ); } - if self.default_symbolic { - SymValue::new_symbolic(self.ctx, &format!("mem_{:x}", concrete_addr), size * 8) - } else { - SymValue::concrete(0, size * 8) - } + result } - /// Write a value to memory. - /// - /// # Arguments - /// * `addr` - The address to write to - /// * `value` - The value to write - /// * `size` - The size in bytes to write pub fn write(&mut self, addr: &SymValue<'ctx>, value: &SymValue<'ctx>, size: u32) { self.write_with_constraints(addr, value, size, &[]); } - /// Write a value to memory with path constraints. pub fn write_with_constraints( &mut self, addr: &SymValue<'ctx>, @@ -245,102 +641,299 @@ impl<'ctx> SymMemory<'ctx> { let bits = size * 8; let value = adjust_bits(self.ctx, value, bits); - if let Some(concrete_addr) = addr.as_concrete() { - if let Some(concrete_value) = value.as_concrete() { - for i in 0..size { - let byte_addr = concrete_addr.wrapping_add(i as u64); - let byte_value = ((concrete_value >> (i * 8)) & 0xFF) as u8; - self.concrete.insert(byte_addr, byte_value); - } - } - self.symbolic_writes - .push((addr.clone(), value.clone(), size)); + if let Some(concrete_addr) = addr.as_concrete() + && let Some(pointer) = self.resolve_concrete_pointer(concrete_addr, addr.bits(), size) + { + self.write_region_pointer(&pointer, &value, size); return; } - let (targets, truncated) = self.enumerate_symbolic_addresses(addr, constraints); - if targets.is_empty() { - self.symbolic_writes - .push((addr.clone(), value.clone(), size)); + let resolved = self.resolve_pointer(addr, size, constraints); + if resolved.pointers.is_empty() { + if let Some(region) = + Rc::make_mut(&mut self.regions).get_mut(&self.escaped_unknown_region) + { + region.push_unresolved_write(addr.clone(), value, size); + } return; } let addr_bv = addr.to_bv(self.ctx); - for target in targets { - let existing = self.read_concrete(target, size); - let existing = adjust_bits(self.ctx, &existing, bits); - let cond = addr_bv.eq(BV::from_u64(target, addr.bits())); + for pointer in resolved.pointers { + let existing = self.read_region_pointer(&pointer, size); + let Some(def) = self.region_def(pointer.region_id) else { + continue; + }; + let Some(base_addr) = def.base_addr else { + continue; + }; + let concrete_target = base_addr.wrapping_add(pointer.offset); + let cond = addr_bv.eq(BV::from_u64(concrete_target, addr.bits())); let taint = existing.get_taint() | value.get_taint() | addr.get_taint(); let merged = SymValue::symbolic_tainted( cond.ite(&value.to_bv(self.ctx), &existing.to_bv(self.ctx)), bits, taint, ); - let target_addr = SymValue::concrete(target, addr.bits()); - self.symbolic_writes.push((target_addr, merged, size)); + self.write_region_pointer(&pointer, &merged, size); } - if truncated { - self.symbolic_writes - .push((addr.clone(), value.clone(), size)); + if resolved.truncated + && let Some(region) = + Rc::make_mut(&mut self.regions).get_mut(&self.escaped_unknown_region) + { + region.push_unresolved_write(addr.clone(), value, size); } } - /// Write concrete bytes to memory. pub fn write_bytes(&mut self, addr: u64, bytes: &[u8]) { - for (i, &byte) in bytes.iter().enumerate() { - self.concrete.insert(addr + i as u64, byte); + for (index, byte) in bytes.iter().copied().enumerate() { + let addr = SymValue::concrete(addr + index as u64, 64); + let value = SymValue::concrete(byte as u64, 8); + self.write(&addr, &value, 1); } } - /// Read concrete bytes from memory. pub fn read_bytes(&self, addr: u64, size: usize) -> Option> { let mut bytes = Vec::with_capacity(size); - for i in 0..size { - if let Some(&byte) = self.concrete.get(&(addr + i as u64)) { - bytes.push(byte); - } else { - return None; - } + for index in 0..size { + let pointer = self.resolve_concrete_pointer(addr + index as u64, 64, 1)?; + let region = self.regions.get(&pointer.region_id)?; + bytes.push(*region.concrete.get(&pointer.offset)?); } Some(bytes) } - /// Check if an address range is fully concrete. pub fn is_concrete_range(&self, addr: u64, size: u32) -> bool { - for i in 0..size { - if !self.concrete.contains_key(&(addr + i as u64)) { + for index in 0..size { + let Some(pointer) = self.resolve_concrete_pointer(addr + index as u64, 64, 1) else { + return false; + }; + let Some(region) = self.regions.get(&pointer.region_id) else { + return false; + }; + if !region.concrete.contains_key(&pointer.offset) { return false; } } true } - /// Get the number of concrete bytes stored. pub fn concrete_size(&self) -> usize { - self.concrete.len() + self.regions + .values() + .map(|region| region.concrete.len()) + .sum() } - /// Get the number of symbolic writes. pub fn symbolic_writes_count(&self) -> usize { - self.symbolic_writes.len() + self.regions + .values() + .map(|region| region.offset_writes.len() + region.unresolved_writes.len()) + .sum() } - /// Clone the memory state (for forking). pub fn fork(&self) -> Self { Self { ctx: self.ctx, - concrete: self.concrete.clone(), - symbolic_writes: self.symbolic_writes.clone(), + regions: self.regions.clone(), default_symbolic: self.default_symbolic, max_symbolic_targets: self.max_symbolic_targets, + next_region_id: self.next_region_id, + escaped_unknown_region: self.escaped_unknown_region, } } - /// Clear all memory. pub fn clear(&mut self) { - self.concrete.clear(); - self.symbolic_writes.clear(); + for region in Rc::make_mut(&mut self.regions).values_mut() { + region.concrete.clear(); + region.offset_writes.clear(); + region.latest_write_by_offset.clear(); + region.unresolved_writes.clear(); + } + } + + fn resolve_concrete_pointer( + &self, + addr: u64, + ptr_bits: u32, + size: u32, + ) -> Option { + let region = self + .regions + .values() + .filter(|region| region.contains_absolute_range(addr, size)) + .min_by(|left, right| compare_region_specificity(left, right)) + .map(|region| region.def.id) + .unwrap_or(self.escaped_unknown_region); + + let offset = if region == self.escaped_unknown_region { + addr + } else { + self.regions.get(®ion)?.absolute_to_offset(addr)? + }; + + Some(RegionPointer { + region_id: region, + offset, + ptr_bits, + }) + } + + fn read_concrete(&self, addr: u64, ptr_bits: u32, size: u32) -> SymValue<'ctx> { + let Some(pointer) = self.resolve_concrete_pointer(addr, ptr_bits, size) else { + return SymValue::concrete(0, size * 8); + }; + self.read_region_pointer(&pointer, size) + } + + fn read_region_pointer(&self, pointer: &RegionPointer, size: u32) -> SymValue<'ctx> { + let Some(region) = self.regions.get(&pointer.region_id) else { + return self.default_byte(pointer, size); + }; + region.read_offset( + self.ctx, + pointer.offset, + size, + self.default_symbolic, + &format!("mem_{}_{}", pointer.region_id.0, pointer.offset), + ) + } + + fn write_region_pointer(&mut self, pointer: &RegionPointer, value: &SymValue<'ctx>, size: u32) { + let Some(region) = Rc::make_mut(&mut self.regions).get_mut(&pointer.region_id) else { + return; + }; + region.write_offset(self.ctx, pointer.offset, value, size); + } + + fn default_byte(&self, pointer: &RegionPointer, size: u32) -> SymValue<'ctx> { + if self.default_symbolic { + SymValue::new_symbolic( + self.ctx, + &format!("mem_{}_{}", pointer.region_id.0, pointer.offset), + size * 8, + ) + } else { + SymValue::concrete(0, size * 8) + } + } + + fn enumerate_symbolic_addresses( + &self, + addr: &SymValue<'ctx>, + constraints: &[Bool], + ) -> (Vec, bool) { + if let Some(affine) = recognize_affine_pointer(addr) { + let (targets, truncated) = + self.enumerate_symbolic_addresses_sat(&affine.base, constraints); + return ( + targets + .into_iter() + .filter_map(|value| value.checked_add_signed(affine.delta)) + .collect(), + truncated, + ); + } + self.enumerate_symbolic_addresses_sat(addr, constraints) + } + + fn enumerate_symbolic_addresses_sat( + &self, + addr: &SymValue<'ctx>, + constraints: &[Bool], + ) -> (Vec, bool) { + if self.max_symbolic_targets == 0 { + return (Vec::new(), true); + } + + let solver = Solver::new(); + for constraint in constraints { + solver.assert(constraint); + } + + let addr_bv = addr.to_bv(self.ctx); + let mut targets = Vec::new(); + let mut truncated = false; + + while targets.len() < self.max_symbolic_targets { + if solver.check() != SatResult::Sat { + break; + } + let Some(model) = solver.get_model() else { + break; + }; + let Some(value) = model.eval(&addr_bv, true).and_then(|value| value.as_u64()) else { + truncated = true; + break; + }; + + targets.push(value); + solver.assert(addr_bv.eq(BV::from_u64(value, addr.bits())).not()); + } + + if targets.len() == self.max_symbolic_targets && solver.check() == SatResult::Sat { + truncated = true; + } + + (targets, truncated) + } +} + +fn compare_region_specificity<'ctx>( + left: &MemoryRegion<'ctx>, + right: &MemoryRegion<'ctx>, +) -> std::cmp::Ordering { + fn kind_rank(kind: &MemoryRegionKind) -> u8 { + match kind { + MemoryRegionKind::Replay => 0, + MemoryRegionKind::Input => 1, + MemoryRegionKind::Global => 2, + MemoryRegionKind::Heap => 3, + MemoryRegionKind::Stack => 4, + MemoryRegionKind::EscapedUnknown => 5, + } + } + + let left_key = ( + left.def.extent.is_none(), + left.def.extent.unwrap_or(u64::MAX), + kind_rank(&left.def.kind), + std::cmp::Reverse(left.def.base_addr.unwrap_or(0)), + left.def.id, + ); + let right_key = ( + right.def.extent.is_none(), + right.def.extent.unwrap_or(u64::MAX), + kind_rank(&right.def.kind), + std::cmp::Reverse(right.def.base_addr.unwrap_or(0)), + right.def.id, + ); + left_key.cmp(&right_key) +} + +fn merge_values<'ctx>( + ctx: &'ctx Context, + cond: &Bool, + base: &SymValue<'ctx>, + incoming: &SymValue<'ctx>, +) -> SymValue<'ctx> { + let bits = base.bits().max(incoming.bits()); + let taint = base.get_taint() | incoming.get_taint(); + let base_bv = widen_value(ctx, base, bits); + let incoming_bv = widen_value(ctx, incoming, bits); + SymValue::symbolic_tainted(cond.ite(&incoming_bv, &base_bv), bits, taint) +} + +fn widen_value<'ctx>(ctx: &'ctx Context, value: &SymValue<'ctx>, bits: u32) -> BV { + let bv = value.to_bv(ctx); + let value_bits = value.bits(); + if value_bits == bits { + bv + } else if value_bits < bits { + bv.zero_ext(bits - value_bits) + } else { + bv.extract(bits - 1, 0) } } @@ -355,16 +948,116 @@ fn adjust_bits<'ctx>(ctx: &'ctx Context, value: &SymValue<'ctx>, bits: u32) -> S } } +fn hash_sym_value<'ctx, H: Hasher>(ctx: &'ctx Context, value: &SymValue<'ctx>, hasher: &mut H) { + value.bits().hash(hasher); + value.get_taint().hash(hasher); + value.to_bv(ctx).hash(hasher); +} + impl<'ctx> std::fmt::Debug for SymMemory<'ctx> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SymMemory") - .field("concrete_bytes", &self.concrete.len()) - .field("symbolic_writes", &self.symbolic_writes.len()) + .field("regions", &self.regions.len()) + .field("concrete_bytes", &self.concrete_size()) + .field("symbolic_writes", &self.symbolic_writes_count()) .field("default_symbolic", &self.default_symbolic) .finish() } } +#[derive(Clone)] +struct AffinePointer<'ctx> { + base: SymValue<'ctx>, + delta: i64, +} + +fn recognize_affine_pointer<'ctx>(addr: &SymValue<'ctx>) -> Option> { + let ast = addr.as_ast()?; + let (base, delta) = recognize_affine_bv(ast)?; + Some(AffinePointer { + base: SymValue::symbolic_tainted(base, addr.bits(), addr.get_taint()), + delta, + }) +} + +fn recognize_affine_bv(ast: &BV) -> Option<(BV, i64)> { + if ast.as_u64().is_some() { + return None; + } + + if ast.is_const() && ast.decl().kind() == DeclKind::UNINTERPRETED { + return Some((ast.clone(), 0)); + } + + match ast.decl().kind() { + DeclKind::BADD => { + let children = ast.children(); + if children.len() != 2 { + return None; + } + let left = children[0].as_bv()?; + let right = children[1].as_bv()?; + if let Some(delta) = affine_constant(&right) { + let (base, base_delta) = recognize_affine_bv(&left)?; + return base_delta + .checked_add(delta) + .map(|combined| (widen_affine_base(&base, ast.get_size()), combined)); + } + if let Some(delta) = affine_constant(&left) { + let (base, base_delta) = recognize_affine_bv(&right)?; + return base_delta + .checked_add(delta) + .map(|combined| (widen_affine_base(&base, ast.get_size()), combined)); + } + None + } + DeclKind::BSUB => { + let children = ast.children(); + if children.len() != 2 { + return None; + } + let left = children[0].as_bv()?; + let right = children[1].as_bv()?; + let delta = affine_constant(&right)?; + let (base, base_delta) = recognize_affine_bv(&left)?; + base_delta + .checked_sub(delta) + .map(|combined| (widen_affine_base(&base, ast.get_size()), combined)) + } + DeclKind::ZERO_EXT | DeclKind::SIGN_EXT => { + let child = ast.nth_child(0)?.as_bv()?; + let (base, delta) = recognize_affine_bv(&child)?; + Some((widen_affine_base(&base, ast.get_size()), delta)) + } + _ => None, + } +} + +fn widen_affine_base(base: &BV, target_bits: u32) -> BV { + let base_bits = base.get_size(); + if base_bits == target_bits { + base.clone() + } else if base_bits < target_bits { + base.zero_ext(target_bits - base_bits) + } else { + base.extract(target_bits - 1, 0) + } +} + +fn affine_constant(ast: &BV) -> Option { + let value = ast.as_u64()?; + if let Ok(delta) = i64::try_from(value) { + return Some(delta); + } + let bits = ast.get_size(); + if bits == 0 || bits >= 64 { + let sign_bit = 1u64 << 63; + return (value & sign_bit != 0).then_some(value as i64); + } + let sign_bit = 1u64 << (bits - 1); + (value & sign_bit != 0).then_some(value as i64) +} + #[cfg(test)] mod tests { use super::*; @@ -375,57 +1068,81 @@ mod tests { fn test_concrete_read_write() { let ctx = Context::thread_local(); let mut mem = SymMemory::new(&ctx); + let globals = mem.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x1000), + Some(0x100), + ); - let addr = SymValue::concrete(0x1000, 64); - let value = SymValue::concrete(0xDEADBEEF, 32); - - mem.write(&addr, &value, 4); - - let read_value = mem.read(&addr, 4); - assert_eq!(read_value.as_concrete(), Some(0xDEADBEEF)); - } - - #[test] - fn test_byte_access() { - let ctx = Context::thread_local(); - let mut mem = SymMemory::new(&ctx); - - mem.write_bytes(0x1000, &[0x11, 0x22, 0x33, 0x44]); - - let bytes = mem.read_bytes(0x1000, 4).unwrap(); - assert_eq!(bytes, vec![0x11, 0x22, 0x33, 0x44]); + let ptr = RegionPointer { + region_id: globals, + offset: 0, + ptr_bits: 64, + }; + mem.region_write(&ptr, &SymValue::concrete(0xdeadbeef, 32), 4); let addr = SymValue::concrete(0x1000, 64); - let value = mem.read(&addr, 4); - assert_eq!(value.as_concrete(), Some(0x44332211)); // Little-endian + assert_eq!(mem.read(&addr, 4).as_concrete(), Some(0xdeadbeef)); } #[test] - fn test_uninitialized_read() { + fn test_region_resolution_prefers_specific_input_over_stack() { let ctx = Context::thread_local(); - let mem = SymMemory::new(&ctx); + let mut mem = SymMemory::new(&ctx); + let _stack = mem.define_region( + MemoryRegionKind::Stack, + "stack_window", + Some(0x7fff_0000 - 0x8000), + Some(0x10000), + ); + let input = mem.define_region( + MemoryRegionKind::Input, + "stdin", + Some(0x7fff_1000), + Some(0x10), + ); - let addr = SymValue::concrete(0x2000, 64); - let value = mem.read(&addr, 4); - // Default is concrete 0 - assert_eq!(value.as_concrete(), Some(0)); + let resolved = mem.resolve_pointer(&SymValue::concrete(0x7fff_1004, 64), 1, &[]); + assert_eq!(resolved.pointers.len(), 1); + assert_eq!(resolved.pointers[0].region_id, input); + assert_eq!(resolved.pointers[0].offset, 4); } #[test] - fn test_symbolic_default() { + fn test_region_resolution_prefers_replay_over_global_when_overlapping() { let ctx = Context::thread_local(); - let mem = SymMemory::new_symbolic(&ctx); + let mut mem = SymMemory::new(&ctx); + let global = mem.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x4000), + Some(0x20), + ); + let replay = mem.define_region( + MemoryRegionKind::Replay, + "checkpoint", + Some(0x4000), + Some(0x20), + ); - let addr = SymValue::concrete(0x2000, 64); - let value = mem.read(&addr, 4); - // Default is symbolic - assert!(value.is_symbolic()); + let resolved = mem.resolve_pointer(&SymValue::concrete(0x4004, 64), 1, &[]); + assert_eq!(resolved.pointers.len(), 1); + assert_eq!(resolved.pointers[0].region_id, replay); + assert_ne!(resolved.pointers[0].region_id, global); + assert_eq!(resolved.pointers[0].offset, 4); } #[test] fn test_symbolic_address_write_then_read() { let ctx = Context::thread_local(); let mut mem = SymMemory::new(&ctx); + mem.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x1000), + Some(0x2000), + ); let idx = SymValue::new_symbolic(&ctx, "idx", 64); let addr_bv = idx.to_bv(&ctx); @@ -433,25 +1150,226 @@ mod tests { let eq2 = addr_bv.eq(BV::from_u64(0x2000, 64)); let constraint = eq1.clone() | eq2.clone(); - let value = SymValue::concrete(0xCAFEBABE, 32); + let value = SymValue::concrete(0xcafebabe, 32); mem.write_with_constraints(&idx, &value, 4, std::slice::from_ref(&constraint)); - let read_val = mem.read_with_constraints(&idx, 4, &[constraint]); - assert!(read_val.is_symbolic()); + let read_value = mem.read_with_constraints(&idx, 4, &[constraint]); + assert!(read_value.is_symbolic()); let solver = Solver::new(); solver.assert(&eq1); assert_eq!(solver.check(), SatResult::Sat); let model = solver.get_model().unwrap(); - let result_bv = read_val.to_bv(&ctx); - let val = model.eval(&result_bv, true).unwrap().as_u64().unwrap(); - assert_eq!(val, 0xCAFEBABE); + let value = model + .eval(&read_value.to_bv(&ctx), true) + .unwrap() + .as_u64() + .unwrap(); + assert_eq!(value, 0xcafebabe); let solver = Solver::new(); solver.assert(&eq2); assert_eq!(solver.check(), SatResult::Sat); let model = solver.get_model().unwrap(); - let val = model.eval(&result_bv, true).unwrap().as_u64().unwrap(); - assert_eq!(val, 0xCAFEBABE); + let value = model + .eval(&read_value.to_bv(&ctx), true) + .unwrap() + .as_u64() + .unwrap(); + assert_eq!(value, 0xcafebabe); + } + + #[test] + fn test_unknown_region_preserves_concrete_bytes() { + let ctx = Context::thread_local(); + let mut mem = SymMemory::new(&ctx); + mem.write_bytes(0x9000, &[0x11, 0x22, 0x33, 0x44]); + assert_eq!( + mem.read_bytes(0x9000, 4), + Some(vec![0x11, 0x22, 0x33, 0x44]) + ); + } + + #[test] + fn test_overlapping_concrete_offset_writes_preserve_exact_bytes() { + let ctx = Context::thread_local(); + let mut mem = SymMemory::new(&ctx); + let globals = mem.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x2000), + Some(0x100), + ); + + let base = RegionPointer { + region_id: globals, + offset: 0, + ptr_bits: 64, + }; + let overlap = RegionPointer { + region_id: globals, + offset: 1, + ptr_bits: 64, + }; + mem.region_write(&base, &SymValue::concrete(0x1122_3344, 32), 4); + mem.region_write(&overlap, &SymValue::concrete(0xaabb, 16), 2); + + let addr = SymValue::concrete(0x2000, 64); + assert_eq!(mem.read(&addr, 4).as_concrete(), Some(0x11aa_bb44)); + assert_eq!( + mem.read_bytes(0x2000, 4), + Some(vec![0x44, 0xbb, 0xaa, 0x11]) + ); + } + + #[test] + fn test_symbolic_offset_write_invalidates_overwritten_concrete_bytes() { + let ctx = Context::thread_local(); + let mut mem = SymMemory::new(&ctx); + let globals = mem.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x3000), + Some(0x100), + ); + mem.seed_region_bytes(globals, 0, &[0x10, 0x20, 0x30, 0x40]); + + let ptr = RegionPointer { + region_id: globals, + offset: 1, + ptr_bits: 64, + }; + mem.region_write(&ptr, &SymValue::new_symbolic(&ctx, "sym_word", 16), 2); + + assert_eq!(mem.read_bytes(0x3000, 4), None); + assert!(!mem.is_concrete_range(0x3000, 4)); + assert_eq!( + mem.read(&SymValue::concrete(0x3000, 64), 1).as_concrete(), + Some(0x10) + ); + assert!(mem.read(&SymValue::concrete(0x3001, 64), 1).is_symbolic()); + assert!(mem.read(&SymValue::concrete(0x3002, 64), 1).is_symbolic()); + assert_eq!( + mem.read(&SymValue::concrete(0x3003, 64), 1).as_concrete(), + Some(0x40) + ); + } + + #[test] + fn test_overlapping_symbolic_offset_writes_compose_by_byte() { + let ctx = Context::thread_local(); + let mut mem = SymMemory::new(&ctx); + let globals = mem.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x4000), + Some(0x100), + ); + + let wide = RegionPointer { + region_id: globals, + offset: 0, + ptr_bits: 64, + }; + let byte = RegionPointer { + region_id: globals, + offset: 1, + ptr_bits: 64, + }; + let word = SymValue::new_symbolic(&ctx, "word", 32); + mem.region_write(&wide, &word, 4); + mem.region_write(&byte, &SymValue::concrete(0xaa, 8), 1); + + let read_back = mem.read(&SymValue::concrete(0x4000, 64), 4); + let solver = Solver::new(); + solver.assert(word.to_bv(&ctx).eq(BV::from_u64(0x1122_3344, 32))); + assert_eq!(solver.check(), SatResult::Sat); + let model = solver.get_model().unwrap(); + let value = model + .eval(&read_back.to_bv(&ctx), true) + .and_then(|value| value.as_u64()) + .unwrap(); + assert_eq!(value, 0x1122_aa44); + } + + #[test] + fn test_wide_concrete_region_write_does_not_shift_overflow() { + let ctx = Context::thread_local(); + let mut mem = SymMemory::new(&ctx); + let globals = mem.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x5000), + Some(0x100), + ); + + let ptr = RegionPointer { + region_id: globals, + offset: 0, + ptr_bits: 64, + }; + mem.region_write(&ptr, &SymValue::concrete(0x1122_3344_5566_7788, 128), 16); + + assert_eq!( + mem.read_bytes(0x5000, 16), + Some(vec![ + 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0, 0, 0, 0, 0, 0, 0, 0, + ]) + ); + } + + #[test] + fn test_affine_pointer_recognizer_extracts_symbol_plus_delta() { + let ctx = Context::thread_local(); + let base = SymValue::new_symbolic(&ctx, "ptr_base", 64); + let addr = base.add(&ctx, &SymValue::concrete(0x20, 64)); + + let affine = recognize_affine_pointer(&addr).expect("affine recognizer should match"); + assert_eq!(affine.delta, 0x20); + let same = affine.base.to_bv(&ctx).eq(base.to_bv(&ctx)); + assert_eq!(same.simplify().as_bool(), Some(true)); + } + + #[test] + fn test_semantic_fingerprint_ignores_equivalent_concrete_write_chunking() { + let ctx = Context::thread_local(); + let mut lhs = SymMemory::new(&ctx); + let mut rhs = SymMemory::new(&ctx); + let globals_lhs = lhs.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x5000), + Some(0x100), + ); + let globals_rhs = rhs.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x5000), + Some(0x100), + ); + + lhs.region_write( + &RegionPointer { + region_id: globals_lhs, + offset: 0, + ptr_bits: 64, + }, + &SymValue::concrete(0x4433_2211, 32), + 4, + ); + for (index, byte) in [0x11u8, 0x22, 0x33, 0x44].into_iter().enumerate() { + rhs.region_write( + &RegionPointer { + region_id: globals_rhs, + offset: index as u64, + ptr_bits: 64, + }, + &SymValue::concrete(byte as u64, 8), + 1, + ); + } + + assert_eq!(lhs.read_bytes(0x5000, 4), rhs.read_bytes(0x5000, 4)); + assert_eq!(lhs.semantic_fingerprint(), rhs.semantic_fingerprint()); } } diff --git a/crates/r2sym/src/path.rs b/crates/r2sym/src/path.rs index ce1d8bf..7824ab6 100644 --- a/crates/r2sym/src/path.rs +++ b/crates/r2sym/src/path.rs @@ -3,21 +3,108 @@ //! This module provides different strategies for exploring paths //! during symbolic execution, including DFS, BFS, and coverage-guided. -use std::collections::{HashSet, VecDeque}; +use std::cmp::Reverse; +use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque}; +use std::fs::OpenOptions; +use std::io::Write; +use std::rc::Rc; use std::time::{Duration, Instant}; -use r2ssa::{BlockTerminator, SsaArtifact}; +use r2ssa::{AbiProfile, BlockTerminator, SSAOp, SsaArtifact, observe_call_arguments}; use z3::Context; +use crate::backward::DerivedCallSummaryView; +use crate::constraints::FinalConstraintGraph; use crate::executor::SymExecutor; +use crate::loops::{self, ExactLoopFoldEvidence, ExactLoopRecurrenceEvidence, LoopSummaryKind}; +use crate::sim::{ + CallConv, DerivedFunctionSummary, PreparedFunctionScope, evaluate_derived_summary_guidance, +}; use crate::solver::SymSolver; -use crate::state::{ExitStatus, SymState}; +use crate::spec::ExplorationSpec; +use crate::state::{ExitStatus, RuntimeBlockReason, SymState}; +use crate::tactics::{ + SolveTacticConfig, constrain_exact_fold_inputs, constrain_exact_recurrence_candidate, + tactic_candidates_for_constraint_graph, +}; + +const TARGET_DISTANCE_CACHE_LIMIT: usize = 128; +const EXACT_RUNTIME_LOOP_MAX_ITERS: u64 = 4096; +const EXACT_RUNTIME_LOOP_MAX_BLOCK_STEPS: u64 = 200_000; +const SYMBOLIC_CONTINUATION_MAX_TARGETS: usize = 512; + +fn debug_target_guidance_enabled() -> bool { + std::env::var_os("R2SLEIGH_DEBUG_TARGET_GUIDANCE").is_some() +} + +fn debug_target_guidance_log(message: &str) { + if !debug_target_guidance_enabled() { + return; + } + let path = std::env::var("R2SLEIGH_DEBUG_TARGET_GUIDANCE_LOG") + .unwrap_or_else(|_| "/tmp/r2sleigh_target_guidance.log".to_string()); + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { + let _ = writeln!(file, "{message}"); + } +} + +fn debug_target_match_unsat_enabled() -> bool { + std::env::var_os("R2SLEIGH_DEBUG_TARGET_MATCH_UNSAT").is_some() +} + +fn debug_target_match_unsat_log(message: &str) { + if !debug_target_match_unsat_enabled() { + return; + } + let path = std::env::var("R2SLEIGH_DEBUG_TARGET_MATCH_UNSAT_LOG") + .unwrap_or_else(|_| "/tmp/r2sleigh_target_match_unsat.log".to_string()); + if let Ok(mut file) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(path) + { + let _ = writeln!(file, "{message}"); + } +} + +fn debug_log_target_unsat_state(state: &SymState<'_>, target_addr: u64) { + if !debug_target_match_unsat_enabled() { + return; + } + let mut registers = state + .registers() + .iter() + .filter_map(|(name, value)| { + let interesting = ["AL_", "EAX_", "RAX_", "RCX_", "RDI_", "RDX_", "RSP_"] + .iter() + .any(|prefix| name.starts_with(prefix)); + interesting.then(|| match value.as_concrete() { + Some(value) => format!("{name}={value:#x}"), + None => format!("{name}=sym({}b)", value.bits()), + }) + }) + .collect::>(); + registers.sort(); + debug_target_match_unsat_log(&format!( + "target=0x{target_addr:x} pc=0x{:x} prev_pc={} depth={} constraints={} regs=[{}]", + state.pc, + state + .prev_pc() + .map(|pc| format!("0x{pc:x}")) + .unwrap_or_else(|| "none".to_string()), + state.depth, + state.num_constraints(), + registers.join(", ") + )); +} /// Configuration for path exploration. #[derive(Debug, Clone)] pub struct ExploreConfig { /// Maximum number of states to explore. pub max_states: usize, + /// Maximum number of completed paths to collect during full exploration. + pub max_completed_paths: Option, /// Maximum execution depth per path. pub max_depth: usize, /// Timeout for the entire exploration. @@ -28,17 +115,21 @@ pub struct ExploreConfig { pub prune_infeasible: bool, /// Whether to merge states at join points. pub merge_states: bool, + /// Whether to drop same-PC states that are already covered by weaker states. + pub subsumption_states: bool, } impl Default for ExploreConfig { fn default() -> Self { Self { max_states: 1000, + max_completed_paths: None, max_depth: 100, timeout: Some(Duration::from_secs(60)), strategy: ExploreStrategy::Dfs, prune_infeasible: true, merge_states: false, + subsumption_states: false, } } } @@ -92,7 +183,9 @@ impl<'ctx> PathResult<'ctx> { /// Get all register names in the final state. pub fn register_names(&self) -> Vec { - self.state.register_names().cloned().collect() + let mut names: Vec = self.state.register_names().cloned().collect(); + names.sort(); + names } /// Get a register value (returns None if symbolic or not set). @@ -110,15 +203,50 @@ impl<'ctx> PathResult<'ctx> { #[derive(Debug, Clone, Default)] pub struct SolvedPath { /// Concrete input values (symbolic variable name -> value). - pub inputs: std::collections::HashMap, + pub inputs: BTreeMap, + /// Concrete multi-byte input buffers (input source name -> bytes). + pub input_buffers: BTreeMap>, /// Concrete register values at path end. - pub registers: std::collections::HashMap, + pub registers: BTreeMap, /// Concrete memory bytes for tracked symbolic regions. - pub memory: std::collections::HashMap>, + pub memory: BTreeMap>, /// Final program counter. pub final_pc: u64, /// Path constraints that were satisfied. pub num_constraints: usize, + /// Optional provenance when the candidate was produced by a solve tactic. + pub generation: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SolvedPathGenerationKind { + ExactRecurrenceConstraintTactic, + MitmConstraintTactic, + DomainConstraintTactic, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SolvedPathGeneration { + pub kind: SolvedPathGenerationKind, + pub reason: String, + pub constrained_bytes: usize, +} + +/// Result of a spec-driven exploration run. +#[derive(Debug, Default)] +pub struct SpecExploreResult<'ctx> { + /// Feasible paths that matched a find predicate. + pub found_paths: Vec>, + /// Number of states pruned by avoid predicates. + pub avoided_states: usize, + /// Number of states pruned as infeasible. + pub unsat_states: usize, + /// Number of states that ended in an execution error. + pub errored_states: usize, + /// Number of completed states that did not hit a find predicate. + pub completed_states: usize, + /// Non-fatal diagnostics gathered during execution. + pub diagnostics: Vec, } /// Path explorer for symbolic execution. @@ -133,6 +261,18 @@ pub struct PathExplorer<'ctx> { config: ExploreConfig, /// Statistics. stats: ExploreStats, + /// Whether query helpers should prioritize states closer to their target. + target_guided_queries: bool, + /// Whether target-guided queries may first route through exception-dispatch bridge sites. + exception_bridge_guidance_enabled: bool, + /// Whether unsupported runtime loop summaries may produce residual states. + residual_runtime_loop_summaries_enabled: bool, + /// Derived helper summaries registered for direct-call targets. + derived_call_summaries: HashMap>, + /// Cached reverse-distance maps keyed by (function entry, target address). + target_distance_cache: HashMap<(u64, u64), HashMap>, + /// Candidate-generation tactics used during model extraction. + solve_tactic_config: SolveTacticConfig, } /// Statistics from path exploration. @@ -150,405 +290,2559 @@ pub struct ExploreStats { pub max_depth_reached: usize, /// Total execution time. pub total_time: Duration, + /// Whether exploration stopped because the timeout budget expired. + pub timed_out: bool, + /// Whether exploration stopped because the max_states budget was exhausted. + pub max_states_exhausted: bool, + /// Number of queued states discarded because another state subsumed them. + pub states_subsumed: usize, + /// Number of implication checks attempted for same-PC subsumption. + pub subsumption_checks: usize, + /// Number of implication checks that found a covering state. + pub subsumption_hits: usize, + /// Number of target-guided queue pops that differed from the base strategy order. + pub target_guided_reorders: usize, + /// Number of states dropped because their PC cannot reach the target in the CFG. + pub target_pruned_cfg_unreachable: usize, + /// Number of states dropped because an exact helper summary had no satisfiable case. + pub target_pruned_summary_contradiction: usize, + /// Number of target-guided states whose ranking/pruning used derived summary metadata. + pub target_summary_rank_hits: usize, + /// Number of states that reached the target PC but were unsatisfiable. + pub target_match_unsat: usize, + /// Number of states blocked by a missing exception handler. + pub runtime_missing_exception_handler: usize, + /// Number of states that entered runtime code without a materialized executable alias. + pub runtime_missing_materialized_code: usize, + /// Number of states that could not resume from an exception continuation. + pub runtime_missing_continuation_seed: usize, + /// Number of states that reached runtime code with unresolved provenance. + pub runtime_region_provenance_unknown: usize, + /// Number of symbolic runtime breakpoint dispatch candidates forked. + pub runtime_symbolic_breakpoint_forks: usize, + /// Number of symbolic runtime breakpoint candidates proven infeasible before enqueue. + pub runtime_symbolic_breakpoint_pruned: usize, + /// Number of residual summaries used to accelerate runtime breakpoint loops. + pub runtime_breakpoint_loop_summaries: usize, + /// Number of exact summaries used to accelerate runtime breakpoint loops. + pub runtime_breakpoint_loop_exact_summaries: usize, + /// Number of exact loop summaries derived by recurrence algebra. + pub runtime_loop_exact_recurrence_summaries: usize, + /// Canonical exact recurrence evidence derived by loop algebra. + pub runtime_loop_exact_recurrences: Vec, + /// Exact memory-fold recurrences derived by loop algebra. + pub runtime_loop_exact_folds: Vec, + /// Number of runtime loop summaries refused for soundness or missing context. + pub runtime_loop_refusals: usize, + /// Number of runtime loop candidates with unknown carried state. + pub runtime_loop_unknown_carried_state: usize, + /// Number of runtime loop candidates downgraded because exact iteration budget was exceeded. + pub runtime_loop_budget_residuals: usize, } -impl<'ctx> PathExplorer<'ctx> { - /// Create a new path explorer. - pub fn new(ctx: &'ctx Context) -> Self { - Self { - _ctx: ctx, - executor: SymExecutor::new(ctx), - solver: SymSolver::new(ctx), - config: ExploreConfig::default(), - stats: ExploreStats::default(), - } - } +#[derive(Clone)] +struct RegisteredDerivedCallSummary<'ctx> { + summary: Rc>, + callconv: CallConv, +} - /// Create a path explorer with configuration. - pub fn with_config(ctx: &'ctx Context, config: ExploreConfig) -> Self { - let solver = if let Some(timeout) = config.timeout { - SymSolver::with_timeout(ctx, timeout) - } else { - SymSolver::new(ctx) - }; +#[derive(Clone, Default)] +struct BlockSummaryRank { + has_summary: bool, + has_exact_summary: bool, + min_case_count: usize, +} + +struct TargetGuidanceContext { + target_addr: u64, + distances: HashMap, + reachable_blocks: HashSet, + call_targets_by_block: HashMap>, + block_summary_rank: HashMap, + allow_cross_function_states: bool, +} + +#[derive(Clone, Copy, Default)] +struct StateSummaryGuidance { + summary_hits: usize, + min_feasible_cases: usize, + contradictory: bool, +} + +#[derive(Clone, Copy)] +struct ResolvedBlock<'a> { + func: &'a SsaArtifact, + block: &'a r2ssa::FunctionSSABlock, + static_addr: u64, + runtime_addr: u64, + runtime_aliased: bool, +} + +struct StateWorklist<'ctx> { + strategy: ExploreStrategy, + ready: VecDeque, + same_pc: HashMap>, + slots: Vec>>, + live_states: usize, +} +impl<'ctx> StateWorklist<'ctx> { + fn new(strategy: ExploreStrategy) -> Self { Self { - _ctx: ctx, - executor: SymExecutor::new(ctx), - solver, - config, - stats: ExploreStats::default(), + strategy, + ready: VecDeque::new(), + same_pc: HashMap::new(), + slots: Vec::new(), + live_states: 0, } } - /// Get the exploration statistics. - pub fn stats(&self) -> &ExploreStats { - &self.stats + fn push(&mut self, state: SymState<'ctx>) -> usize { + let id = self.slots.len(); + let pc = state.pc; + self.slots.push(Some(state)); + self.ready.push_back(id); + self.same_pc.entry(pc).or_default().push_back(id); + self.live_states += 1; + id } - /// Get the solver for additional queries. - pub fn solver(&self) -> &SymSolver<'ctx> { - &self.solver + fn state(&self, id: usize) -> Option<&SymState<'ctx>> { + self.slots.get(id)?.as_ref() } - /// Register a call hook for a concrete target address. - pub fn register_call_hook(&mut self, addr: u64, hook: F) - where - F: Fn(&mut SymState<'ctx>) -> crate::executor::CallHookResult + 'ctx, - { - self.executor - .register_call_hook(addr, move |state| Ok(hook(state))); + fn same_pc_ids(&self, pc: u64) -> Vec { + self.same_pc + .get(&pc) + .map(|bucket| bucket.iter().copied().collect()) + .unwrap_or_default() } - /// Solve a path's constraints and extract concrete values. - /// - /// Returns None if the path is infeasible. - pub fn solve_path(&self, path: &PathResult<'ctx>) -> Option { - if !path.feasible { - return None; - } - - // Get a model from the solver - let model = self.solver.solve(&path.state)?; + fn remove_slot(&mut self, id: usize) -> Option> { + self.take_slot(id) + } - let mut solved = SolvedPath { - final_pc: path.state.pc, - num_constraints: path.state.num_constraints(), - ..Default::default() - }; + fn pop_next(&mut self) -> Option> { + loop { + let id = match self.strategy { + ExploreStrategy::Dfs => self.ready.pop_back(), + ExploreStrategy::Bfs => self.ready.pop_front(), + ExploreStrategy::Random => { + if self.ready.is_empty() { + None + } else if self.live_states.is_multiple_of(2) { + self.ready.pop_front() + } else { + self.ready.pop_back() + } + } + }?; - // Extract concrete register values - for (name, value) in path.state.registers() { - if let Some(concrete) = model.eval(value) { - solved.registers.insert(name.clone(), concrete); + if let Some(state) = self.take_slot(id) { + return Some(state); } } + } - // Include explicitly tracked symbolic inputs. - for (name, value) in path.state.symbolic_inputs() { - if let Some(concrete) = model.eval(value) { - solved.inputs.entry(name.clone()).or_insert(concrete); + fn default_candidate(&mut self) -> Option { + match self.strategy { + ExploreStrategy::Dfs => { + while let Some(id) = self.ready.back().copied() { + if self.state(id).is_some() { + return Some(id); + } + self.ready.pop_back(); + } + None } - } - - // Try to identify symbolic inputs (variables starting with "sym_"). - for (name, value) in path.state.registers() { - if solved.inputs.contains_key(name) { - continue; + ExploreStrategy::Bfs => { + while let Some(id) = self.ready.front().copied() { + if self.state(id).is_some() { + return Some(id); + } + self.ready.pop_front(); + } + None } - if (name.starts_with("sym_") || value.is_symbolic()) - && let Some(concrete) = model.eval(value) - { - solved.inputs.insert(name.clone(), concrete); + ExploreStrategy::Random => { + if self.ready.is_empty() { + return None; + } + if self.live_states.is_multiple_of(2) { + while let Some(id) = self.ready.front().copied() { + if self.state(id).is_some() { + return Some(id); + } + self.ready.pop_front(); + } + } else { + while let Some(id) = self.ready.back().copied() { + if self.state(id).is_some() { + return Some(id); + } + self.ready.pop_back(); + } + } + None } } + } - // Extract tracked symbolic memory buffers. - for region in path.state.symbolic_memory() { - if let Some(bytes) = model.eval_bytes(®ion.value, region.size as usize) { - solved.memory.insert(region.name.clone(), bytes); + fn take_same_pc(&mut self, pc: u64) -> Option> { + loop { + let id = { + let bucket = self.same_pc.get_mut(&pc)?; + match self.strategy { + ExploreStrategy::Dfs => bucket.pop_back(), + ExploreStrategy::Bfs => bucket.pop_front(), + ExploreStrategy::Random => { + if bucket.is_empty() { + None + } else if self.live_states.is_multiple_of(2) { + bucket.pop_front() + } else { + bucket.pop_back() + } + } + } + }?; + + let remove_bucket = self.same_pc.get(&pc).is_some_and(VecDeque::is_empty); + if remove_bucket { + self.same_pc.remove(&pc); } - } - Some(solved) + if let Some(state) = self.take_slot(id) { + return Some(state); + } + } } - /// Solve all feasible paths and return concrete solutions. - pub fn solve_all_paths(&self, paths: &[PathResult<'ctx>]) -> Vec> { - paths.iter().map(|p| self.solve_path(p)).collect() + fn take_slot(&mut self, id: usize) -> Option> { + let slot = self.slots.get_mut(id)?; + let state = slot.take()?; + self.live_states = self.live_states.saturating_sub(1); + Some(state) } +} - /// Explore all paths in a function. - pub fn explore( - &mut self, - func: &SsaArtifact, - initial_state: SymState<'ctx>, - ) -> Vec> { - let start_time = Instant::now(); - let mut results = Vec::new(); - let mut worklist: VecDeque> = VecDeque::new(); - worklist.push_back(initial_state); +#[derive(Clone, Debug, Eq, PartialEq)] +struct TargetGuidedQueueEntry { + rank: ( + usize, + usize, + usize, + usize, + usize, + usize, + usize, + usize, + usize, + usize, + ), + id: usize, +} - while let Some(mut state) = self.next_state(&mut worklist) { - if self.config.merge_states - && let Some(other) = take_merge_candidate(&mut worklist, state.pc) - { - state = state.merge_with(&other); - } - // Check timeout - if let Some(timeout) = self.config.timeout - && start_time.elapsed() > timeout - { - break; - } +impl Ord for TargetGuidedQueueEntry { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.rank + .cmp(&other.rank) + .then_with(|| self.id.cmp(&other.id)) + } +} - // Check state limit - if self.stats.states_explored >= self.config.max_states { - break; - } +impl PartialOrd for TargetGuidedQueueEntry { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} - self.stats.states_explored += 1; +enum DriverAction<'ctx> { + Continue(Box>), + Skip, + Finish, +} - // Check depth limit - if state.depth >= self.config.max_depth { - state.terminate(ExitStatus::MaxDepth); - self.stats.paths_max_depth += 1; - results.push(PathResult::new(state, true)); - continue; - } +enum DriverMode<'ctx> { + Explore { + results: Vec>, + }, + FindFirst { + target_addr: u64, + require_feasible: bool, + found: Option>, + }, + FindAll { + target_addr: u64, + require_feasible: bool, + matches: Vec>, + }, + Avoid { + avoid_set: HashSet, + found: Option>, + }, + Spec { + find_set: HashSet, + avoid_set: HashSet, + max_finds: usize, + result: SpecExploreResult<'ctx>, + }, +} - // Check feasibility - if self.config.prune_infeasible && !self.solver.is_sat(&state) { - self.stats.paths_pruned += 1; - continue; - } +impl<'ctx> DriverMode<'ctx> { + fn explore_path_cap_reached(&self, max_completed_paths: Option) -> bool { + let Some(limit) = max_completed_paths else { + return false; + }; + match self { + DriverMode::Explore { results } => results.len() >= limit, + _ => false, + } + } - // Get current block - let block_addr = state.pc; - let Some(block) = func.get_block(block_addr) else { - // No block at this address - path ends - state.terminate(ExitStatus::Return); - results.push(PathResult::new(state, true)); - self.stats.paths_completed += 1; - continue; - }; + fn on_timeout(&mut self) -> bool { + if let DriverMode::Spec { result, .. } = self { + result.diagnostics.push("exploration timed out".to_string()); + } + true + } - // Execute the block - match self.executor.execute_block(&mut state, block) { - Ok(forked_states) => { - // Add forked states to worklist - for mut forked in forked_states { - forked.set_prev_pc(Some(block_addr)); - worklist.push_back(forked); - } + fn on_max_states(&mut self) -> bool { + if let DriverMode::Spec { result, .. } = self { + result + .diagnostics + .push("exploration stopped at max_states budget".to_string()); + } + true + } - // Update max depth before potentially moving state - if state.depth > self.stats.max_depth_reached { - self.stats.max_depth_reached = state.depth; - } + fn on_unsat_pruned(&mut self) { + if let DriverMode::Spec { result, .. } = self { + result.unsat_states += 1; + } + } - // Check if state terminated - if state.is_terminated() { - let feasible = self.solver.is_sat(&state); - results.push(PathResult::new(state, feasible)); - self.stats.paths_completed += 1; + fn on_state_popped( + &mut self, + explorer: &mut PathExplorer<'ctx>, + state: SymState<'ctx>, + ) -> DriverAction<'ctx> { + match self { + DriverMode::Explore { .. } => DriverAction::Continue(Box::new(state)), + DriverMode::FindFirst { + target_addr, + require_feasible, + found, + } => { + if explorer.state_matches_target(&state, *target_addr) { + if !*require_feasible || explorer.solver.is_sat(&state) { + *found = Some(PathResult::new(state, true)); + DriverAction::Finish } else { - // Continue exploring this path - // Update PC to next block if not changed by control flow - if state.pc == block_addr - && let Some(next) = self.fallthrough_target(func, block_addr) - { - state.pc = next; - } - state.set_prev_pc(Some(block_addr)); - worklist.push_back(state); + debug_log_target_unsat_state(&state, *target_addr); + explorer.stats.target_match_unsat += 1; + DriverAction::Skip } + } else { + DriverAction::Continue(Box::new(state)) } - Err(e) => { - // Update max depth before moving state - if state.depth > self.stats.max_depth_reached { - self.stats.max_depth_reached = state.depth; + } + DriverMode::FindAll { + target_addr, + require_feasible, + matches, + } => { + if explorer.state_matches_target(&state, *target_addr) { + if !*require_feasible || explorer.solver.is_sat(&state) { + explorer.record_depth(state.depth); + explorer.stats.paths_completed += 1; + matches.push(PathResult::new(state, true)); + } else { + debug_log_target_unsat_state(&state, *target_addr); + explorer.stats.target_match_unsat += 1; } - state.terminate(ExitStatus::Error(format!("{}", e))); - results.push(PathResult::new(state, false)); - self.stats.paths_completed += 1; + DriverAction::Skip + } else { + DriverAction::Continue(Box::new(state)) } } - } - - self.stats.total_time = start_time.elapsed(); - results - } - - fn fallthrough_target(&self, func: &SsaArtifact, block_addr: u64) -> Option { - let block = func.cfg().get_block(block_addr)?; - match block.terminator { - BlockTerminator::Fallthrough { next } => Some(next), - BlockTerminator::ConditionalBranch { false_target, .. } => Some(false_target), - BlockTerminator::Call { fallthrough, .. } => fallthrough, - BlockTerminator::IndirectCall { fallthrough } => fallthrough, - BlockTerminator::Branch { target } => Some(target), - _ => None, - } - } - - /// Get the next state from the worklist based on strategy. - fn next_state(&self, worklist: &mut VecDeque>) -> Option> { - match self.config.strategy { - ExploreStrategy::Dfs => worklist.pop_back(), - ExploreStrategy::Bfs => worklist.pop_front(), - ExploreStrategy::Random => { - if worklist.is_empty() { - None + DriverMode::Avoid { avoid_set, .. } => { + if explorer.state_hits_any_target(&state, avoid_set) { + DriverAction::Skip } else { - // Simple random: alternate between front and back - if worklist.len().is_multiple_of(2) { - worklist.pop_front() + DriverAction::Continue(Box::new(state)) + } + } + DriverMode::Spec { + find_set, + avoid_set, + max_finds, + result, + } => { + if explorer.state_hits_any_target(&state, avoid_set) { + result.avoided_states += 1; + return DriverAction::Skip; + } + if explorer.state_hits_any_target(&state, find_set) { + if explorer.solver.is_sat(&state) { + explorer.record_depth(state.depth); + explorer.stats.paths_completed += 1; + result.found_paths.push(PathResult::new(state, true)); + if result.found_paths.len() >= *max_finds { + return DriverAction::Finish; + } } else { - worklist.pop_back() + explorer.stats.paths_pruned += 1; + result.unsat_states += 1; } + DriverAction::Skip + } else { + DriverAction::Continue(Box::new(state)) } } } } - /// Explore paths to find inputs that reach a target address. - pub fn find_path_to( + fn on_depth_limit( &mut self, - func: &SsaArtifact, - initial_state: SymState<'ctx>, - target_addr: u64, - ) -> Option> { - let start_time = Instant::now(); - let mut worklist: VecDeque> = VecDeque::new(); - worklist.push_back(initial_state); - - while let Some(mut state) = self.next_state(&mut worklist) { - if self.config.merge_states - && let Some(other) = take_merge_candidate(&mut worklist, state.pc) - { - state = state.merge_with(&other); + explorer: &mut PathExplorer<'ctx>, + mut state: SymState<'ctx>, + max_completed_paths: Option, + ) -> DriverAction<'ctx> { + match self { + DriverMode::Explore { results } => { + state.terminate(ExitStatus::MaxDepth); + explorer.record_depth(state.depth); + explorer.stats.paths_max_depth += 1; + results.push(PathResult::new(state, true)); + if self.explore_path_cap_reached(max_completed_paths) { + DriverAction::Finish + } else { + DriverAction::Skip + } } - // Check timeout - if let Some(timeout) = self.config.timeout - && start_time.elapsed() > timeout - { - break; + DriverMode::FindFirst { .. } => DriverAction::Skip, + DriverMode::FindAll { .. } => { + explorer.stats.paths_max_depth += 1; + DriverAction::Skip } - - // Check if we reached the target - if state.pc == target_addr { - let feasible = self.solver.is_sat(&state); - if feasible { - return Some(PathResult::new(state, true)); + DriverMode::Avoid { found, .. } => { + if explorer.solver.is_sat(&state) { + *found = Some(PathResult::new(state, true)); + DriverAction::Finish + } else { + DriverAction::Skip } - continue; } + DriverMode::Spec { result, .. } => { + explorer.stats.paths_max_depth += 1; + result.completed_states += 1; + DriverAction::Skip + } + } + } - // Check limits - if self.stats.states_explored >= self.config.max_states { - break; + fn on_missing_block( + &mut self, + explorer: &mut PathExplorer<'ctx>, + mut state: SymState<'ctx>, + block_addr: u64, + max_completed_paths: Option, + ) -> DriverAction<'ctx> { + explorer.record_runtime_missing_block(&state, block_addr); + match self { + DriverMode::Explore { results } => { + state.terminate(ExitStatus::Return); + results.push(PathResult::new(state, true)); + explorer.stats.paths_completed += 1; + if self.explore_path_cap_reached(max_completed_paths) { + DriverAction::Finish + } else { + DriverAction::Skip + } } - if state.depth >= self.config.max_depth { - continue; + DriverMode::FindFirst { .. } | DriverMode::FindAll { .. } => DriverAction::Skip, + DriverMode::Avoid { found, .. } => { + if explorer.solver.is_sat(&state) { + *found = Some(PathResult::new(state, true)); + DriverAction::Finish + } else { + DriverAction::Skip + } + } + DriverMode::Spec { result, .. } => { + result + .diagnostics + .push(format!("no SSA block at 0x{block_addr:x}")); + result.completed_states += 1; + explorer.stats.paths_completed += 1; + DriverAction::Skip } + } + } - self.stats.states_explored += 1; + fn on_terminated_state( + &mut self, + explorer: &mut PathExplorer<'ctx>, + state: SymState<'ctx>, + block_addr: u64, + max_completed_paths: Option, + ) -> DriverAction<'ctx> { + explorer.record_runtime_exit_status(&state.exit_status); + match self { + DriverMode::Explore { results } => { + explorer.record_depth(state.depth); + let feasible = explorer.solver.is_sat(&state); + results.push(PathResult::new(state, feasible)); + explorer.stats.paths_completed += 1; + if self.explore_path_cap_reached(max_completed_paths) { + return DriverAction::Finish; + } + } + DriverMode::FindFirst { .. } + | DriverMode::FindAll { .. } + | DriverMode::Avoid { .. } => {} + DriverMode::Spec { result, .. } => { + explorer.stats.paths_completed += 1; + result.diagnostics.push(format!( + "state terminated at 0x{:x} with {:?}", + block_addr, state.exit_status + )); + match &state.exit_status { + Some(ExitStatus::Error(_)) | Some(ExitStatus::RuntimeBlocked(_)) => { + result.errored_states += 1 + } + _ => result.completed_states += 1, + } + } + } + DriverAction::Skip + } + + fn on_execute_error( + &mut self, + explorer: &mut PathExplorer<'ctx>, + mut state: SymState<'ctx>, + block_addr: u64, + error: String, + max_completed_paths: Option, + ) -> DriverAction<'ctx> { + match self { + DriverMode::Explore { results } => { + explorer.record_depth(state.depth); + state.terminate(ExitStatus::Error(error)); + results.push(PathResult::new(state, false)); + explorer.stats.paths_completed += 1; + if self.explore_path_cap_reached(max_completed_paths) { + return DriverAction::Finish; + } + } + DriverMode::FindAll { .. } => { + explorer.stats.paths_completed += 1; + } + DriverMode::Spec { result, .. } => { + explorer.stats.paths_completed += 1; + result.errored_states += 1; + result + .diagnostics + .push(format!("execution error at 0x{block_addr:x}: {error}")); + } + DriverMode::FindFirst { .. } | DriverMode::Avoid { .. } => {} + } + DriverAction::Skip + } + + fn allow_enqueue(&mut self, state: &SymState<'ctx>) -> bool { + match self { + DriverMode::Avoid { .. } => true, + DriverMode::Spec { + avoid_set, result, .. + } => { + let static_pc = state.resolve_runtime_pc(state.pc).unwrap_or(state.pc); + if avoid_set.contains(&state.pc) || avoid_set.contains(&static_pc) { + result.avoided_states += 1; + false + } else { + true + } + } + _ => true, + } + } +} + +impl<'ctx> PathExplorer<'ctx> { + fn state_matches_target(&self, state: &SymState<'ctx>, target_addr: u64) -> bool { + state.pc == target_addr || self.effective_static_pc(state) == target_addr + } + + fn state_hits_any_target(&self, state: &SymState<'ctx>, targets: &HashSet) -> bool { + targets.contains(&state.pc) || targets.contains(&self.effective_static_pc(state)) + } + + fn max_inline_runahead_depth_delta(&self) -> usize { + self.config + .max_depth + .saturating_mul(64) + .max(self.config.max_depth) + } + + fn finalize_active_state_after_block( + &self, + func: &SsaArtifact, + state: &mut SymState<'ctx>, + block_runtime_addr: u64, + block_static_addr: u64, + ) { + state.pc = state + .remap_static_pc_to_runtime(state.pc) + .unwrap_or(state.pc); + if state.pc == block_runtime_addr + && let Some(next) = self.fallthrough_target(func, block_static_addr) + { + state.pc = state.remap_static_pc_to_runtime(next).unwrap_or(next); + } + state.set_prev_pc(Some(block_runtime_addr)); + } + + fn patch_pending_exception_resume_pc( + &self, + func: &SsaArtifact, + state: &mut SymState<'ctx>, + block_static_addr: u64, + ) { + let Some(pending) = state.pending_exception() else { + return; + }; + if state.pc != pending.handler_addr { + return; + } + let Some(resume_static_pc) = self.fallthrough_target(func, block_static_addr) else { + return; + }; + let resume_pc = state + .remap_static_pc_to_runtime(resume_static_pc) + .unwrap_or(resume_static_pc); + let _ = state.set_pending_exception_resume_pc(resume_pc); + } + + fn record_runtime_block_reason(&mut self, reason: RuntimeBlockReason) { + match reason { + RuntimeBlockReason::MissingExceptionHandler => { + self.stats.runtime_missing_exception_handler += 1; + } + RuntimeBlockReason::MissingRuntimeMaterializedCode => { + self.stats.runtime_missing_materialized_code += 1; + } + RuntimeBlockReason::MissingContinuationSeed => { + self.stats.runtime_missing_continuation_seed += 1; + } + RuntimeBlockReason::RuntimeRegionProvenanceUnknown => { + self.stats.runtime_region_provenance_unknown += 1; + } + } + } + + fn record_runtime_exit_status(&mut self, status: &Option) { + if let Some(ExitStatus::RuntimeBlocked(reason)) = status { + self.record_runtime_block_reason(*reason); + } + } + + fn record_runtime_missing_block(&mut self, state: &SymState<'ctx>, block_addr: u64) { + if let Some(region) = state.runtime_region_for_pc(block_addr) { + if !region.executable || region.source_base.is_none() { + self.record_runtime_block_reason( + RuntimeBlockReason::MissingRuntimeMaterializedCode, + ); + } else { + self.record_runtime_block_reason( + RuntimeBlockReason::RuntimeRegionProvenanceUnknown, + ); + } + } + } + + fn runtime_continuation_candidate_targets( + &self, + root: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + state: &SymState<'ctx>, + ) -> Vec { + let mut candidates = BTreeSet::new(); + let mut collect = |function: &SsaArtifact| { + for static_pc in function.cfg().block_addrs() { + if let Some(runtime_pc) = state.remap_static_pc_to_runtime(static_pc) { + candidates.insert(runtime_pc); + } + } + }; + collect(root); + if let Some(scope) = scope { + for function in scope.functions().values() { + collect(&function.prepared); + } + } + candidates + .into_iter() + .take(SYMBOLIC_CONTINUATION_MAX_TARGETS) + .collect() + } + + fn fork_symbolic_exception_resume_targets( + &mut self, + root: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + state: &SymState<'ctx>, + ) -> Vec> { + if !matches!( + &state.exit_status, + Some(ExitStatus::RuntimeBlocked( + RuntimeBlockReason::MissingContinuationSeed + )) + ) { + return Vec::new(); + } + let Some(pending) = state.pending_exception() else { + return Vec::new(); + }; + let rip = state.mem_read( + &crate::SymValue::concrete(pending.context_addr.saturating_add(0xf8), 64), + 8, + ); + if rip.as_concrete().is_some() || rip.is_unknown() { + return Vec::new(); + } + + let mut resumed = Vec::new(); + let candidates = self.runtime_continuation_candidate_targets(root, scope, state); + let candidates_checked = candidates.len(); + for candidate in candidates { + let candidate_value = crate::SymValue::concrete(candidate, rip.bits()); + if !self.solver.can_be_equal(state, &rip, &candidate_value) { + continue; + } + let mut forked = state.fork(); + forked.active = true; + forked.exit_status = None; + forked.constrain_eq(&rip, candidate); + forked.pc = candidate; + forked.set_prev_pc(None); + forked.clear_pending_exception(); + resumed.push(forked); + } + if !resumed.is_empty() { + debug_target_guidance_log(&format!( + "symbolic_exception_resume_forks count={} candidates_checked={}", + resumed.len(), + candidates_checked + )); + } + resumed + } + + fn resolve_scope_function<'a>( + &self, + root: &'a SsaArtifact, + scope: Option<&'a PreparedFunctionScope>, + pc: u64, + ) -> Option<&'a SsaArtifact> { + if root.get_block(pc).is_some() { + return Some(root); + } + scope + .and_then(|scope| scope.function_containing_block(pc)) + .map(|function| &function.prepared) + } + + fn exception_bridge_guidance_target( + &mut self, + root: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + target_addr: u64, + ) -> Option { + if !self.exception_bridge_guidance_enabled { + return None; + } + let scope = scope?; + let target_func = self.resolve_scope_function(root, Some(scope), target_addr); + if target_func.is_some_and(|target_func| target_func.entry == root.entry) { + let distances = self.target_distance_map(root, target_addr); + if distances.contains_key(&root.entry) { + return None; + } + } + + let observations = observe_call_arguments(root, &AbiProfile::windows_x64()); + let mut registration_sites = Vec::new(); + let mut raise_sites = Vec::new(); + + for (call_id, call) in &root.call_sites().by_id { + let Some(target) = root.resolved_call_target(call) else { + continue; + }; + match self.executor.call_hook_tag(target) { + Some(crate::executor::CallHookTag::WindowsAddVectoredExceptionHandler) => { + if observations.contains_key(call_id) { + if let Some((block_addr, _)) = root.inst_op_site(call.at) { + registration_sites.push(block_addr); + } else { + registration_sites.push(0); + } + } + } + Some(crate::executor::CallHookTag::WindowsRaiseException) => { + if let Some((block_addr, _)) = root.inst_op_site(call.at) { + raise_sites.push(block_addr); + } + } + _ => {} + } + } + + raise_sites.sort_unstable(); + raise_sites.dedup(); + registration_sites.sort_unstable(); + registration_sites.dedup(); + + if raise_sites.is_empty() || registration_sites.is_empty() { + return None; + } + + raise_sites.into_iter().next() + } + + pub(crate) fn exception_bridge_target_in_scope( + &mut self, + root: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + target_addr: u64, + ) -> Option { + self.exception_bridge_guidance_target(root, scope, target_addr) + } + + pub(crate) fn advance_current_block_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + mut state: SymState<'ctx>, + ) -> Result>, String> { + let mut active_states = Vec::new(); + if let Some(dispatched) = self.dispatch_runtime_breakpoint(&mut state) { + active_states.push(dispatched); + } + let Some(block) = self.resolve_block(func, scope, &state) else { + let missing_pc = state.pc; + self.record_runtime_missing_block(&state, missing_pc); + return Err(format!("missing block at 0x{missing_pc:x}")); + }; + let block_addr = block.runtime_addr; + let block_static_addr = block.static_addr; + let block_func = block.func; + + let mut enqueue_forked_state = |explorer: &mut Self, mut forked: SymState<'ctx>| { + explorer.remap_state_pc_after_block(&mut forked, block); + forked.set_prev_pc(Some(block_addr)); + if forked.is_terminated() { + explorer.record_runtime_exit_status(&forked.exit_status); + explorer.stats.paths_completed += 1; + } else { + active_states.push(forked); + } + }; + + match self.executor.execute_block(&mut state, block.block) { + Ok(forked_states) => { + self.record_depth(state.depth); + for forked in forked_states { + enqueue_forked_state(self, forked); + } + if state.is_terminated() { + self.record_runtime_exit_status(&state.exit_status); + self.stats.paths_completed += 1; + } else { + self.patch_pending_exception_resume_pc( + block_func, + &mut state, + block_static_addr, + ); + self.finalize_active_state_after_block( + block_func, + &mut state, + block_addr, + block_static_addr, + ); + active_states.push(state); + } + Ok(active_states) + } + Err(error) => Err(error.to_string()), + } + } + + fn resolve_block<'a>( + &self, + func: &'a SsaArtifact, + scope: Option<&'a PreparedFunctionScope>, + state: &SymState<'ctx>, + ) -> Option> { + if let Some(block) = func.get_block(state.pc) { + return Some(ResolvedBlock { + func, + block, + static_addr: state.pc, + runtime_addr: state.pc, + runtime_aliased: false, + }); + } + if let Some(scope_func) = self.resolve_scope_function(func, scope, state.pc) + && let Some(block) = scope_func.get_block(state.pc) + { + return Some(ResolvedBlock { + func: scope_func, + block, + static_addr: state.pc, + runtime_addr: state.pc, + runtime_aliased: false, + }); + } + let static_addr = state.resolve_runtime_pc(state.pc)?; + let scope_func = self.resolve_scope_function(func, scope, static_addr)?; + let block = scope_func.get_block(static_addr)?; + Some(ResolvedBlock { + func: scope_func, + block, + static_addr, + runtime_addr: state.pc, + runtime_aliased: true, + }) + } + + fn effective_static_pc(&self, state: &SymState<'ctx>) -> u64 { + state.resolve_runtime_pc(state.pc).unwrap_or(state.pc) + } + + fn dispatch_runtime_breakpoint( + &mut self, + state: &mut SymState<'ctx>, + ) -> Option> { + if state.dispatch_runtime_breakpoint_if_ready() { + return None; + } + if let Some(breakpoint) = &state.runtime().active_breakpoint + && breakpoint.breakpoint.as_concrete().is_none() + { + let pc = crate::SymValue::concrete(state.pc, breakpoint.breakpoint.bits()); + if !self.solver.can_be_equal(state, &breakpoint.breakpoint, &pc) { + self.stats.runtime_symbolic_breakpoint_pruned += 1; + return None; + } + } + let dispatched = state.fork_symbolic_runtime_breakpoint_at(state.pc); + if dispatched.is_some() { + self.stats.runtime_symbolic_breakpoint_forks += 1; + } + dispatched + } + + fn summarize_runtime_breakpoint_loop( + &mut self, + root: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + block_func: &SsaArtifact, + block: &r2ssa::FunctionSSABlock, + state: &SymState<'ctx>, + ) -> Option> { + let branch = loops::runtime_counter_threshold_branch(block)?; + if !self.residual_runtime_loop_summaries_enabled + && let Some(exact) = + self.exact_runtime_breakpoint_loop_runahead(root, scope, block, state, &branch) + { + let summary = loops::bounded_exact_summary(block.addr, &branch, 0, exact); + self.stats.runtime_breakpoint_loop_exact_summaries += 1; + debug_target_guidance_log(&format!( + "runtime_loop_exact_summary target=0x{:x} from=0x{:x}", + branch.target, block.addr + )); + return summary.resulting_state; + } + let summary = loops::summarize_residual_runtime_loop( + self._ctx, + block_func, + block, + state, + &branch, + self.residual_runtime_loop_summaries_enabled, + ); + match summary.kind { + LoopSummaryKind::Exact | LoopSummaryKind::BoundedExact => { + self.stats.runtime_breakpoint_loop_exact_summaries += 1; + if summary.kind == LoopSummaryKind::Exact { + self.stats.runtime_loop_exact_recurrence_summaries += 1; + } + self.record_exact_loop_recurrences(summary.exact_recurrences.iter().cloned()); + summary.resulting_state + } + LoopSummaryKind::Residual => { + for reason in &summary.reasons { + if reason == "runtime_loop_unknown_carried_state" { + self.stats.runtime_loop_unknown_carried_state += 1; + } + if reason == "runtime_loop_iteration_budget" { + self.stats.runtime_loop_budget_residuals += 1; + } + } + if summary.resulting_state.is_some() { + self.stats.runtime_breakpoint_loop_summaries += 1; + } + debug_target_guidance_log(&format!( + "runtime_loop_summary kind={:?} target={} from=0x{:x} iterations={} carried={} reasons={}", + summary.kind, + summary + .exit_target + .map(|target| format!("0x{target:x}")) + .unwrap_or_else(|| "none".to_string()), + summary.header, + summary + .iterations + .map(|iterations| iterations.to_string()) + .unwrap_or_else(|| "unknown".to_string()), + summary.carried_state.len(), + summary.reasons.join(","), + )); + summary.resulting_state + } + LoopSummaryKind::Refused => { + self.stats.runtime_loop_refusals += 1; + debug_target_guidance_log(&format!( + "runtime_loop_summary_refused block=0x{:x} reasons={}", + summary.header, + summary.reasons.join(","), + )); + None + } + } + } + + fn record_exact_loop_recurrences( + &mut self, + recurrences: impl IntoIterator, + ) { + const MAX_RECORDED_EXACT_RECURRENCES: usize = 128; + for recurrence in recurrences { + if self.stats.runtime_loop_exact_recurrences.len() >= MAX_RECORDED_EXACT_RECURRENCES { + break; + } + if !self + .stats + .runtime_loop_exact_recurrences + .contains(&recurrence) + { + self.stats.runtime_loop_exact_recurrences.push(recurrence); + } + } + self.stats + .runtime_loop_exact_recurrences + .sort_by(|lhs, rhs| { + ( + lhs.header, + lhs.exit_target, + lhs.accumulator.as_str(), + format!("{:?}", lhs.kind), + ) + .cmp(&( + rhs.header, + rhs.exit_target, + rhs.accumulator.as_str(), + format!("{:?}", rhs.kind), + )) + }); + let folds = self + .stats + .runtime_loop_exact_recurrences + .iter() + .filter_map(ExactLoopRecurrenceEvidence::as_fold) + .collect::>(); + self.record_exact_loop_folds(folds); + } + + fn record_exact_loop_folds(&mut self, folds: impl IntoIterator) { + const MAX_RECORDED_EXACT_FOLDS: usize = 128; + for fold in folds { + if self.stats.runtime_loop_exact_folds.len() >= MAX_RECORDED_EXACT_FOLDS { + break; + } + if !self.stats.runtime_loop_exact_folds.contains(&fold) { + self.stats.runtime_loop_exact_folds.push(fold); + } + } + self.stats.runtime_loop_exact_folds.sort_by(|lhs, rhs| { + ( + lhs.header, + lhs.exit_target, + lhs.accumulator.as_str(), + lhs.term.addr.as_str(), + ) + .cmp(&( + rhs.header, + rhs.exit_target, + rhs.accumulator.as_str(), + rhs.term.addr.as_str(), + )) + }); + } + + fn exact_runtime_breakpoint_loop_runahead( + &mut self, + root: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + block: &r2ssa::FunctionSSABlock, + state: &SymState<'ctx>, + branch: &loops::RuntimeLoopBranch, + ) -> Option> { + if state.pending_exception().is_none() || state.runtime().runtime_regions.is_empty() { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=missing_runtime_context", + block.addr + )); + return None; + } + let mut target_probe = state.fork(); + target_probe.pc = branch.target; + if self.resolve_block(root, scope, &target_probe).is_none() { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=missing_target target=0x{:x}", + block.addr, branch.target + )); + return None; + } + let Some(counter) = loops::concrete_state_var_at_block_entry(state, block, &branch.counter) + else { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=unknown_counter counter={}", + block.addr, + branch.counter.display_name() + )); + return None; + }; + let Some(remaining) = branch.threshold.checked_sub(counter) else { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=counter_past_threshold counter={} threshold={}", + block.addr, counter, branch.threshold + )); + return None; + }; + if !(8..=EXACT_RUNTIME_LOOP_MAX_ITERS).contains(&remaining) { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=iteration_budget remaining={} threshold={}", + block.addr, remaining, branch.threshold + )); + return None; + } + + let mut runahead = state.fork(); + let mut guard_visits = 0u64; + for block_step in 0..EXACT_RUNTIME_LOOP_MAX_BLOCK_STEPS { + if let Some(dispatched) = self.dispatch_runtime_breakpoint(&mut runahead) { + runahead = dispatched; + } + let static_pc = self.effective_static_pc(&runahead); + if static_pc == branch.target { + debug_target_guidance_log(&format!( + "runtime_loop_exact_summary target=0x{:x} from=0x{:x} guard_visits={} block_steps={}", + branch.target, block.addr, guard_visits, block_step + )); + return Some(runahead); + } + if runahead.is_terminated() { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=terminated block_steps={} status={:?}", + block.addr, block_step, runahead.exit_status + )); + return None; + } + if static_pc == block.addr { + guard_visits = guard_visits.saturating_add(1); + if guard_visits > remaining.saturating_add(1) { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=guard_visit_budget visits={} remaining={}", + block.addr, guard_visits, remaining + )); + return None; + } + } + let Some(resolved) = self.resolve_block(root, scope, &runahead) else { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=missing_block static_pc=0x{:x} runtime_pc=0x{:x}", + block.addr, static_pc, runahead.pc + )); + return None; + }; + let forked = self + .executor + .execute_block(&mut runahead, resolved.block) + .ok()?; + if !forked.is_empty() { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=forked block_steps={} forks={}", + block.addr, + block_step, + forked.len() + )); + return None; + } + if runahead.is_terminated() { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=terminated block_steps={} status={:?}", + block.addr, block_step, runahead.exit_status + )); + return None; + } + self.patch_pending_exception_resume_pc( + resolved.func, + &mut runahead, + resolved.static_addr, + ); + self.finalize_active_state_after_block( + resolved.func, + &mut runahead, + resolved.runtime_addr, + resolved.static_addr, + ); + } + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=block_step_budget budget={}", + block.addr, EXACT_RUNTIME_LOOP_MAX_BLOCK_STEPS + )); + None + } + + fn direct_call_fork_targets( + &self, + root: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + block: &r2ssa::FunctionSSABlock, + ) -> Option> { + let targets = block + .ops + .iter() + .filter_map(|op| match op { + SSAOp::Call { target } => loops::parse_address_var(target), + _ => None, + }) + .filter(|target| self.executor.call_hook_tag(*target).is_none()) + .filter(|target| self.resolve_scope_function(root, scope, *target).is_some()) + .collect::>(); + (!targets.is_empty()).then_some(targets) + } + + fn remap_state_pc_after_block(&self, state: &mut SymState<'ctx>, block: ResolvedBlock<'_>) { + if !block.runtime_aliased || state.pc == block.runtime_addr { + return; + } + if let Some(runtime_pc) = state.remap_static_pc_to_runtime(state.pc) { + state.pc = runtime_pc; + } + } + + /// Create a new path explorer. + pub fn new(ctx: &'ctx Context) -> Self { + Self { + _ctx: ctx, + executor: SymExecutor::new(ctx), + solver: SymSolver::new(ctx), + config: ExploreConfig::default(), + stats: ExploreStats::default(), + target_guided_queries: false, + exception_bridge_guidance_enabled: true, + residual_runtime_loop_summaries_enabled: true, + derived_call_summaries: HashMap::new(), + target_distance_cache: HashMap::new(), + solve_tactic_config: SolveTacticConfig::default(), + } + } + + /// Create a path explorer with configuration. + pub fn with_config(ctx: &'ctx Context, config: ExploreConfig) -> Self { + let solver = if let Some(timeout) = config.timeout { + SymSolver::with_timeout(ctx, timeout) + } else { + SymSolver::new(ctx) + }; + + Self { + _ctx: ctx, + executor: SymExecutor::new(ctx), + solver, + config, + stats: ExploreStats::default(), + target_guided_queries: false, + exception_bridge_guidance_enabled: true, + residual_runtime_loop_summaries_enabled: true, + derived_call_summaries: HashMap::new(), + target_distance_cache: HashMap::new(), + solve_tactic_config: SolveTacticConfig::default(), + } + } + + /// Get the exploration statistics. + pub fn stats(&self) -> &ExploreStats { + &self.stats + } + + pub fn set_solve_tactic_config(&mut self, config: SolveTacticConfig) { + self.solve_tactic_config = config; + } + + pub fn solve_tactic_config(&self) -> &SolveTacticConfig { + &self.solve_tactic_config + } + + pub(crate) fn with_isolated_stats( + &mut self, + f: impl FnOnce(&mut Self) -> T, + ) -> (T, ExploreStats) { + let previous = std::mem::take(&mut self.stats); + let result = f(self); + let isolated = std::mem::take(&mut self.stats); + self.stats = previous; + (result, isolated) + } + + /// Get the solver for additional queries. + pub fn solver(&self) -> &SymSolver<'ctx> { + &self.solver + } + + pub(crate) fn with_prune_infeasible( + &mut self, + prune_infeasible: bool, + f: impl FnOnce(&mut Self) -> T, + ) -> T { + let previous = self.config.prune_infeasible; + self.config.prune_infeasible = prune_infeasible; + let result = f(self); + self.config.prune_infeasible = previous; + result + } + + pub(crate) fn with_exception_bridge_guidance( + &mut self, + enabled: bool, + f: impl FnOnce(&mut Self) -> T, + ) -> T { + let previous = self.exception_bridge_guidance_enabled; + self.exception_bridge_guidance_enabled = enabled; + let result = f(self); + self.exception_bridge_guidance_enabled = previous; + result + } + + pub(crate) fn with_residual_runtime_loop_summaries( + &mut self, + enabled: bool, + f: impl FnOnce(&mut Self) -> T, + ) -> T { + let previous = self.residual_runtime_loop_summaries_enabled; + self.residual_runtime_loop_summaries_enabled = enabled; + let result = f(self); + self.residual_runtime_loop_summaries_enabled = previous; + result + } + + /// Enable target-guided ordering for query-only helpers. + pub fn set_target_guided_queries(&mut self, enabled: bool) { + self.target_guided_queries = enabled; + } + + /// Whether target-guided ordering is enabled for query helpers. + pub fn target_guided_queries_enabled(&self) -> bool { + self.target_guided_queries + } + + /// Register a call hook for a concrete target address. + pub fn register_call_hook(&mut self, addr: u64, hook: F) + where + F: Fn(&mut SymState<'ctx>) -> crate::executor::CallHookResult + 'ctx, + { + self.derived_call_summaries.remove(&addr); + self.executor + .register_call_hook(addr, move |state| Ok(hook(state))); + } + + pub fn register_tagged_call_hook( + &mut self, + addr: u64, + tag: crate::executor::CallHookTag, + hook: F, + ) where + F: Fn(&mut SymState<'ctx>) -> crate::executor::CallHookResult + 'ctx, + { + self.derived_call_summaries.remove(&addr); + self.executor + .register_tagged_call_hook(addr, tag, move |state| Ok(hook(state))); + } + + pub(crate) fn register_derived_call_hook( + &mut self, + addr: u64, + summary: Rc>, + callconv: CallConv, + hook: F, + ) where + F: Fn(&mut SymState<'ctx>) -> crate::executor::CallHookResult + 'ctx, + { + self.derived_call_summaries + .insert(addr, RegisteredDerivedCallSummary { summary, callconv }); + self.executor + .register_call_hook(addr, move |state| Ok(hook(state))); + } + + pub(crate) fn derived_call_summary_views(&self) -> HashMap> { + self.derived_call_summaries + .iter() + .map(|(addr, registered)| { + ( + *addr, + DerivedCallSummaryView { + summary: registered.summary.clone(), + callconv: registered.callconv.clone(), + }, + ) + }) + .collect() + } + + /// Solve a path's constraints and extract concrete values. + /// + /// Returns None if the path is infeasible. + pub fn solve_path(&self, path: &PathResult<'ctx>) -> Option { + if !path.feasible { + return None; + } + + // Get a model from the solver + let model = self.solver.solve(&path.state)?; + + let mut solved = SolvedPath { + final_pc: path.state.pc, + num_constraints: path.state.num_constraints(), + ..Default::default() + }; + + // Extract concrete register values + for (name, value) in path.state.registers() { + if let Some(concrete) = model.eval(value) { + solved.registers.insert(name.clone(), concrete); + } + } + + // Include explicitly tracked symbolic inputs. + for (name, value) in path.state.symbolic_inputs() { + if let Some(concrete) = model.eval(value) { + solved.inputs.entry(name.clone()).or_insert(concrete); + } + } + + // Try to identify symbolic inputs (variables starting with "sym_"). + for (name, value) in path.state.registers() { + if solved.inputs.contains_key(name) { + continue; + } + if (name.starts_with("sym_") || value.is_symbolic()) + && let Some(concrete) = model.eval(value) + { + solved.inputs.insert(name.clone(), concrete); + } + } + + // Extract tracked symbolic memory buffers. + for region in path.state.symbolic_memory() { + if let Some(bytes) = model.eval_bytes(®ion.value, region.size as usize) { + solved.memory.insert(region.name.clone(), bytes); + } + } + + for input in path.state.symbolic_fd_inputs().values() { + let bytes: Option> = input + .bytes + .iter() + .map(|byte| model.eval(byte).map(|v| v as u8)) + .collect(); + if let Some(bytes) = bytes { + solved.input_buffers.insert(input.name.clone(), bytes); + } + } + + Some(solved) + } - // Check feasibility - if self.config.prune_infeasible && !self.solver.is_sat(&state) { - self.stats.paths_pruned += 1; + /// Solve a path after applying constraint-graph tactics and exact input-domain tactics. + pub fn solve_path_with_constraint_graph_tactics( + &self, + path: &PathResult<'ctx>, + graph: &FinalConstraintGraph, + recurrences: &[ExactLoopRecurrenceEvidence], + ) -> Option { + if !path.feasible || !self.solve_tactic_config.enabled { + return None; + } + let folds = loops::exact_fold_evidence_from_recurrences(recurrences); + + if !graph.is_empty() { + for candidate in tactic_candidates_for_constraint_graph( + graph, + Some(&path.state), + &self.solve_tactic_config, + ) { + let mut state = path.state.fork(); + let report = constrain_exact_recurrence_candidate( + &mut state, + &candidate.recurrence, + &candidate.bytes, + ); + if report.constrained_bytes == 0 { + continue; + } + let constrained_path = PathResult { + state, + exit_status: path.exit_status.clone(), + depth: path.depth, + feasible: path.feasible, + }; + if let Some(mut solution) = self.solve_path(&constrained_path) { + solution.generation = Some(SolvedPathGeneration { + kind: if candidate.used_mitm { + SolvedPathGenerationKind::MitmConstraintTactic + } else { + SolvedPathGenerationKind::ExactRecurrenceConstraintTactic + }, + reason: candidate.reason, + constrained_bytes: report.constrained_bytes, + }); + return Some(solution); + } + } + } + + for domain in &self.solve_tactic_config.preferred_domains { + let mut state = path.state.fork(); + let report = constrain_exact_fold_inputs( + &mut state, + &folds, + domain, + self.solve_tactic_config.max_constrained_bytes, + ); + if report.constrained_bytes == 0 { continue; } + let constrained_path = PathResult { + state, + exit_status: path.exit_status.clone(), + depth: path.depth, + feasible: path.feasible, + }; + if let Some(mut solution) = self.solve_path(&constrained_path) { + solution.generation = Some(SolvedPathGeneration { + kind: SolvedPathGenerationKind::DomainConstraintTactic, + reason: "exact fold input-domain constraint".to_string(), + constrained_bytes: report.constrained_bytes, + }); + return Some(solution); + } + } + None + } + + /// Solve all feasible paths and return concrete solutions. + pub fn solve_all_paths(&self, paths: &[PathResult<'ctx>]) -> Vec> { + paths.iter().map(|p| self.solve_path(p)).collect() + } + + /// Whether the most recent exploration stopped because of a budget limit. + pub fn budget_exhausted(&self) -> bool { + self.stats.timed_out || self.stats.max_states_exhausted + } + + fn record_depth(&mut self, depth: usize) { + if depth > self.stats.max_depth_reached { + self.stats.max_depth_reached = depth; + } + } + + fn state_rank(&self, state: &SymState<'ctx>, id: usize) -> (usize, usize, usize) { + (state.num_constraints(), state.depth, id) + } + + fn prune_subsumed_same_pc_state( + &mut self, + worklist: &mut StateWorklist<'ctx>, + state: SymState<'ctx>, + ) -> Option> { + if !self.config.subsumption_states || state.is_terminated() { + return Some(state); + } - // Get and execute block - let block_addr = state.pc; - let Some(block) = func.get_block(block_addr) else { + let candidate_ids = worklist.same_pc_ids(state.pc); + if candidate_ids.is_empty() { + return Some(state); + } + + let state_fingerprint = state.semantic_fingerprint(); + let state_rank = self.state_rank(&state, worklist.slots.len()); + let mut keep_state = true; + + for candidate_id in candidate_ids { + let Some(existing) = worklist.state(candidate_id) else { continue; }; + if existing.is_terminated() || existing.semantic_fingerprint() != state_fingerprint { + continue; + } - if let Ok(forked_states) = self.executor.execute_block(&mut state, block) { - for mut forked in forked_states { - forked.set_prev_pc(Some(block_addr)); - worklist.push_back(forked); - } + self.stats.subsumption_checks += 1; + let existing_subsumes_new = self.solver.implies(&state, existing); + let new_subsumes_existing = self.solver.implies(existing, &state); - if !state.is_terminated() { - if state.pc == block_addr - && let Some(next) = self.fallthrough_target(func, block_addr) - { - state.pc = next; + match (existing_subsumes_new, new_subsumes_existing) { + (Some(true), Some(true)) => { + let existing_rank = self.state_rank(existing, candidate_id); + if existing_rank <= state_rank { + self.stats.subsumption_hits += 1; + self.stats.states_subsumed += 1; + keep_state = false; + break; + } + if worklist.remove_slot(candidate_id).is_some() { + self.stats.subsumption_hits += 1; + self.stats.states_subsumed += 1; + } + } + (Some(true), _) => { + self.stats.subsumption_hits += 1; + self.stats.states_subsumed += 1; + keep_state = false; + break; + } + (_, Some(true)) => { + if worklist.remove_slot(candidate_id).is_some() { + self.stats.subsumption_hits += 1; + self.stats.states_subsumed += 1; } - state.set_prev_pc(Some(block_addr)); - worklist.push_back(state); } + _ => {} } } - None + keep_state.then_some(state) } - /// Explore paths to collect all feasible states that reach a target address. - pub fn find_paths_to( + fn drive_in_scope( &mut self, func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, initial_state: SymState<'ctx>, - target_addr: u64, - ) -> Vec> { + mode: &mut DriverMode<'ctx>, + ) { let start_time = Instant::now(); - let mut matches = Vec::new(); - let mut worklist: VecDeque> = VecDeque::new(); - worklist.push_back(initial_state); + let mut worklist = StateWorklist::new(self.config.strategy); + worklist.push(initial_state); - while let Some(mut state) = self.next_state(&mut worklist) { + 'worklist: while let Some(mut state) = worklist.pop_next() { if self.config.merge_states - && let Some(other) = take_merge_candidate(&mut worklist, state.pc) + && let Some(other) = worklist.take_same_pc(state.pc) { state = state.merge_with(&other); } + if let Some(timeout) = self.config.timeout && start_time.elapsed() > timeout + && mode.on_timeout() { + self.stats.timed_out = true; break; } - if state.pc == target_addr { - let feasible = self.solver.is_sat(&state); - if feasible { - if state.depth > self.stats.max_depth_reached { - self.stats.max_depth_reached = state.depth; + match mode.on_state_popped(self, state) { + DriverAction::Continue(next_state) => state = *next_state, + DriverAction::Skip => continue, + DriverAction::Finish => break, + } + + if self.stats.states_explored >= self.config.max_states && mode.on_max_states() { + self.stats.max_states_exhausted = true; + break; + } + + if state.depth >= self.config.max_depth { + match mode.on_depth_limit(self, state, self.config.max_completed_paths) { + DriverAction::Continue(next_state) => state = *next_state, + DriverAction::Skip => continue, + DriverAction::Finish => break, + } + } + + self.stats.states_explored += 1; + + if self.config.prune_infeasible && !self.solver.is_sat(&state) { + self.stats.paths_pruned += 1; + mode.on_unsat_pruned(); + continue; + } + + let inline_start_depth = state.depth; + let mut revisit_current_state = false; + loop { + if let Some(timeout) = self.config.timeout + && start_time.elapsed() > timeout + && mode.on_timeout() + { + self.stats.timed_out = true; + break 'worklist; + } + + if std::mem::take(&mut revisit_current_state) { + match mode.on_state_popped(self, state) { + DriverAction::Continue(next_state) => state = *next_state, + DriverAction::Skip => continue 'worklist, + DriverAction::Finish => break 'worklist, } - self.stats.paths_completed += 1; - matches.push(PathResult::new(state, true)); } + + let Some(block) = self.resolve_block(func, scope, &state) else { + let missing_pc = state.pc; + match mode.on_missing_block( + self, + state, + missing_pc, + self.config.max_completed_paths, + ) { + DriverAction::Finish => break 'worklist, + DriverAction::Continue(_) | DriverAction::Skip => continue 'worklist, + } + }; + let block_addr = block.runtime_addr; + let block_static_addr = block.static_addr; + let block_func = block.func; + match self.executor.execute_block(&mut state, block.block) { + Ok(forked_states) => { + self.record_depth(state.depth); + + let had_forks = !forked_states.is_empty(); + for mut forked in forked_states { + self.remap_state_pc_after_block(&mut forked, block); + forked.set_prev_pc(Some(block_addr)); + if mode.allow_enqueue(&forked) + && let Some(forked) = + self.prune_subsumed_same_pc_state(&mut worklist, forked) + { + worklist.push(forked); + } + } + + if !state.is_terminated() { + self.patch_pending_exception_resume_pc( + block_func, + &mut state, + block_static_addr, + ); + self.finalize_active_state_after_block( + block_func, + &mut state, + block_addr, + block_static_addr, + ); + if mode.allow_enqueue(&state) { + let can_inline_continue = !had_forks + && state.depth.saturating_sub(inline_start_depth) + < self.max_inline_runahead_depth_delta(); + if can_inline_continue { + revisit_current_state = true; + continue; + } + if let Some(state) = + self.prune_subsumed_same_pc_state(&mut worklist, state) + { + worklist.push(state); + } + } + } else { + match mode.on_terminated_state( + self, + state, + block_addr, + self.config.max_completed_paths, + ) { + DriverAction::Finish => break 'worklist, + DriverAction::Continue(_) | DriverAction::Skip => { + continue 'worklist; + } + } + } + break; + } + Err(e) => match mode.on_execute_error( + self, + state, + block_addr, + e.to_string(), + self.config.max_completed_paths, + ) { + DriverAction::Finish => break 'worklist, + DriverAction::Continue(_) | DriverAction::Skip => continue 'worklist, + }, + } + } + } + + self.stats.total_time = start_time.elapsed(); + } + + fn target_guidance_context( + &mut self, + func: &SsaArtifact, + target_addr: u64, + allow_cross_function_states: bool, + ) -> TargetGuidanceContext { + let distances = self.target_distance_map(func, target_addr); + let reachable_blocks = distances.keys().copied().collect::>(); + let mut call_targets_by_block: HashMap> = HashMap::new(); + let mut block_summary_rank: HashMap = HashMap::new(); + + for call in func.call_sites().by_id.values() { + let Some(target) = func.resolved_call_target(call) else { + continue; + }; + let Some((block_addr, _)) = func.inst_op_site(call.at) else { + continue; + }; + call_targets_by_block + .entry(block_addr) + .or_default() + .push(target); + + let Some(binding) = self.derived_call_summaries.get(&target) else { + continue; + }; + let entry = block_summary_rank + .entry(block_addr) + .or_insert_with(|| BlockSummaryRank { + has_summary: false, + has_exact_summary: false, + min_case_count: usize::MAX, + }); + entry.has_summary = true; + entry.has_exact_summary |= matches!( + binding.summary.completion, + crate::sim::DerivedSummaryCompletion::Exact + ); + entry.min_case_count = entry.min_case_count.min(binding.summary.cases.len()); + } + + TargetGuidanceContext { + target_addr, + distances, + reachable_blocks, + call_targets_by_block, + block_summary_rank, + allow_cross_function_states, + } + } + + fn symbolic_fanout_proxy(&self, state: &SymState<'ctx>) -> usize { + state + .registers() + .values() + .filter(|value| value.is_symbolic()) + .count() + .saturating_add(state.symbolic_inputs().len()) + .saturating_add(state.symbolic_memory().len()) + .saturating_add(state.symbolic_fd_inputs().len()) + } + + fn state_summary_guidance( + &self, + guidance: &TargetGuidanceContext, + state: &SymState<'ctx>, + ) -> StateSummaryGuidance { + let static_pc = self.effective_static_pc(state); + let Some(targets) = guidance.call_targets_by_block.get(&static_pc) else { + return StateSummaryGuidance::default(); + }; + + let mut result = StateSummaryGuidance { + summary_hits: 0, + min_feasible_cases: usize::MAX, + contradictory: false, + }; + + for target in targets { + let Some(binding) = self.derived_call_summaries.get(target) else { + continue; + }; + let summary_guidance = evaluate_derived_summary_guidance( + state, + &binding.summary, + &binding.callconv, + &self.solver, + ); + if !summary_guidance.summary_known { continue; } + result.summary_hits += 1; + result.min_feasible_cases = result + .min_feasible_cases + .min(summary_guidance.feasible_cases.max(1)); + if summary_guidance.contradictory { + result.contradictory = true; + break; + } + } + + if result.summary_hits == 0 { + StateSummaryGuidance::default() + } else { + result + } + } + + fn target_enqueue_allowed( + &mut self, + guidance: &TargetGuidanceContext, + state: &SymState<'ctx>, + ) -> bool { + let static_pc = self.effective_static_pc(state); + let runtime_continuation_bridge = + state.pending_exception().is_some() || state.runtime_region_for_pc(state.pc).is_some(); + if !guidance.reachable_blocks.contains(&static_pc) + && !guidance.allow_cross_function_states + && !runtime_continuation_bridge + { + self.stats.target_pruned_cfg_unreachable += 1; + debug_target_guidance_log(&format!( + "cfg_prune target=0x{:x} runtime_pc=0x{:x} static_pc=0x{:x} prev_pc={} terminated={} depth={}", + guidance.target_addr, + state.pc, + static_pc, + state + .prev_pc() + .map(|pc| format!("0x{pc:x}")) + .unwrap_or_else(|| "none".to_string()), + state.is_terminated(), + state.depth, + )); + return false; + } + + let summary_guidance = self.state_summary_guidance(guidance, state); + if summary_guidance.summary_hits > 0 { + self.stats.target_summary_rank_hits += 1; + } + if summary_guidance.contradictory { + self.stats.target_pruned_summary_contradiction += 1; + return false; + } + true + } + + fn enqueue_target_guided_state( + &mut self, + worklist: &mut StateWorklist<'ctx>, + target_heap: &mut BinaryHeap>, + guidance: &TargetGuidanceContext, + mode: &mut DriverMode<'ctx>, + state: SymState<'ctx>, + ) { + if !mode.allow_enqueue(&state) || !self.target_enqueue_allowed(guidance, &state) { + return; + } + let Some(state) = self.prune_subsumed_same_pc_state(worklist, state) else { + return; + }; + let id = worklist.push(state); + if let Some(state) = worklist.state(id) { + target_heap.push(Reverse(TargetGuidedQueueEntry { + rank: self.state_target_rank(state, id, guidance), + id, + })); + } + } + + fn drive_target_guided_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + mode: &mut DriverMode<'ctx>, + target_addr: u64, + allow_cross_function_states: bool, + ) { + let start_time = Instant::now(); + let mut worklist = StateWorklist::new(self.config.strategy); + let mut target_heap: BinaryHeap> = BinaryHeap::new(); + let guidance = self.target_guidance_context(func, target_addr, allow_cross_function_states); + if self.target_enqueue_allowed(&guidance, &initial_state) + && let Some(state) = self.prune_subsumed_same_pc_state(&mut worklist, initial_state) + { + let id = worklist.push(state); + if let Some(state) = worklist.state(id) { + target_heap.push(Reverse(TargetGuidedQueueEntry { + rank: self.state_target_rank(state, id, &guidance), + id, + })); + } + } + + 'worklist: while let Some((mut state, reordered)) = + self.pop_target_guided_state(&mut worklist, &mut target_heap) + { + if reordered { + self.stats.target_guided_reorders += 1; + } + if self.config.merge_states + && let Some(other) = worklist.take_same_pc(state.pc) + { + state = state.merge_with(&other); + } + + if let Some(timeout) = self.config.timeout + && start_time.elapsed() > timeout + && mode.on_timeout() + { + self.stats.timed_out = true; + break; + } + + match mode.on_state_popped(self, state) { + DriverAction::Continue(next_state) => state = *next_state, + DriverAction::Skip => continue, + DriverAction::Finish => break, + } - if self.stats.states_explored >= self.config.max_states { + if self.stats.states_explored >= self.config.max_states && mode.on_max_states() { + self.stats.max_states_exhausted = true; break; } + if state.depth >= self.config.max_depth { - self.stats.paths_max_depth += 1; - continue; + match mode.on_depth_limit(self, state, self.config.max_completed_paths) { + DriverAction::Continue(next_state) => state = *next_state, + DriverAction::Skip => continue, + DriverAction::Finish => break, + } } self.stats.states_explored += 1; if self.config.prune_infeasible && !self.solver.is_sat(&state) { self.stats.paths_pruned += 1; + debug_target_guidance_log(&format!( + "unsat_prune target=0x{:x} runtime_pc=0x{:x} static_pc=0x{:x} prev_pc={} terminated={} depth={} constraints={}", + guidance.target_addr, + state.pc, + self.effective_static_pc(&state), + state + .prev_pc() + .map(|pc| format!("0x{pc:x}")) + .unwrap_or_else(|| "none".to_string()), + state.is_terminated(), + state.depth, + state.num_constraints(), + )); + mode.on_unsat_pruned(); continue; } - let block_addr = state.pc; - let Some(block) = func.get_block(block_addr) else { - continue; - }; + let inline_start_depth = state.depth; + let mut revisit_current_state = false; + loop { + let mut had_runtime_dispatch = false; + if let Some(timeout) = self.config.timeout + && start_time.elapsed() > timeout + && mode.on_timeout() + { + self.stats.timed_out = true; + break 'worklist; + } + + if std::mem::take(&mut revisit_current_state) { + match mode.on_state_popped(self, state) { + DriverAction::Continue(next_state) => state = *next_state, + DriverAction::Skip => continue 'worklist, + DriverAction::Finish => break 'worklist, + } + } + + if let Some(dispatched) = self.dispatch_runtime_breakpoint(&mut state) { + had_runtime_dispatch = true; + self.enqueue_target_guided_state( + &mut worklist, + &mut target_heap, + &guidance, + mode, + dispatched, + ); + } + + if !self.target_enqueue_allowed(&guidance, &state) { + continue 'worklist; + } - match self.executor.execute_block(&mut state, block) { - Ok(forked_states) => { - for mut forked in forked_states { - forked.set_prev_pc(Some(block_addr)); - worklist.push_back(forked); + let Some(block) = self.resolve_block(func, scope, &state) else { + let missing_pc = state.pc; + match mode.on_missing_block( + self, + state, + missing_pc, + self.config.max_completed_paths, + ) { + DriverAction::Finish => break 'worklist, + DriverAction::Continue(_) | DriverAction::Skip => continue 'worklist, } + }; + let block_addr = block.runtime_addr; + let block_static_addr = block.static_addr; + let block_func = block.func; + debug_target_guidance_log(&format!( + "block_exec target=0x{:x} runtime_pc=0x{:x} static_pc=0x{:x} depth={} states={} constraints={}", + guidance.target_addr, + block_addr, + block_static_addr, + state.depth, + self.stats.states_explored, + state.num_constraints(), + )); - if !state.is_terminated() { - if state.pc == block_addr - && let Some(next) = self.fallthrough_target(func, block_addr) - { - state.pc = next; + if let Some(summarized) = self.summarize_runtime_breakpoint_loop( + func, + scope, + block_func, + block.block, + &state, + ) { + self.enqueue_target_guided_state( + &mut worklist, + &mut target_heap, + &guidance, + mode, + summarized, + ); + continue 'worklist; + } + + let direct_call_targets = self.direct_call_fork_targets(func, scope, block.block); + if let Some(targets) = direct_call_targets.as_ref() { + let mut targets = targets.iter().copied().collect::>(); + targets.sort_unstable(); + debug_target_guidance_log(&format!( + "direct_call_fork_targets block=0x{:x} targets={:?}", + block_static_addr, targets + )); + } + let previous_direct_call_targets = self + .executor + .replace_direct_call_fork_targets(direct_call_targets); + let execution = self.executor.execute_block(&mut state, block.block); + self.executor + .replace_direct_call_fork_targets(previous_direct_call_targets); + + match execution { + Ok(forked_states) => { + self.record_depth(state.depth); + debug_target_guidance_log(&format!( + "block_done target=0x{:x} runtime_pc=0x{:x} static_pc=0x{:x} depth={} forks={} terminated={}", + guidance.target_addr, + block_addr, + block_static_addr, + state.depth, + forked_states.len(), + state.is_terminated(), + )); + + let had_forks = !forked_states.is_empty(); + for mut forked in forked_states { + self.remap_state_pc_after_block(&mut forked, block); + forked.set_prev_pc(Some(block_addr)); + self.enqueue_target_guided_state( + &mut worklist, + &mut target_heap, + &guidance, + mode, + forked, + ); + } + + if !state.is_terminated() { + self.patch_pending_exception_resume_pc( + block_func, + &mut state, + block_static_addr, + ); + self.finalize_active_state_after_block( + block_func, + &mut state, + block_addr, + block_static_addr, + ); + if mode.allow_enqueue(&state) + && self.target_enqueue_allowed(&guidance, &state) + { + let can_inline_continue = !had_forks + && !had_runtime_dispatch + && state.depth.saturating_sub(inline_start_depth) + < self.max_inline_runahead_depth_delta(); + if can_inline_continue { + revisit_current_state = true; + continue; + } + if let Some(state) = + self.prune_subsumed_same_pc_state(&mut worklist, state) + { + let id = worklist.push(state); + if let Some(state) = worklist.state(id) { + target_heap.push(Reverse(TargetGuidedQueueEntry { + rank: self.state_target_rank(state, id, &guidance), + id, + })); + } + } + } + } else { + let resumed = + self.fork_symbolic_exception_resume_targets(func, scope, &state); + if !resumed.is_empty() { + for resumed_state in resumed { + self.enqueue_target_guided_state( + &mut worklist, + &mut target_heap, + &guidance, + mode, + resumed_state, + ); + } + continue 'worklist; + } + match mode.on_terminated_state( + self, + state, + block_addr, + self.config.max_completed_paths, + ) { + DriverAction::Finish => break 'worklist, + DriverAction::Continue(_) | DriverAction::Skip => { + continue 'worklist; + } + } } - state.set_prev_pc(Some(block_addr)); - worklist.push_back(state); + break; } + Err(e) => match mode.on_execute_error( + self, + state, + block_addr, + e.to_string(), + self.config.max_completed_paths, + ) { + DriverAction::Finish => break 'worklist, + DriverAction::Continue(_) | DriverAction::Skip => continue 'worklist, + }, } - Err(_) => { - self.stats.paths_completed += 1; + } + } + + self.stats.total_time = start_time.elapsed(); + } + + fn target_distance_map(&mut self, func: &SsaArtifact, target_addr: u64) -> HashMap { + let key = (func.entry, target_addr); + if let Some(cached) = self.target_distance_cache.get(&key) { + return cached.clone(); + } + let distances = self.compute_target_distance_map(func, target_addr); + if self.target_distance_cache.len() >= TARGET_DISTANCE_CACHE_LIMIT { + self.target_distance_cache.clear(); + } + self.target_distance_cache.insert(key, distances.clone()); + distances + } + + fn compute_target_distance_map( + &self, + func: &SsaArtifact, + target_addr: u64, + ) -> HashMap { + let mut predecessors: HashMap> = HashMap::new(); + for addr in func.cfg().block_addrs() { + let Some(block) = func.cfg().get_block(addr) else { + continue; + }; + for succ in block.successors() { + predecessors.entry(succ).or_default().push(addr); + } + } + + let mut distances: HashMap = HashMap::new(); + let mut queue = VecDeque::new(); + distances.insert(target_addr, 0); + queue.push_back(target_addr); + + while let Some(addr) = queue.pop_front() { + let distance = distances[&addr]; + for pred in predecessors.get(&addr).into_iter().flatten() { + if distances.contains_key(pred) { + continue; + } + distances.insert(*pred, distance.saturating_add(1)); + queue.push_back(*pred); + } + } + + distances + } + + fn pop_target_guided_state( + &mut self, + worklist: &mut StateWorklist<'ctx>, + heap: &mut BinaryHeap>, + ) -> Option<(SymState<'ctx>, bool)> { + loop { + while let Some(Reverse(entry)) = heap.peek() { + if worklist.state(entry.id).is_some() { + break; } + heap.pop(); + } + + let Reverse(entry) = heap.pop()?; + let default_candidate = worklist.default_candidate(); + let reordered = default_candidate.is_some_and(|candidate| candidate != entry.id); + if let Some(state) = worklist.take_slot(entry.id) { + return Some((state, reordered)); } } + } + + fn state_target_rank( + &self, + state: &SymState<'ctx>, + id: usize, + guidance: &TargetGuidanceContext, + ) -> ( + usize, + usize, + usize, + usize, + usize, + usize, + usize, + usize, + usize, + usize, + ) { + let static_pc = self.effective_static_pc(state); + let reachable = guidance.reachable_blocks.contains(&static_pc); + let runtime_continuation_bridge = + state.pending_exception().is_some() || state.runtime_region_for_pc(state.pc).is_some(); + let runtime_walk_penalty = usize::from( + state.pending_exception().is_none() && state.runtime_region_for_pc(state.pc).is_some(), + ); + let reachability_penalty = usize::from(!(reachable || runtime_continuation_bridge)); + let distance = if reachable { + guidance + .distances + .get(&static_pc) + .copied() + .unwrap_or(usize::MAX) + } else if runtime_continuation_bridge { + 0 + } else { + usize::MAX + }; + let summary_rank = guidance + .block_summary_rank + .get(&static_pc) + .cloned() + .unwrap_or_default(); + let summary_known_penalty = usize::from(!summary_rank.has_summary); + let summary_exact_penalty = usize::from(!summary_rank.has_exact_summary); + let summary_case_rank = if summary_rank.has_summary { + summary_rank.min_case_count + } else { + usize::MAX + }; + ( + reachability_penalty, + distance, + summary_known_penalty, + summary_exact_penalty, + summary_case_rank, + runtime_walk_penalty, + self.symbolic_fanout_proxy(state), + state.num_constraints(), + state.depth, + id, + ) + } + + /// Explore all paths in a function. + pub fn explore( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + ) -> Vec> { + self.explore_in_scope(func, None, initial_state) + } + + pub fn explore_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + ) -> Vec> { + if self.config.max_completed_paths == Some(0) { + return Vec::new(); + } + let mut mode = DriverMode::Explore { + results: Vec::new(), + }; + self.drive_in_scope(func, scope, initial_state, &mut mode); + match mode { + DriverMode::Explore { results } => results, + _ => unreachable!("explore should always use explore mode"), + } + } + + fn fallthrough_target(&self, func: &SsaArtifact, block_addr: u64) -> Option { + let block = func.cfg().get_block(block_addr)?; + match block.terminator { + BlockTerminator::Fallthrough { next } => Some(next), + BlockTerminator::ConditionalBranch { false_target, .. } => Some(false_target), + BlockTerminator::Call { fallthrough, .. } => fallthrough, + BlockTerminator::IndirectCall { fallthrough } => fallthrough, + BlockTerminator::Branch { target } => Some(target), + _ => None, + } + } + + /// Explore paths to find inputs that reach a target address. + pub fn find_path_to( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> Option> { + self.find_path_to_in_scope(func, None, initial_state, target_addr) + } + + pub fn find_path_to_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> Option> { + self.find_path_to_in_scope_with_feasibility(func, scope, initial_state, target_addr, true) + } + + fn find_path_to_in_scope_with_feasibility( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + target_addr: u64, + require_feasible: bool, + ) -> Option> { + let mut mode = DriverMode::FindFirst { + target_addr, + require_feasible, + found: None, + }; + let bridge_target = self.exception_bridge_guidance_target(func, scope, target_addr); + let guidance_target = bridge_target.unwrap_or(target_addr); + let guidance_func = if bridge_target.is_some() { + Some(func) + } else { + self.resolve_scope_function(func, scope, target_addr) + }; + let allow_cross_function_states = bridge_target.is_none() + && guidance_func.is_some_and(|guidance_func| guidance_func.entry != func.entry); + debug_target_guidance_log(&format!( + "search_setup target=0x{:x} bridge={} guidance_entry={} target_guided={} cross={}", + target_addr, + bridge_target + .map(|target| format!("0x{target:x}")) + .unwrap_or_else(|| "none".to_string()), + guidance_func + .map(|function| format!("0x{:x}", function.entry)) + .unwrap_or_else(|| "none".to_string()), + self.target_guided_queries, + allow_cross_function_states, + )); + if self.target_guided_queries && guidance_func.is_some() { + self.drive_target_guided_in_scope( + guidance_func.unwrap_or(func), + scope, + initial_state, + &mut mode, + guidance_target, + allow_cross_function_states, + ); + } else { + self.drive_in_scope(func, scope, initial_state, &mut mode); + } + match mode { + DriverMode::FindFirst { found, .. } => found, + _ => unreachable!("find_path_to should always use first-match mode"), + } + } - self.stats.total_time = start_time.elapsed(); - matches + /// Explore paths to collect all feasible states that reach a target address. + pub fn find_paths_to( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> Vec> { + self.find_paths_to_in_scope(func, None, initial_state, target_addr) + } + + pub fn find_paths_to_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> Vec> { + let mut mode = DriverMode::FindAll { + target_addr, + require_feasible: true, + matches: Vec::new(), + }; + let bridge_target = self.exception_bridge_guidance_target(func, scope, target_addr); + let guidance_target = bridge_target.unwrap_or(target_addr); + let guidance_func = if bridge_target.is_some() { + Some(func) + } else { + self.resolve_scope_function(func, scope, target_addr) + }; + let allow_cross_function_states = bridge_target.is_none() + && guidance_func.is_some_and(|guidance_func| guidance_func.entry != func.entry); + if self.target_guided_queries && guidance_func.is_some() { + self.drive_target_guided_in_scope( + guidance_func.unwrap_or(func), + scope, + initial_state, + &mut mode, + guidance_target, + allow_cross_function_states, + ); + } else { + self.drive_in_scope(func, scope, initial_state, &mut mode); + } + match mode { + DriverMode::FindAll { matches, .. } => matches, + _ => unreachable!("find_paths_to should always use all-match mode"), + } } /// Explore paths to find inputs that avoid a target address. @@ -558,108 +2852,104 @@ impl<'ctx> PathExplorer<'ctx> { initial_state: SymState<'ctx>, avoid_addrs: &[u64], ) -> Option> { - let avoid_set: HashSet = avoid_addrs.iter().copied().collect(); - let start_time = Instant::now(); - let mut worklist: VecDeque> = VecDeque::new(); - worklist.push_back(initial_state); - - while let Some(mut state) = self.next_state(&mut worklist) { - if self.config.merge_states - && let Some(other) = take_merge_candidate(&mut worklist, state.pc) - { - state = state.merge_with(&other); - } - // Check timeout - if let Some(timeout) = self.config.timeout - && start_time.elapsed() > timeout - { - break; - } - - // Check if we hit an avoided address - if avoid_set.contains(&state.pc) { - continue; - } + let mut mode = DriverMode::Avoid { + avoid_set: avoid_addrs.iter().copied().collect(), + found: None, + }; + self.drive_in_scope(func, None, initial_state, &mut mode); + match mode { + DriverMode::Avoid { found, .. } => found, + _ => unreachable!("find_path_avoiding should always use avoid mode"), + } + } - // Check limits - if self.stats.states_explored >= self.config.max_states { - break; - } - if state.depth >= self.config.max_depth { - let feasible = self.solver.is_sat(&state); - if feasible { - return Some(PathResult::new(state, true)); - } - continue; - } + /// Run a typed exploration specification. + pub fn run_spec( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + spec: &ExplorationSpec, + ) -> Result, String> { + let mut mode = DriverMode::Spec { + find_set: spec.find_addresses()?.into_iter().collect(), + avoid_set: spec.avoid_addresses()?.into_iter().collect(), + max_finds: spec.max_finds(), + result: SpecExploreResult::default(), + }; + self.drive_in_scope(func, None, initial_state, &mut mode); + match mode { + DriverMode::Spec { result, .. } => Ok(result), + _ => unreachable!("run_spec should always use spec mode"), + } + } +} - self.stats.states_explored += 1; +#[cfg(test)] +mod tests { + use super::*; + use std::rc::Rc; - // Check feasibility - if self.config.prune_infeasible && !self.solver.is_sat(&state) { - self.stats.paths_pruned += 1; - continue; - } + use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; + use r2ssa::{InterprocFunctionId, SsaArtifact}; + use z3::ast::BV; - // Get and execute block - let block_addr = state.pc; - let Some(block) = func.get_block(block_addr) else { - // Reached end without hitting avoided addresses - let feasible = self.solver.is_sat(&state); - if feasible { - return Some(PathResult::new(state, true)); - } - continue; - }; + use crate::SymValue; + use crate::executor::CallHookResult; + use crate::sim::{ + CallConv, DerivedFunctionSummary, DerivedSummaryCase, DerivedSummaryCompletion, + }; - if let Ok(forked_states) = self.executor.execute_block(&mut state, block) { - for mut forked in forked_states { - forked.set_prev_pc(Some(block_addr)); - if !avoid_set.contains(&forked.pc) { - worklist.push_back(forked); - } - } + const RAX: u64 = 0; + const RDI: u64 = 56; + const TMP0: u64 = 0x80; + const TMP1: u64 = 0x88; + const TMP2: u64 = 0x90; - if !state.is_terminated() && !avoid_set.contains(&state.pc) { - if state.pc == block_addr - && let Some(next) = self.fallthrough_target(func, block_addr) - { - state.pc = next; - } - state.set_prev_pc(Some(block_addr)); - worklist.push_back(state); - } - } + fn make_reg(offset: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Register, + offset, + size, + meta: None, } + } - None + fn make_const(val: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: val, + size, + meta: None, + } } -} -fn take_merge_candidate<'ctx>( - worklist: &mut VecDeque>, - pc: u64, -) -> Option> { - let len = worklist.len(); - for idx in 0..len { - if worklist[idx].pc == pc { - return worklist.remove(idx); + fn make_ram(addr: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Ram, + offset: addr, + size, + meta: None, } } - None -} -#[cfg(test)] -mod tests { - use super::*; - use crate::SymValue; + fn make_x86_64_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("AL", RAX, 1)); + arch.add_register(RegisterDef::new("EAX", RAX, 4)); + arch.add_register(RegisterDef::new("RAX", RAX, 8)); + arch.add_register(RegisterDef::new("RDI", RDI, 8)); + arch + } #[test] fn test_explore_config_default() { let config = ExploreConfig::default(); assert_eq!(config.max_states, 1000); + assert_eq!(config.max_completed_paths, None); assert_eq!(config.max_depth, 100); assert!(config.prune_infeasible); + assert!(!config.subsumption_states); } #[test] @@ -677,6 +2967,219 @@ mod tests { assert_eq!(stats.paths_completed, 0); } + #[test] + fn test_state_worklist_same_pc_lookup_skips_popped_entries() { + let ctx = Context::thread_local(); + let mut worklist = StateWorklist::new(ExploreStrategy::Bfs); + + worklist.push(SymState::new(&ctx, 0x1000)); + worklist.push(SymState::new(&ctx, 0x2000)); + worklist.push(SymState::new(&ctx, 0x1000)); + + let first = worklist.pop_next().expect("first state should exist"); + assert_eq!(first.pc, 0x1000); + + let merged = worklist + .take_same_pc(0x1000) + .expect("queued same-pc state should still be found"); + assert_eq!(merged.pc, 0x1000); + + let remaining = worklist.pop_next().expect("non-merged state should remain"); + assert_eq!(remaining.pc, 0x2000); + assert!(worklist.pop_next().is_none()); + assert!(worklist.take_same_pc(0x1000).is_none()); + } + + #[test] + fn test_target_guided_heap_prefers_best_and_skips_stale_entries() { + let ctx = Context::thread_local(); + let mut explorer = PathExplorer::new(&ctx); + let mut worklist = StateWorklist::new(ExploreStrategy::Dfs); + + let id_a = worklist.push(SymState::new(&ctx, 0x1000)); + let id_b = worklist.push(SymState::new(&ctx, 0x2000)); + let id_c = worklist.push(SymState::new(&ctx, 0x3000)); + + let guidance = TargetGuidanceContext { + target_addr: 0x2000, + distances: HashMap::from([(0x1000, 2), (0x2000, 0), (0x3000, 1)]), + reachable_blocks: HashSet::from([0x1000, 0x2000, 0x3000]), + call_targets_by_block: HashMap::new(), + block_summary_rank: HashMap::new(), + allow_cross_function_states: false, + }; + + let mut heap: BinaryHeap> = BinaryHeap::new(); + for id in [id_a, id_b, id_c] { + let state = worklist.state(id).expect("live state"); + heap.push(Reverse(TargetGuidedQueueEntry { + rank: explorer.state_target_rank(state, id, &guidance), + id, + })); + } + + let (state, reordered) = explorer + .pop_target_guided_state(&mut worklist, &mut heap) + .expect("best state should exist"); + assert_eq!(state.pc, 0x2000); + assert!( + reordered, + "best state should differ from DFS default candidate" + ); + + assert!( + worklist.remove_slot(id_c).is_some(), + "simulate a stale heap entry" + ); + + let (state, reordered) = explorer + .pop_target_guided_state(&mut worklist, &mut heap) + .expect("remaining state should exist"); + assert_eq!(state.pc, 0x1000); + assert!( + !reordered, + "after skipping stale entries, the chosen state should match the live default candidate" + ); + } + + #[test] + fn test_target_distance_map_reuses_cache_for_same_entry_and_target() { + let ctx = Context::thread_local(); + let mut explorer = PathExplorer::new(&ctx); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1008, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1008, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, None).expect("symbolic function"); + + let first = explorer.target_distance_map(&func, 0x1008); + let second = explorer.target_distance_map(&func, 0x1008); + + assert_eq!(first, second); + assert_eq!(explorer.target_distance_cache.len(), 1); + assert_eq!(first.get(&0x1008), Some(&0)); + assert_eq!(first.get(&0x1004), Some(&1)); + assert_eq!(first.get(&0x1000), Some(&2)); + } + + #[test] + fn test_same_pc_subsumption_prefers_weaker_constraint_state() { + let ctx = Context::thread_local(); + let mut config = ExploreConfig { + subsumption_states: true, + ..ExploreConfig::default() + }; + config.prune_infeasible = false; + let mut explorer = PathExplorer::with_config(&ctx, config); + let mut worklist = StateWorklist::new(ExploreStrategy::Dfs); + + let mut existing = SymState::new(&ctx, 0x1000); + existing.make_symbolic("sym_input", 64); + let input = existing.get_register("sym_input"); + existing.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(10, 64))); + worklist.push(existing); + + let mut stronger = SymState::new(&ctx, 0x1000); + stronger.make_symbolic("sym_input", 64); + let input = stronger.get_register("sym_input"); + stronger.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(10, 64))); + stronger.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(5, 64))); + + let pruned = explorer.prune_subsumed_same_pc_state(&mut worklist, stronger); + assert!( + pruned.is_none(), + "stronger same-pc state should be subsumed" + ); + assert_eq!(explorer.stats.subsumption_checks, 1); + assert_eq!(explorer.stats.subsumption_hits, 1); + assert_eq!(explorer.stats.states_subsumed, 1); + assert_eq!(worklist.live_states, 1); + } + + #[test] + fn test_same_pc_subsumption_replaces_stronger_constraint_state() { + let ctx = Context::thread_local(); + let mut config = ExploreConfig { + subsumption_states: true, + ..ExploreConfig::default() + }; + config.prune_infeasible = false; + let mut explorer = PathExplorer::with_config(&ctx, config); + let mut worklist = StateWorklist::new(ExploreStrategy::Dfs); + + let mut stronger = SymState::new(&ctx, 0x1000); + stronger.make_symbolic("sym_input", 64); + let input = stronger.get_register("sym_input"); + stronger.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(10, 64))); + stronger.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(5, 64))); + worklist.push(stronger); + + let mut weaker = SymState::new(&ctx, 0x1000); + weaker.make_symbolic("sym_input", 64); + let input = weaker.get_register("sym_input"); + weaker.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(10, 64))); + let kept = explorer.prune_subsumed_same_pc_state(&mut worklist, weaker); + + assert!(kept.is_some(), "weaker same-pc state should survive"); + assert_eq!(explorer.stats.subsumption_checks, 1); + assert_eq!(explorer.stats.subsumption_hits, 1); + assert_eq!(explorer.stats.states_subsumed, 1); + assert_eq!(worklist.live_states, 0); + } + + #[test] + fn test_same_pc_subsumption_tie_break_prefers_shallower_state() { + let ctx = Context::thread_local(); + let config = ExploreConfig { + subsumption_states: true, + ..ExploreConfig::default() + }; + let mut explorer = PathExplorer::with_config(&ctx, config); + let mut worklist = StateWorklist::new(ExploreStrategy::Random); + + let mut existing = SymState::new(&ctx, 0x1000); + existing.make_symbolic("sym_input", 64); + existing.step(); + worklist.push(existing); + + let mut new_state = SymState::new(&ctx, 0x1000); + new_state.make_symbolic("sym_input", 64); + let kept = explorer.prune_subsumed_same_pc_state(&mut worklist, new_state); + + assert!( + kept.is_some(), + "shallower state should replace deeper equivalent state" + ); + assert_eq!(worklist.live_states, 0); + assert_eq!(explorer.stats.subsumption_hits, 1); + assert_eq!(explorer.stats.states_subsumed, 1); + } + #[test] fn test_path_result_methods() { let ctx = Context::thread_local(); @@ -684,12 +3187,16 @@ mod tests { let mut state = SymState::new(&ctx, 0x1000); state.set_register("rax", SymValue::concrete(42, 64)); state.make_symbolic("rbx", 64); + state.set_register("aaa", SymValue::concrete(7, 64)); let result = PathResult::new(state, true); assert_eq!(result.final_pc(), 0x1000); assert_eq!(result.num_constraints(), 0); - assert!(result.register_names().contains(&"rax".to_string())); + assert_eq!( + result.register_names(), + vec!["aaa".to_string(), "rax".to_string(), "rbx".to_string()] + ); assert_eq!(result.get_concrete_register("rax"), Some(42)); assert!(result.is_register_symbolic("rbx")); } @@ -727,9 +3234,1169 @@ mod tests { fn test_solved_path_default() { let solved = SolvedPath::default(); assert!(solved.inputs.is_empty()); + assert!(solved.input_buffers.is_empty()); assert!(solved.registers.is_empty()); assert!(solved.memory.is_empty()); assert_eq!(solved.final_pc, 0); assert_eq!(solved.num_constraints, 0); } + + #[test] + fn test_solve_path_emits_stable_map_order() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.new_symbolic_input("z_input", 64); + state.new_symbolic_input("a_input", 64); + state.set_register("z_reg", SymValue::concrete(3, 64)); + state.set_register("a_reg", SymValue::concrete(1, 64)); + state.set_register("m_reg", SymValue::concrete(9, 64)); + + let result = PathResult::new(state, true); + let explorer = PathExplorer::new(&ctx); + let solved = explorer.solve_path(&result).expect("path should solve"); + + assert_eq!( + solved.inputs.keys().cloned().collect::>(), + vec!["a_input".to_string(), "z_input".to_string()] + ); + assert_eq!( + solved.registers.keys().cloned().collect::>(), + vec![ + "a_reg".to_string(), + "m_reg".to_string(), + "z_reg".to_string() + ] + ); + } + + #[test] + fn test_target_guided_prunes_exact_summary_contradiction() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![R2ILOp::Call { + target: make_const(0x2000, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1010, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(TMP0, 1), + src: make_const(1, 1), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("symbolic function"); + + let mut explorer = PathExplorer::new(&ctx); + explorer.set_target_guided_queries(true); + + let arg0 = SymValue::new_symbolic(&ctx, "summary_arg0", 64); + let guard = arg0.to_bv(&ctx).eq(BV::from_u64(1, 64)); + let summary = Rc::new(DerivedFunctionSummary { + id: InterprocFunctionId(0x2000), + name: Some("contradict_helper".to_string()), + arg_count_hint: 1, + arg_symbols: vec![(0, arg0)], + memory_inputs: Vec::new(), + cases: vec![DerivedSummaryCase { + guard, + return_value: None, + memory_writes: Vec::new(), + }], + completion: DerivedSummaryCompletion::Exact, + }); + explorer.register_derived_call_hook(0x2000, summary, CallConv::x86_64_sysv(), |_state| { + CallHookResult::Fallthrough + }); + + let mut state = SymState::new(&ctx, 0x1000); + state.set_register("RDI_0", SymValue::concrete(2, 64)); + + let paths = explorer.find_paths_to(&func, state, 0x1010); + assert!(paths.is_empty(), "contradictory exact summary should prune"); + assert_eq!(explorer.stats().target_pruned_summary_contradiction, 1); + } + + #[test] + fn test_runtime_alias_region_becomes_queryable() { + let ctx = Context::thread_local(); + let blocks = vec![ + R2ILBlock { + addr: 0x2000, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x2004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x2004, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, None).expect("symbolic function"); + let mut state = SymState::new(&ctx, 0x6000_0000); + let _ = state.define_runtime_region("jit_blob", 0x6000_0000, 0x100, true); + state.note_runtime_store_copy( + 0x6000_0000, + 1, + Some(&crate::RuntimeValueProvenance { + source_addr: 0x2000, + size: 1, + }), + ); + + let mut explorer = PathExplorer::new(&ctx); + let found = explorer.find_path_to(&func, state, 0x6000_0004); + + assert!(found.is_some(), "runtime-mapped target should be reachable"); + assert_eq!(explorer.stats().runtime_missing_materialized_code, 0); + } + + #[test] + fn test_runtime_alias_region_matches_static_source_target() { + let ctx = Context::thread_local(); + let blocks = vec![ + R2ILBlock { + addr: 0x2000, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x2004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x2004, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, None).expect("symbolic function"); + let mut state = SymState::new(&ctx, 0x6000_0000); + let _ = state.define_runtime_region("jit_blob", 0x6000_0000, 0x100, true); + state.note_runtime_store_copy( + 0x6000_0000, + 1, + Some(&crate::RuntimeValueProvenance { + source_addr: 0x2000, + size: 1, + }), + ); + + let mut explorer = PathExplorer::new(&ctx); + let found = explorer.find_path_to(&func, state, 0x2004); + + assert!( + found.is_some(), + "runtime-mapped execution should match the static source target" + ); + } + + #[test] + fn test_block_entry_concrete_lookup_uses_selected_phi_source() { + let ctx = Context::thread_local(); + let counter = r2ssa::SSAVar::new("RCX", 7, 8); + let block = r2ssa::FunctionSSABlock { + addr: 0x1400, + size: 4, + phis: vec![r2ssa::PhiNode { + dst: counter.clone(), + sources: vec![ + (0x1000, r2ssa::SSAVar::new("RCX", 4, 8)), + (0x1200, r2ssa::SSAVar::new("RCX", 6, 8)), + ], + }], + ops: Vec::new(), + }; + let mut state = SymState::new(&ctx, 0x1400); + state.set_prev_pc(Some(0x1000)); + state.set_register("RCX_4", SymValue::concrete(0, 64)); + state.set_register("RCX_6", SymValue::concrete(0xdead_beef_cafe_1337, 64)); + state.set_register("RCX_7", SymValue::concrete(0xcccc_cccc_cccc_cccc, 64)); + + assert_eq!( + loops::concrete_state_var_at_block_entry(&state, &block, &counter), + Some(0), + "loop summaries must read the incoming phi source, not a stale phi destination" + ); + } + + #[test] + fn test_target_guided_follows_scoped_direct_call_thunk() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let root_blocks = vec![R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }]; + let thunk_blocks = vec![ + R2ILBlock { + addr: 0x2000, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x3000, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x3000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let root = SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic"); + let thunk = SsaArtifact::for_symbolic(&thunk_blocks, Some(&arch)).expect("thunk symbolic"); + let scope = crate::PreparedFunctionScope::new( + 0x1000, + vec![ + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("thunk".to_string()), + prepared: thunk, + }, + ], + ) + .expect("scope"); + + let mut explorer = PathExplorer::new(&ctx); + explorer.set_target_guided_queries(true); + let found = explorer.find_path_to_in_scope( + &root, + Some(&scope), + SymState::new(&ctx, 0x1000), + 0x3000, + ); + + assert!( + found.is_some(), + "target-guided queries should follow direct calls into scoped thunk/helper blocks" + ); + assert_eq!(found.unwrap().final_pc(), 0x3000); + } + + #[test] + fn test_runtime_missing_materialized_code_is_tracked() { + let ctx = Context::thread_local(); + let blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let func = SsaArtifact::for_symbolic(&blocks, None).expect("symbolic function"); + let mut state = SymState::new(&ctx, 0x6000_0000); + let _ = state.define_runtime_region("jit_blob", 0x6000_0000, 0x100, true); + + let mut explorer = PathExplorer::new(&ctx); + let found = explorer.find_paths_to(&func, state, 0x6000_0004); + + assert!( + found.is_empty(), + "unmaterialized runtime region should not be queryable" + ); + assert_eq!(explorer.stats().runtime_missing_materialized_code, 1); + } + + #[test] + fn test_deterministic_linear_runahead_reaches_target_past_depth_budget() { + let ctx = Context::thread_local(); + let mut blocks = Vec::new(); + for idx in 0..16u64 { + let addr = 0x1000 + idx * 4; + let next = addr + 4; + let ops = if idx == 15 { + vec![R2ILOp::Return { + target: make_const(0, 8), + }] + } else { + vec![R2ILOp::Branch { + target: make_const(next, 8), + }] + }; + blocks.push(R2ILBlock { + addr, + size: 4, + ops, + switch_info: None, + op_metadata: Default::default(), + }); + } + let func = SsaArtifact::for_symbolic(&blocks, None).expect("symbolic function"); + + let mut explorer = PathExplorer::with_config( + &ctx, + ExploreConfig { + max_depth: 5, + ..ExploreConfig::default() + }, + ); + let target = 0x1000 + 15 * 4; + let found = explorer.find_path_to(&func, SymState::new(&ctx, 0x1000), target); + + assert!( + found.is_some(), + "deterministic linear chain should run ahead to the far target" + ); + let found = found.unwrap(); + assert_eq!(found.final_pc(), target); + assert!( + found.depth > 5, + "execution depth should still reflect the concrete block work" + ); + assert_eq!(explorer.stats().paths_max_depth, 0); + } + + #[test] + fn test_target_guided_symbolic_byte_loop_reaches_concrete_terminator() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_reg(RDI, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntZExt { + dst: make_reg(RAX, 4), + src: make_reg(TMP0, 1), + }, + R2ILOp::IntZExt { + dst: make_reg(RAX, 8), + src: make_reg(RAX, 4), + }, + R2ILOp::Branch { + target: make_const(0x1010, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1010, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntZExt { + dst: make_reg(RAX, 4), + src: make_reg(RAX, 1), + }, + R2ILOp::IntAdd { + dst: make_reg(RDI, 8), + a: make_reg(RDI, 8), + b: make_const(1, 8), + }, + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_reg(RDI, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntZExt { + dst: make_reg(RAX, 4), + src: make_reg(TMP0, 1), + }, + R2ILOp::IntZExt { + dst: make_reg(RAX, 8), + src: make_reg(RAX, 4), + }, + R2ILOp::IntAnd { + dst: make_reg(TMP1, 1), + a: make_reg(RAX, 1), + b: make_reg(RAX, 1), + }, + R2ILOp::IntEqual { + dst: make_reg(TMP2, 1), + a: make_reg(TMP1, 1), + b: make_const(0, 1), + }, + R2ILOp::BoolNot { + dst: make_reg(TMP0 + 1, 1), + src: make_reg(TMP2, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP0 + 1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1014, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("symbolic function"); + + let mut state = SymState::new(&ctx, 0x1000); + let region = state.define_memory_region( + crate::MemoryRegionKind::Input, + "argv_like", + Some(0x2000), + Some(0x10), + ); + state.set_register("RDI_0", SymValue::concrete(0x2000, 64)); + let byte0 = state.new_symbolic_input("loop_byte_0", 8); + let byte1 = state.new_symbolic_input("loop_byte_1", 8); + state.add_constraint(byte0.to_bv(&ctx).eq(BV::from_u64(0, 8)).not()); + state.add_constraint(byte1.to_bv(&ctx).eq(BV::from_u64(0, 8)).not()); + state.mem_write(&SymValue::concrete(0x2000, 64), &byte0, 1); + state.mem_write(&SymValue::concrete(0x2001, 64), &byte1, 1); + state.seed_region_bytes(region, 2, &[0]); + + let mut explorer = PathExplorer::new(&ctx); + explorer.set_target_guided_queries(true); + let paths = explorer.find_paths_to(&func, state, 0x1014); + + assert_eq!(paths.len(), 1, "terminator exit should remain reachable"); + assert_eq!(paths[0].final_pc(), 0x1014); + assert_eq!( + explorer.stats().target_match_unsat, + 1, + "only the impossible symbolic early exit should be unsat" + ); + } + + #[test] + fn test_target_guidance_keeps_pending_exception_bridge_states() { + let ctx = Context::thread_local(); + let mut explorer = PathExplorer::new(&ctx); + let guidance = TargetGuidanceContext { + target_addr: 0x1000, + distances: HashMap::from([(0x1000, 0)]), + reachable_blocks: HashSet::from([0x1000]), + call_targets_by_block: HashMap::new(), + block_summary_rank: HashMap::new(), + allow_cross_function_states: false, + }; + let (_, context_addr) = { + let mut tmp = SymState::new(&ctx, 0x2000); + tmp.allocate_heap_region("context", 0x400) + }; + let mut state = SymState::new(&ctx, 0x2000); + state.set_pending_exception(crate::state::PendingExceptionContinuation { + handler_addr: 0x2000, + exception_code: 0x8000_0004, + exception_pointers_addr: 0x7000_0000, + exception_record_addr: 0x7000_0100, + context_addr, + }); + + assert!( + explorer.target_enqueue_allowed(&guidance, &state), + "pending exception continuation should bypass root-CFG pruning" + ); + } + + #[test] + fn test_target_guidance_keeps_runtime_region_bridge_states() { + let ctx = Context::thread_local(); + let mut explorer = PathExplorer::new(&ctx); + let guidance = TargetGuidanceContext { + target_addr: 0x2004, + distances: HashMap::from([(0x2004, 0)]), + reachable_blocks: HashSet::from([0x2004]), + call_targets_by_block: HashMap::new(), + block_summary_rank: HashMap::new(), + allow_cross_function_states: false, + }; + let mut state = SymState::new(&ctx, 0x6000_0000); + let _ = state.define_runtime_region("jit_blob", 0x6000_0000, 0x100, true); + + assert!( + explorer.target_enqueue_allowed(&guidance, &state), + "runtime-materialized states should bypass root-CFG pruning" + ); + } + + #[test] + fn test_pending_exception_resume_pc_uses_block_fallthrough() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Call { + target: make_const(0x4000, 8), + }], + }, + R2ILBlock { + addr: 0x1004, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("symbolic function"); + let mut state = SymState::new(&ctx, 0x2000); + let (_, context_addr) = state.allocate_heap_region("context", 0x400); + state.set_pending_exception(crate::state::PendingExceptionContinuation { + handler_addr: 0x2000, + exception_code: 0x8000_0004, + exception_pointers_addr: 0x7000_0000, + exception_record_addr: 0x7000_0100, + context_addr, + }); + + let explorer = PathExplorer::new(&ctx); + explorer.patch_pending_exception_resume_pc(&func, &mut state, 0x1000); + + assert_eq!( + state + .mem_read( + &SymValue::concrete(context_addr.saturating_add(0xF8), 64), + 8 + ) + .as_concrete(), + Some(0x1004) + ); + } + + #[test] + fn test_exception_handler_target_guidance_uses_raise_site_bridge() { + let ctx = Context::thread_local(); + let mut arch = make_x86_64_arch(); + arch.add_register(RegisterDef::new("RCX", 0x80, 8)); + arch.add_register(RegisterDef::new("RDX", 0x88, 8)); + arch.add_register(RegisterDef::new("R8", 0x90, 8)); + arch.add_register(RegisterDef::new("R9", 0x98, 8)); + + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(1, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Call { + target: make_const(0x5000, 8), + }, + R2ILOp::Branch { + target: make_const(0x1010, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1010, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(0x8000_0004, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x90, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x98, 8), + src: make_const(0, 8), + }, + R2ILOp::Call { + target: make_const(0x5008, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }, + ]; + let handler_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }]; + let root = SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic"); + let handler = + SsaArtifact::for_symbolic(&handler_blocks, Some(&arch)).expect("handler symbolic"); + let scope = crate::PreparedFunctionScope::new( + 0x1000, + vec![ + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("handler".to_string()), + prepared: handler, + }, + ], + ) + .expect("scope"); + + let mut explorer = PathExplorer::new(&ctx); + explorer.register_tagged_call_hook( + 0x5000, + crate::executor::CallHookTag::WindowsAddVectoredExceptionHandler, + |_| CallHookResult::Fallthrough, + ); + explorer.register_tagged_call_hook( + 0x5008, + crate::executor::CallHookTag::WindowsRaiseException, + |_| CallHookResult::Fallthrough, + ); + + assert_eq!( + explorer.exception_bridge_guidance_target(&root, Some(&scope), 0x2000), + Some(0x1010) + ); + } + + #[test] + fn test_runtime_target_guidance_uses_raise_site_bridge() { + let ctx = Context::thread_local(); + let mut arch = make_x86_64_arch(); + arch.add_register(RegisterDef::new("RCX", 0x80, 8)); + arch.add_register(RegisterDef::new("RDX", 0x88, 8)); + arch.add_register(RegisterDef::new("R8", 0x90, 8)); + arch.add_register(RegisterDef::new("R9", 0x98, 8)); + + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(1, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Call { + target: make_const(0x5000, 8), + }, + R2ILOp::Branch { + target: make_const(0x1010, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1010, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(0x8000_0004, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x90, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x98, 8), + src: make_const(0, 8), + }, + R2ILOp::Call { + target: make_const(0x5008, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }, + ]; + let handler_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }]; + let root = SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic"); + let handler = + SsaArtifact::for_symbolic(&handler_blocks, Some(&arch)).expect("handler symbolic"); + let scope = crate::PreparedFunctionScope::new( + 0x1000, + vec![ + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("handler".to_string()), + prepared: handler, + }, + ], + ) + .expect("scope"); + + let mut explorer = PathExplorer::new(&ctx); + explorer.register_tagged_call_hook( + 0x5000, + crate::executor::CallHookTag::WindowsAddVectoredExceptionHandler, + |_| CallHookResult::Fallthrough, + ); + explorer.register_tagged_call_hook( + 0x5008, + crate::executor::CallHookTag::WindowsRaiseException, + |_| CallHookResult::Fallthrough, + ); + + assert_eq!( + explorer.exception_bridge_guidance_target(&root, Some(&scope), 0x7000), + Some(0x1010) + ); + } + + #[test] + fn test_cfg_unreachable_local_target_uses_raise_site_bridge() { + let ctx = Context::thread_local(); + let mut arch = make_x86_64_arch(); + arch.add_register(RegisterDef::new("RCX", 0x80, 8)); + arch.add_register(RegisterDef::new("RDX", 0x88, 8)); + arch.add_register(RegisterDef::new("R8", 0x90, 8)); + arch.add_register(RegisterDef::new("R9", 0x98, 8)); + + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(1, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Call { + target: make_const(0x5000, 8), + }, + R2ILOp::Branch { + target: make_const(0x1010, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1010, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(0x8000_0004, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x90, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x98, 8), + src: make_const(0, 8), + }, + R2ILOp::Call { + target: make_const(0x5008, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1020, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let handler_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }]; + let root = SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic"); + let handler = + SsaArtifact::for_symbolic(&handler_blocks, Some(&arch)).expect("handler symbolic"); + let scope = crate::PreparedFunctionScope::new( + 0x1000, + vec![ + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("handler".to_string()), + prepared: handler, + }, + ], + ) + .expect("scope"); + + let mut explorer = PathExplorer::new(&ctx); + explorer.register_tagged_call_hook( + 0x5000, + crate::executor::CallHookTag::WindowsAddVectoredExceptionHandler, + |_| CallHookResult::Fallthrough, + ); + explorer.register_tagged_call_hook( + 0x5008, + crate::executor::CallHookTag::WindowsRaiseException, + |_| CallHookResult::Fallthrough, + ); + + assert_eq!( + explorer.exception_bridge_guidance_target(&root, Some(&scope), 0x1020), + Some(0x1010) + ); + } + + #[test] + fn test_import_mediated_exception_calls_use_raise_site_bridge() { + let ctx = Context::thread_local(); + let mut arch = make_x86_64_arch(); + arch.add_register(RegisterDef::new("RCX", 0x80, 8)); + arch.add_register(RegisterDef::new("RDX", 0x88, 8)); + arch.add_register(RegisterDef::new("R8", 0x90, 8)); + arch.add_register(RegisterDef::new("R9", 0x98, 8)); + + let addveh_target = Varnode { + space: SpaceId::Unique, + offset: 0x500, + size: 8, + meta: None, + }; + let raise_target = Varnode { + space: SpaceId::Unique, + offset: 0x508, + size: 8, + meta: None, + }; + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(1, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Copy { + dst: addveh_target.clone(), + src: make_ram(0x5000, 8), + }, + R2ILOp::CallInd { + target: addveh_target, + }, + R2ILOp::Branch { + target: make_const(0x1010, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1010, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(0x8000_0004, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x90, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x98, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: raise_target.clone(), + src: make_ram(0x5008, 8), + }, + R2ILOp::CallInd { + target: raise_target, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }, + ]; + let handler_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }]; + let root = SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic"); + let handler = + SsaArtifact::for_symbolic(&handler_blocks, Some(&arch)).expect("handler symbolic"); + let scope = crate::PreparedFunctionScope::new( + 0x1000, + vec![ + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("handler".to_string()), + prepared: handler, + }, + ], + ) + .expect("scope"); + + let mut explorer = PathExplorer::new(&ctx); + explorer.register_tagged_call_hook( + 0x5000, + crate::executor::CallHookTag::WindowsAddVectoredExceptionHandler, + |_| CallHookResult::Fallthrough, + ); + explorer.register_tagged_call_hook( + 0x5008, + crate::executor::CallHookTag::WindowsRaiseException, + |_| CallHookResult::Fallthrough, + ); + + assert_eq!( + explorer.exception_bridge_guidance_target(&root, Some(&scope), 0x2000), + Some(0x1010) + ); + } + + #[test] + fn test_import_mediated_bridge_works_with_installed_runtime_hooks() { + let ctx = Context::thread_local(); + let mut arch = make_x86_64_arch(); + arch.add_register(RegisterDef::new("RCX", 0x80, 8)); + arch.add_register(RegisterDef::new("RDX", 0x88, 8)); + arch.add_register(RegisterDef::new("R8", 0x90, 8)); + arch.add_register(RegisterDef::new("R9", 0x98, 8)); + + let addveh_target = Varnode { + space: SpaceId::Unique, + offset: 0x520, + size: 8, + meta: None, + }; + let raise_target = Varnode { + space: SpaceId::Unique, + offset: 0x528, + size: 8, + meta: None, + }; + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(1, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Copy { + dst: addveh_target.clone(), + src: make_ram(0x1400a6010, 8), + }, + R2ILOp::CallInd { + target: addveh_target, + }, + R2ILOp::Branch { + target: make_const(0x1010, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1010, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(0x8000_0004, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x90, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x98, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: raise_target.clone(), + src: make_ram(0x1400a6000, 8), + }, + R2ILOp::CallInd { + target: raise_target, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }, + ]; + let root = SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic"); + let scope = crate::PreparedFunctionScope::new( + 0x1000, + vec![crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }], + ) + .expect("scope"); + + let mut explorer = PathExplorer::new(&ctx); + let symbol_map = HashMap::from([ + ( + 0x1400a6010, + "sym.imp.KERNEL32.dll_AddVectoredExceptionHandler".to_string(), + ), + ( + 0x1400a6000, + "sym.imp.KERNEL32.dll_RaiseException".to_string(), + ), + ]); + crate::install_runtime_hooks_for_scope(&mut explorer, &scope, Some(&arch), &symbol_map); + + assert_eq!( + explorer.exception_bridge_guidance_target(&root, Some(&scope), 0x7000), + Some(0x1010) + ); + } } diff --git a/crates/r2sym/src/query.rs b/crates/r2sym/src/query.rs new file mode 100644 index 0000000..d1fa23e --- /dev/null +++ b/crates/r2sym/src/query.rs @@ -0,0 +1,4277 @@ +//! Typed query-oriented symbolic analysis APIs. +//! +//! This module wraps the lower-level path exploration engine in reusable, +//! analysis-oriented queries that can be consumed by the plugin or other +//! analysis layers without exposing command-shaped policy. + +use std::collections::{BTreeMap, BTreeSet, VecDeque}; +use std::fs::OpenOptions; +use std::io::Write; +use std::time::Duration; + +use r2il::ArchSpec; +use z3::Context; +use z3::ast::{Ast, BV, Bool}; + +use r2ssa::{ + AssumptionSubject, AssumptionUsageReport, AssumptionValue, CompareKind, + PreparedAssumptionBindingKind, SSAOp, SSAVar, SsaArtifact, +}; + +#[cfg(test)] +use crate::BackwardConditionPrecision; +use crate::SymState; +use crate::backward::{ + BackwardConditionSummary, CompiledBackwardCondition, + compile_branch_precondition_with_summaries, compile_target_precondition_with_summaries, + compile_value_postcondition_with_summaries, +}; +use crate::constraints::build_final_constraint_graph_for_path; +use crate::path::{ExploreConfig, ExploreStats, PathExplorer, PathResult, SolvedPath}; +use crate::runtime::install_runtime_hooks_for_scope; +use crate::semantics::{ + SemanticArtifact, TargetQueryExecutionRoute, TargetQueryRouteInput, VmStepSummary, + build_vm_step_summary, classify_interpreter_like, +}; +use crate::sim::{PreparedFunctionScope, SummaryProfile, SummaryRegistry}; +use crate::solver::{SatResult, SolverStats}; +use crate::state::ExitStatus; +use crate::tactics::SolveTacticConfig; +use crate::verification::{ + EvidenceSummary, LiftedReplayBackend, SolveStatus, SolveVerification, SolveVerificationRequest, + SolveWitness, VerificationRequirement, evidence_summary_for_route_and_stats, + solution_extraction_allowed, verification_requirement_for_route_and_stats, verify_solve_result, +}; + +const MAX_VM_COMPILED_STEPS: usize = 8; + +fn debug_query_route_enabled() -> bool { + std::env::var_os("R2SLEIGH_DEBUG_QUERY_ROUTE").is_some() +} + +fn debug_query_route_log(message: &str) { + if !debug_query_route_enabled() { + return; + } + let path = std::env::var("R2SLEIGH_DEBUG_QUERY_ROUTE_LOG") + .unwrap_or_else(|_| "/tmp/r2sleigh_query_route.log".to_string()); + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { + let _ = writeln!(file, "{message}"); + } +} + +fn debug_query_phase_enabled() -> bool { + std::env::var_os("R2SLEIGH_DEBUG_QUERY_PHASES").is_some() +} + +fn debug_query_phase_log(message: &str) { + if !debug_query_phase_enabled() { + return; + } + let path = std::env::var("R2SLEIGH_DEBUG_QUERY_PHASES_LOG") + .unwrap_or_else(|_| "/tmp/r2sleigh_query_phases.log".to_string()); + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { + let _ = writeln!(file, "{message}"); + } +} + +#[derive(Debug, Clone, Default)] +struct QueryAssumptionOutcome { + usage: AssumptionUsageReport, + conditioned: bool, + conflicted: bool, +} + +fn apply_predicate_assumption_to_state<'ctx>( + explorer: &PathExplorer<'ctx>, + func: &SsaArtifact, + state: &mut SymState<'ctx>, + block_addr: u64, + truth: bool, +) -> Result { + let derived_summaries = explorer.derived_call_summary_views(); + let Some(compiled) = compile_branch_precondition_with_summaries( + func, + state, + block_addr, + truth, + &derived_summaries, + ) else { + return Ok(false); + }; + match explorer + .solver() + .sat_with_constraint(state, &compiled.predicate) + { + SatResult::Sat + if compiled.summary.evidence().allows_hard_proof() + || compiled.summary.evidence().allows_narrowing() => + { + state.add_constraint(compiled.predicate); + Ok(true) + } + SatResult::Sat | SatResult::Unknown => Ok(false), + SatResult::Unsat => Err(format!( + "branch assumption for block 0x{block_addr:x} contradicts symbolic state" + )), + } +} + +fn assumption_usage_for_query<'ctx>( + explorer: &PathExplorer<'ctx>, + func: &SsaArtifact, + state: &mut SymState<'ctx>, +) -> QueryAssumptionOutcome { + let mut usage = AssumptionUsageReport::default(); + let mut conditioned = false; + let mut conflicted = false; + + for binding in &func.facts().applied_assumption_bindings { + match (&binding.binding, &binding.assumption.value) { + ( + PreparedAssumptionBindingKind::Predicate { + block_addr, truth, .. + }, + AssumptionValue::Branch { .. }, + ) => match apply_predicate_assumption_to_state( + explorer, + func, + state, + *block_addr, + *truth, + ) { + Ok(true) => { + usage.mark_applied(&binding.assumption); + conditioned = true; + } + Ok(false) => usage.mark_ignored(&binding.assumption), + Err(reason) => { + usage.mark_conflict(&binding.assumption, reason); + conflicted = true; + } + }, + ( + PreparedAssumptionBindingKind::Register { + state_name, bits, .. + }, + value, + ) => { + let Some(current) = state.registers().get(state_name).cloned() else { + usage.mark_ignored(&binding.assumption); + continue; + }; + match apply_assumption_value_to_sym( + state, + &binding.assumption, + ¤t, + *bits, + value, + ) { + Ok(true) => { + usage.mark_applied(&binding.assumption); + conditioned = true; + } + Ok(false) => usage.mark_ignored(&binding.assumption), + Err(reason) => { + usage.mark_conflict(&binding.assumption, reason); + conflicted = true; + } + } + } + _ => {} + } + } + + for assumption in func.facts().assumptions.iter() { + if matches!(assumption.subject, AssumptionSubject::MemoryWindow { .. }) { + match apply_memory_window_assumption(state, assumption) { + Ok(true) => { + usage.mark_applied(assumption); + conditioned = true; + } + Ok(false) => usage.mark_ignored(assumption), + Err(reason) => { + usage.mark_conflict(assumption, reason); + conflicted = true; + } + } + } + } + + for assumption in func.facts().assumptions.iter() { + usage.mark_ignored(assumption); + } + + QueryAssumptionOutcome { + usage, + conditioned, + conflicted, + } +} + +fn apply_assumption_value_to_sym<'ctx>( + state: &mut SymState<'ctx>, + assumption: &r2ssa::AnalysisAssumption, + value: &crate::value::SymValue<'ctx>, + bits: u32, + assumption_value: &AssumptionValue, +) -> Result { + match assumption_value { + AssumptionValue::Constant { value: rhs } => { + if let Some(existing) = value.as_concrete() { + if existing != *rhs { + return Err(format!( + "assumption constant 0x{rhs:x} contradicts concrete value 0x{existing:x}" + )); + } + return Ok(true); + } + state.constrain_eq(value, *rhs); + Ok(true) + } + AssumptionValue::Range { min, max } => { + if min > max { + return Err("invalid range assumption".to_string()); + } + if let Some(existing) = value.as_concrete() { + if existing < *min || existing > *max { + return Err(format!( + "assumption range [{min:#x}, {max:#x}] excludes concrete value {existing:#x}" + )); + } + return Ok(true); + } + state.constrain_range(value, *min, *max); + Ok(true) + } + AssumptionValue::FiniteSet { values } => { + if values.is_empty() { + return Err("empty finite-set assumption".to_string()); + } + if let Some(existing) = value.as_concrete() { + if !values.contains(&existing) { + return Err(format!( + "finite-set assumption excludes concrete value {existing:#x}" + )); + } + return Ok(true); + } + let bv = value.to_bv(state.context()); + let ors = values + .iter() + .map(|item| bv.eq(BV::from_u64(*item, bits.max(1)))) + .collect::>(); + let refs = ors.iter().collect::>(); + state.add_constraint(Bool::or(&refs)); + Ok(true) + } + AssumptionValue::EnumDomain { values, .. } => { + let values = values.iter().map(|value| *value as u64).collect::>(); + apply_assumption_value_to_sym( + state, + assumption, + value, + bits, + &AssumptionValue::FiniteSet { values }, + ) + } + AssumptionValue::TypeHint { .. } => Ok(false), + AssumptionValue::Branch { .. } => match &assumption.subject { + AssumptionSubject::Predicate { .. } => Ok(true), + _ => Ok(false), + }, + } +} + +fn apply_memory_window_assumption<'ctx>( + state: &mut SymState<'ctx>, + assumption: &r2ssa::AnalysisAssumption, +) -> Result { + let AssumptionSubject::MemoryWindow { addr, size } = &assumption.subject else { + return Ok(false); + }; + let Some(region) = state + .symbolic_memory() + .iter() + .find(|region| region.addr == *addr && region.size == *size) + .cloned() + else { + return Ok(false); + }; + if *size > 8 { + return Ok(false); + } + apply_assumption_value_to_sym( + state, + assumption, + ®ion.value, + region.value.bits(), + &assumption.value, + ) +} + +/// Query execution strategy. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum QueryMode { + /// Use the current forward symbolic execution engine. + ForwardOnly, + /// Reserve a slot for future target-guided pruning. + TargetGuided, +} + +/// Typed configuration for engine-level symbolic queries. +#[derive(Debug, Clone)] +pub struct SymQueryConfig { + /// Core exploration budgets and strategy. + pub explore: ExploreConfig, + /// Query execution mode. + pub mode: QueryMode, + /// Summary profile to use when installing function summaries. + pub summary_profile: SummaryProfile, + /// Candidate-generation tactics used after exact semantic summaries. + pub solve_tactics: SolveTacticConfig, +} + +impl Default for SymQueryConfig { + fn default() -> Self { + let explore = ExploreConfig { + subsumption_states: true, + ..ExploreConfig::default() + }; + Self { + explore, + mode: QueryMode::ForwardOnly, + summary_profile: SummaryProfile::Default, + solve_tactics: SolveTacticConfig::default(), + } + } +} + +impl SymQueryConfig { + /// Build a path explorer for this query configuration. + pub fn make_explorer<'ctx>(&self, ctx: &'ctx Context) -> PathExplorer<'ctx> { + let mut explorer = PathExplorer::with_config(ctx, self.explore.clone()); + explorer.set_target_guided_queries(matches!(self.mode, QueryMode::TargetGuided)); + explorer.set_solve_tactic_config(self.solve_tactics.clone()); + explorer + } +} + +/// Recommend a query-depth budget based on the SSA op count and symbolic input surface. +/// +/// Depth is currently counted per executed SSA op, not per basic block. A flat budget +/// can therefore under-approximate string/hash loops and other byte-at-a-time workers. +/// This helper keeps the budget tied to concrete function cost and the current symbolic +/// input surface instead of hard-coding sample-specific constants downstream. +pub fn recommended_query_max_depth(func: &SsaArtifact, initial_state: &SymState<'_>) -> usize { + let cfg_risk = func.function().cfg_risk_summary(); + let total_ops = func + .blocks() + .map(|block| block.ops.len()) + .sum::() + .max(1); + let max_block_ops = func + .blocks() + .map(|block| block.ops.len()) + .max() + .unwrap_or(1) + .max(1); + let symbolic_surface = initial_state.symbolic_inputs().len().clamp(1, 16); + let bounded_copy_loop_work = estimated_bounded_copy_loop_work(func); + let loop_multiplier = if func.function().cfg_risk_summary().back_edge_count == 0 { + symbolic_surface.saturating_add(4) + } else { + symbolic_surface.saturating_add(2) + }; + let base_budget = total_ops + .saturating_mul(loop_multiplier) + .max(max_block_ops.saturating_mul(loop_multiplier.saturating_mul(2))); + let budget = base_budget.max( + total_ops + .saturating_add(bounded_copy_loop_work) + .saturating_add(max_block_ops.saturating_mul(loop_multiplier)), + ); + if cfg_risk.block_count >= 64 || cfg_risk.back_edge_count > 0 { + budget.max(4096) + } else { + budget + } +} + +/// Recommend a query timeout budget for the current function shape. +/// +/// Large bounded copy loops often materialize runtime code byte-by-byte before any +/// symbolic branching happens. Those routes need more wall-clock time even when the +/// actual search frontier stays small. +pub fn recommended_query_timeout(func: &SsaArtifact) -> Duration { + let cfg_risk = func.function().cfg_risk_summary(); + let bounded_copy_loop_work = estimated_bounded_copy_loop_work(func); + if bounded_copy_loop_work >= 32 * 1024 { + Duration::from_secs(180) + } else if bounded_copy_loop_work >= 8 * 1024 { + Duration::from_secs(120) + } else if cfg_risk.block_count >= 64 || cfg_risk.back_edge_count > 0 { + Duration::from_secs(60) + } else { + Duration::from_secs(20) + } +} + +const CONTINUATION_QUERY_MAX_DEPTH_CAP: usize = 8_192; +const CONTINUATION_QUERY_MAX_STATES_CAP: usize = 256; +const CONTINUATION_QUERY_TIMEOUT_CAP: Duration = Duration::from_secs(10); +const CONTINUATION_SEEDED_STATE_CAP: usize = 1; + +#[derive(Debug, Clone)] +pub struct QueryExecutionPolicy { + pub route: crate::TargetQueryRoutePlan, + pub evidence_summary: EvidenceSummary, + pub verification_requirement: VerificationRequirement, + pub max_states: usize, + pub max_depth: usize, + pub timeout: Option, + pub install_scope_summaries: bool, + pub install_runtime_hooks: bool, +} + +impl QueryExecutionPolicy { + pub fn for_route( + config: &SymQueryConfig, + func: &SsaArtifact, + initial_state: &SymState<'_>, + route: crate::TargetQueryRoutePlan, + ) -> Self { + let max_states = + recommended_query_max_states_for_route(config.explore.max_states, Some(&route)); + let max_depth = config + .explore + .max_depth + .max(recommended_query_max_depth_for_route( + func, + initial_state, + Some(&route), + )); + let current_timeout = config + .explore + .timeout + .unwrap_or_else(|| Duration::from_secs(20)); + let recommended_timeout = recommended_query_timeout_for_route(func, Some(&route)); + let timeout = if route_is_continuation_seeded(Some(&route)) { + current_timeout.min(recommended_timeout) + } else { + current_timeout.max(recommended_timeout) + }; + let route_only_stats = ExploreStats::default(); + let evidence_summary = + evidence_summary_for_route_and_stats(&route, &route_only_stats, None); + let verification_requirement = + verification_requirement_for_route_and_stats(&route, &route_only_stats, None); + Self { + install_scope_summaries: !route_skips_eager_scope_summaries(&route), + install_runtime_hooks: true, + evidence_summary, + verification_requirement, + route, + max_states, + max_depth, + timeout: Some(timeout), + } + } +} + +pub fn apply_query_execution_policy(config: &mut SymQueryConfig, policy: &QueryExecutionPolicy) { + config.explore.max_states = policy.max_states; + config.explore.max_depth = policy.max_depth; + config.explore.timeout = policy.timeout; +} + +pub fn route_skips_eager_scope_summaries(route: &crate::TargetQueryRoutePlan) -> bool { + matches!( + &route.target_plan, + crate::TargetQueryPlan::Residual { reasons } + if reasons.iter().any(|reason| reason == "LargeCfg") + ) +} + +pub fn install_symbolic_hooks_for_query_policy<'ctx>( + explorer: &mut PathExplorer<'ctx>, + z3_ctx: &'ctx Context, + scope: &PreparedFunctionScope, + arch: Option<&ArchSpec>, + symbol_map: &std::collections::HashMap, + summary_profile: SummaryProfile, + policy: &QueryExecutionPolicy, +) { + if policy.install_scope_summaries + && let Some(arch) = arch + && let Some(registry) = + SummaryRegistry::with_profile_for_arch_and_symbols(arch, symbol_map, summary_profile) + { + let _ = registry.install_scope_summaries_for_explorer( + explorer, + z3_ctx, + scope, + Some(arch), + symbol_map, + ); + } + if policy.install_runtime_hooks { + install_runtime_hooks_for_scope(explorer, scope, arch, symbol_map); + } +} + +fn route_is_continuation_seeded(route: Option<&crate::TargetQueryRoutePlan>) -> bool { + route.is_some_and(|route| { + matches!( + route.execution, + TargetQueryExecutionRoute::ContinuationSeeded { .. } + ) + }) +} + +pub fn recommended_query_max_depth_for_route( + func: &SsaArtifact, + initial_state: &SymState<'_>, + route: Option<&crate::TargetQueryRoutePlan>, +) -> usize { + let budget = recommended_query_max_depth(func, initial_state); + if route_is_continuation_seeded(route) { + budget.min(CONTINUATION_QUERY_MAX_DEPTH_CAP) + } else { + budget + } +} + +pub fn recommended_query_max_states_for_route( + current_max_states: usize, + route: Option<&crate::TargetQueryRoutePlan>, +) -> usize { + if route_is_continuation_seeded(route) { + current_max_states.min(CONTINUATION_QUERY_MAX_STATES_CAP) + } else { + current_max_states + } +} + +pub fn recommended_query_timeout_for_route( + func: &SsaArtifact, + route: Option<&crate::TargetQueryRoutePlan>, +) -> Duration { + let timeout = recommended_query_timeout(func); + if route_is_continuation_seeded(route) { + timeout.min(CONTINUATION_QUERY_TIMEOUT_CAP) + } else { + timeout + } +} + +fn estimated_bounded_copy_loop_work(func: &SsaArtifact) -> usize { + func.predicates() + .predicates + .values() + .filter_map(|predicate| estimate_bounded_copy_loop_work(func, predicate)) + .sum() +} + +fn estimate_bounded_copy_loop_work( + func: &SsaArtifact, + predicate: &r2ssa::PredicateFact, +) -> Option { + let self_loop = predicate.true_target == predicate.block_addr + || predicate.false_target == predicate.block_addr; + if !self_loop { + return None; + } + + let block = func.function().get_block(predicate.block_addr)?; + let has_load = block.ops.iter().any(|op| matches!(op, SSAOp::Load { .. })); + let has_store = block.ops.iter().any(|op| matches!(op, SSAOp::Store { .. })); + if !has_load || !has_store { + return None; + } + + let comparison = predicate.comparison.as_ref()?; + let bound = [comparison.lhs, comparison.rhs] + .into_iter() + .filter_map(|value_id| func.value_var(value_id)) + .filter_map(ssa_const_value) + .max()?; + + let trip_count = match comparison.kind { + CompareKind::Less => bound, + CompareKind::LessEqual => bound.saturating_add(1), + _ => return None, + }; + + if trip_count == 0 { + return None; + } + + let capped_trip_count = trip_count.min(1 << 20) as usize; + Some(block.ops.len().max(1).saturating_mul(capped_trip_count)) +} + +fn ssa_const_value(var: &SSAVar) -> Option { + let value = var.name.strip_prefix("const:")?; + u64::from_str_radix(value, 16).ok() +} + +/// Completion state for queries that may stop on budgets. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum QueryCompletion { + /// Exploration finished without hitting timeout or state budgets. + Complete, + /// Exploration stopped because a configured budget was exhausted. + BudgetExhausted, +} + +/// Reachability result status. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReachabilityStatus { + Reachable, + Unreachable, + Unknown, + BudgetExhausted, +} + +/// Compact summary of a path condition reaching a given program counter. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PathConditionTerm { + pub expr: String, +} + +/// Typed symbolic path-condition artifact. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SymbolicConditionSet { + pub simplified: String, + pub terms: Vec, + pub num_constraints: usize, +} + +/// Compact summary of a path condition reaching a given program counter. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PathConditionSummary { + pub final_pc: u64, + pub depth: usize, + pub exit_status: ExitStatus, + pub feasible: bool, + pub num_constraints: usize, + pub condition: String, + pub path_condition: SymbolicConditionSet, +} + +/// Result of asking whether a target can be reached. +#[derive(Debug)] +pub struct ReachabilityResult<'ctx> { + pub status: ReachabilityStatus, + pub target_addr: u64, + pub selected_route: crate::TargetQueryRoutePlan, + pub compiled_precondition: Option, + pub paths: Vec>, + pub assumption_usage: AssumptionUsageReport, + pub assumption_conditioned: bool, + pub summary_conditioned: bool, + pub stats: ExploreStats, + pub solver_stats: SolverStats, +} + +/// Result of asking for the conditions that hold at a target PC. +#[derive(Debug)] +pub struct PathConditionResult<'ctx> { + pub completion: QueryCompletion, + pub target_pc: u64, + pub selected_route: crate::TargetQueryRoutePlan, + pub compiled_precondition: Option, + pub conditions: Vec, + pub matching_paths: Vec>, + pub assumption_usage: AssumptionUsageReport, + pub assumption_conditioned: bool, + pub summary_conditioned: bool, + pub stats: ExploreStats, + pub solver_stats: SolverStats, +} + +/// Result of solving for a concrete target. +#[derive(Debug)] +pub struct SolveResult<'ctx> { + pub status: SolveStatus, + pub target_addr: u64, + pub selected_route: crate::TargetQueryRoutePlan, + pub compiled_precondition: Option, + pub matched_paths: Vec>, + pub selected_path_index: Option, + pub solution: Option, + pub assumption_usage: AssumptionUsageReport, + pub assumption_conditioned: bool, + pub summary_conditioned: bool, + pub verification: SolveVerification, + pub witness: SolveWitness, + pub stats: ExploreStats, + pub solver_stats: SolverStats, +} + +/// Function-level symbolic summary with collected paths. +#[derive(Debug)] +pub struct SymbolicFunctionSummary<'ctx> { + pub completion: QueryCompletion, + pub paths: Vec>, + pub feasible_paths: usize, + pub stats: ExploreStats, + pub solver_stats: SolverStats, +} + +struct TargetQueryPaths<'ctx> { + selected_route: crate::TargetQueryRoutePlan, + compiled_precondition: Option, + matched_paths: Vec>, + exact_unsat: bool, + summary_conditioned: bool, +} + +fn completion_from_stats(stats: &ExploreStats) -> QueryCompletion { + if stats.timed_out || stats.max_states_exhausted { + QueryCompletion::BudgetExhausted + } else { + QueryCompletion::Complete + } +} + +fn condition_string<'ctx>(state: &SymState<'ctx>) -> String { + match state.constraints() { + [] => "true".to_string(), + [constraint] => constraint.simplify().to_string(), + _ => state.path_condition().simplify().to_string(), + } +} + +fn symbolic_condition_set<'ctx>(state: &SymState<'ctx>) -> SymbolicConditionSet { + let terms = state + .constraints() + .iter() + .map(|constraint| PathConditionTerm { + expr: constraint.simplify().to_string(), + }) + .collect(); + SymbolicConditionSet { + simplified: condition_string(state), + terms, + num_constraints: state.num_constraints(), + } +} + +fn condition_summary<'ctx>(path: &PathResult<'ctx>) -> PathConditionSummary { + let path_condition = symbolic_condition_set(&path.state); + PathConditionSummary { + final_pc: path.final_pc(), + depth: path.depth, + exit_status: path.exit_status.clone(), + feasible: path.feasible, + num_constraints: path.num_constraints(), + condition: path_condition.simplified.clone(), + path_condition, + } +} + +enum PreconditionApplication<'ctx> { + Continue { + initial_state: Box>, + narrowed_state: Option>>, + compiled_precondition: Option, + summary_conditioned: bool, + }, + ExactUnsat { + compiled_precondition: BackwardConditionSummary, + summary_conditioned: bool, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CompiledPreconditionMode { + Necessary, + NarrowOnly, +} + +fn compiled_mode_from_guidance(mode: crate::QueryGuidanceMode) -> CompiledPreconditionMode { + match mode { + crate::QueryGuidanceMode::Necessary => CompiledPreconditionMode::Necessary, + crate::QueryGuidanceMode::NarrowOnly => CompiledPreconditionMode::NarrowOnly, + } +} + +fn build_target_query_inputs<'a>( + explorer: &mut PathExplorer<'_>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + artifact: Option<&'a SemanticArtifact>, + target_addr: u64, + assumption_conflicted: bool, + allow_continuation_bridge: bool, +) -> TargetQueryRouteInput<'a> { + let mut inputs = artifact + .map(|artifact| artifact.target_query_route_input(target_addr, assumption_conflicted)) + .unwrap_or_else(|| TargetQueryRouteInput { + route: crate::TargetQueryRoutePlan::dynamic_fallback(), + condition_source: None, + memory_terms: Vec::new(), + allow_exact_proof: !assumption_conflicted, + }); + if allow_continuation_bridge + && let Some(bridge_target) = + explorer.exception_bridge_target_in_scope(func, scope, target_addr) + && bridge_target != target_addr + && !matches!( + inputs.route.execution, + TargetQueryExecutionRoute::Refuse { .. } + ) + { + inputs.route.execution = TargetQueryExecutionRoute::ContinuationSeeded { + bridge_target, + route: Box::new(inputs.route.execution.clone()), + }; + debug_query_route_log(&format!( + "target=0x{target_addr:x} route=ContinuationSeeded bridge=0x{bridge_target:x}" + )); + } else { + debug_query_route_log(&format!( + "target=0x{target_addr:x} route={:?}", + inputs.route.execution + )); + } + inputs +} + +pub fn selected_target_query_route_in_scope( + explorer: &mut PathExplorer<'_>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + artifact: Option<&SemanticArtifact>, + target_addr: u64, + assumption_conflicted: bool, +) -> crate::TargetQueryRoutePlan { + build_target_query_inputs( + explorer, + func, + scope, + artifact, + target_addr, + assumption_conflicted, + true, + ) + .route +} + +fn route_uses_summary_guidance( + route: &TargetQueryExecutionRoute, + has_derived_summaries: bool, +) -> bool { + if !has_derived_summaries { + return false; + } + match route { + TargetQueryExecutionRoute::ContinuationSeeded { route, .. } => { + route_uses_summary_guidance(route, has_derived_summaries) + } + TargetQueryExecutionRoute::ArtifactCondition { .. } + | TargetQueryExecutionRoute::DynamicTargetCompile { .. } + | TargetQueryExecutionRoute::VmTargetCompile { .. } => true, + TargetQueryExecutionRoute::ArtifactMemoryOnly + | TargetQueryExecutionRoute::ResidualOnly { .. } + | TargetQueryExecutionRoute::Refuse { .. } => false, + } +} + +fn state_is_continuation_seed<'ctx>(state: &SymState<'ctx>) -> bool { + state.pending_exception().is_some() || state.runtime_region_for_pc(state.pc).is_some() +} + +fn stats_show_summary_guidance(stats: &ExploreStats) -> bool { + stats.target_summary_rank_hits > 0 || stats.target_pruned_summary_contradiction > 0 +} + +#[cfg_attr(test, allow(dead_code))] +#[cfg(test)] +fn precision_rank(precision: BackwardConditionPrecision) -> u8 { + match precision { + BackwardConditionPrecision::Exact => 3, + BackwardConditionPrecision::OverApprox => 2, + BackwardConditionPrecision::ResidualSearchRequired => 1, + BackwardConditionPrecision::Unsupported => 0, + } +} + +fn vm_exit_guard_allows_target( + bindings: &BTreeMap, + arm: &crate::VmTransferArm, + target_addr: u64, +) -> bool { + let guards = arm + .exit_guards + .iter() + .filter(|guard| guard.target == target_addr) + .collect::>(); + if guards.is_empty() { + return true; + } + let mut saw_reliable = false; + for guarded in guards { + let evidence = guarded.guard.evidence(); + if !evidence.is_usable() { + return true; + } + if !evidence.allows_narrowing() { + if matches!(guarded.guard.evaluate(bindings), Some(true)) { + return true; + } + continue; + } + saw_reliable = true; + match guarded.guard.evaluate(bindings) { + Some(true) => return true, + Some(false) => {} + None => return true, + } + } + !saw_reliable +} + +fn vm_arm_reaches_target( + bindings: &BTreeMap, + arm: &crate::VmTransferArm, + target_addr: u64, +) -> bool { + arm.handler_target == target_addr + || arm.region_blocks.contains(&target_addr) + || (arm.exit_targets.contains(&target_addr) + && vm_exit_guard_allows_target(bindings, arm, target_addr)) +} + +fn vm_arm_for_case(vm_step: &VmStepSummary, case_value: u64) -> Option<&crate::VmTransferArm> { + vm_step + .transfers + .iter() + .find(|arm| arm.case_values.contains(&case_value)) +} + +fn vm_selector_bindings(vm_step: &VmStepSummary, case_value: u64) -> BTreeMap { + let mut bindings = BTreeMap::new(); + if let Some(selector) = vm_step.selector.as_ref() { + bindings.insert(selector.clone(), case_value); + } + bindings +} + +fn vm_apply_confident_state_updates( + bindings: &BTreeMap, + updates: &[crate::VmStateUpdate], +) -> BTreeMap { + let mut next = bindings.clone(); + for update in updates { + if !update.evidence().allows_narrowing() { + continue; + } + if let Some(value) = update.value.evaluate_u64(&next) { + next.insert(update.output.clone(), value); + } + } + next +} + +fn vm_apply_confident_memory_writes( + bindings: &BTreeMap, + writes: &[crate::VmMemoryCondition], +) -> BTreeMap { + let mut next = bindings.clone(); + for write in writes { + if !write.evidence().allows_narrowing() { + continue; + } + let Some(binding) = write.binding.as_ref() else { + continue; + }; + let Some(value) = write.value.as_ref() else { + continue; + }; + let Some(value) = value.evaluate_u64(&next) else { + continue; + }; + next.insert(binding.clone(), value); + } + next +} + +fn vm_next_case_value( + bindings: &BTreeMap, + arm: &crate::VmTransferArm, + selector_name: Option<&str>, + dispatch_header: u64, + loop_header: u64, +) -> Option<(u64, BTreeMap)> { + if !arm.redispatch || arm.truncated { + return None; + } + let can_redispatch = arm.exit_targets.iter().any(|target| { + (*target == dispatch_header || *target == loop_header) + && vm_exit_guard_allows_target(bindings, arm, *target) + }) || arm.exit_targets.is_empty(); + if !can_redispatch { + return None; + } + let mut next_bindings = vm_apply_confident_state_updates(bindings, &arm.state_updates); + let selector_update = arm.selector_update.as_ref()?; + if !selector_update.evidence().allows_narrowing() { + return None; + } + let next_case = selector_update.value.evaluate_u64(&next_bindings)?; + next_bindings = vm_apply_confident_memory_writes(&next_bindings, &arm.memory_writes); + next_bindings.insert(selector_update.output.clone(), next_case); + if let Some(selector_name) = selector_name { + next_bindings.insert(selector_name.to_string(), next_case); + } + Some((next_case, next_bindings)) +} + +fn vm_case_reaches_target(vm_step: &VmStepSummary, initial_case: u64, target_addr: u64) -> bool { + let initial_bindings = vm_selector_bindings(vm_step, initial_case); + let mut queue = VecDeque::from([(initial_case, initial_bindings, 0usize)]); + let mut seen = BTreeSet::new(); + + while let Some((case_value, bindings, depth)) = queue.pop_front() { + let binding_key = bindings + .iter() + .map(|(name, value)| (name.clone(), *value)) + .collect::>(); + if !seen.insert((case_value, binding_key)) { + continue; + } + let Some(arm) = vm_arm_for_case(vm_step, case_value) else { + continue; + }; + if vm_arm_reaches_target(&bindings, arm, target_addr) { + return true; + } + if depth >= MAX_VM_COMPILED_STEPS.saturating_sub(1) || !arm.redispatch { + continue; + } + let Some((next_case, next_bindings)) = vm_next_case_value( + &bindings, + arm, + vm_step.selector.as_deref(), + vm_step.dispatch_header, + vm_step.loop_header, + ) else { + continue; + }; + queue.push_back((next_case, next_bindings, depth + 1)); + } + + false +} + +fn vm_target_case_values(vm_step: &VmStepSummary, target_addr: u64) -> Vec { + let mut case_values = BTreeSet::new(); + + for arm in &vm_step.transfers { + for case_value in arm.case_values.iter().copied() { + if vm_case_reaches_target(vm_step, case_value, target_addr) { + case_values.insert(case_value); + } + } + } + + case_values.into_iter().collect() +} + +fn compile_vm_target_precondition_with_summaries<'ctx>( + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + target_addr: u64, + call_summaries: &std::collections::HashMap>, +) -> Option { + let interpreter = classify_interpreter_like(func)?; + let vm_step = build_vm_step_summary(func, &interpreter)?; + let case_values = vm_target_case_values(&vm_step, target_addr); + if case_values.is_empty() { + return None; + } + let selector = func + .function() + .infer_switch_selector_var(vm_step.dispatch_header)?; + let ctx = initial_state.context(); + compile_value_postcondition_with_summaries( + func, + initial_state, + vm_step.dispatch_header, + selector, + { + let case_values = case_values.clone(); + move |value| { + let selector_bv = value.to_bv(ctx); + let disjuncts = case_values + .iter() + .map(|value| { + selector_bv.eq(z3::ast::BV::from_u64(*value, selector_bv.get_size())) + }) + .collect::>(); + match disjuncts.as_slice() { + [] => z3::ast::Bool::from_bool(false), + [single] => single.clone(), + _ => { + let refs = disjuncts.iter().collect::>(); + z3::ast::Bool::or(&refs) + } + } + } + }, + call_summaries, + ) +} + +#[cfg_attr(test, allow(dead_code))] +#[cfg(test)] +fn prefer_compiled_precondition( + current: Option<&CompiledBackwardCondition>, + candidate: &CompiledBackwardCondition, +) -> bool { + let Some(current) = current else { + return true; + }; + let current_rank = precision_rank(current.summary.precision); + let candidate_rank = precision_rank(candidate.summary.precision); + if candidate_rank != current_rank { + return candidate_rank > current_rank; + } + if candidate.summary.backward_memory_residual_fallbacks + != current.summary.backward_memory_residual_fallbacks + { + return candidate.summary.backward_memory_residual_fallbacks + < current.summary.backward_memory_residual_fallbacks; + } + if candidate.summary.memory_terms.len() != current.summary.memory_terms.len() { + return candidate.summary.memory_terms.len() > current.summary.memory_terms.len(); + } + if candidate.summary.supported_paths != current.summary.supported_paths { + return candidate.summary.supported_paths > current.summary.supported_paths; + } + candidate.summary.total_paths < current.summary.total_paths +} + +fn parse_const_u64_text(text: &str) -> Option { + let trimmed = text.trim(); + if let Some(hex) = trimmed.strip_prefix("0x") { + return u64::from_str_radix(hex, 16).ok(); + } + if let Some(hex) = trimmed.strip_prefix("const:") { + return u64::from_str_radix(hex, 16).ok(); + } + trimmed.parse::().ok() +} + +fn constrain_region_backed_memory_term<'ctx>( + narrowed: &mut SymState<'ctx>, + term: &crate::backward::BackwardMemoryCondition, + rhs: u64, +) -> bool { + if !term.exact_value { + return false; + } + let (base_addr, offset_values) = match &term.region { + crate::backward::BackwardMemoryRegion::Region(region) + if term.offset_hi >= term.offset_lo => + { + let Some(def) = narrowed.memory.region_def(region.id) else { + return false; + }; + let Some(base_addr) = def.base_addr else { + return false; + }; + let offset_values = if term.exact_offset || term.offset_lo == term.offset_hi { + vec![term.offset_lo] + } else { + let span = term.offset_hi - term.offset_lo; + if span > 8 { + return false; + } + (term.offset_lo..=term.offset_hi).collect::>() + }; + (base_addr, offset_values) + } + _ => return false, + }; + let mut predicates = Vec::new(); + for offset in offset_values { + let Some(addr) = base_addr.checked_add_signed(offset) else { + continue; + }; + let value = narrowed.mem_read(&crate::SymValue::concrete(addr, 64), term.size); + predicates.push( + value + .to_bv(narrowed.context()) + .eq(BV::from_u64(rhs, value.bits())), + ); + } + match predicates.as_slice() { + [] => false, + [single] => { + narrowed.add_constraint(single.clone()); + true + } + _ => { + let refs = predicates.iter().collect::>(); + narrowed.add_constraint(Bool::or(&refs)); + true + } + } +} + +fn apply_memory_term_narrowing<'ctx>( + state: &SymState<'ctx>, + terms: &[&crate::backward::BackwardMemoryCondition], +) -> Option>> { + let mut narrowed = state.fork(); + let mut applied = 0usize; + for term in terms { + if !term.evidence().allows_narrowing() || !term.exact_value { + continue; + } + let Some(value_expr) = term.value_expr.as_deref() else { + continue; + }; + let Some(rhs) = parse_const_u64_text(value_expr) else { + continue; + }; + if let Some(binding) = term.binding.as_ref() + && let Some(value) = narrowed.symbolic_inputs().get(binding).cloned() + { + narrowed.constrain_eq(&value, rhs); + applied += 1; + continue; + } + if constrain_region_backed_memory_term(&mut narrowed, term, rhs) { + applied += 1; + } + } + (applied > 0).then_some(Box::new(narrowed)) +} + +fn apply_compiled_precondition_with_mode<'ctx>( + explorer: &PathExplorer<'ctx>, + mut initial_state: SymState<'ctx>, + compiled: crate::CompiledBackwardCondition, + mode: CompiledPreconditionMode, + allow_exact_proof: bool, +) -> PreconditionApplication<'ctx> { + let summary = compiled.summary.clone(); + let evidence = summary.evidence(); + match explorer + .solver() + .sat_with_constraint(&initial_state, &compiled.predicate) + { + SatResult::Unsat + if mode == CompiledPreconditionMode::Necessary + && allow_exact_proof + && evidence.allows_hard_proof() => + { + PreconditionApplication::ExactUnsat { + compiled_precondition: summary, + summary_conditioned: false, + } + } + SatResult::Sat => { + if mode == CompiledPreconditionMode::Necessary + && allow_exact_proof + && evidence.allows_hard_proof() + { + initial_state.add_constraint(compiled.predicate); + PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state: None, + compiled_precondition: Some(summary), + summary_conditioned: false, + } + } else if evidence.allows_narrowing() { + let mut narrowed_state = initial_state.fork(); + narrowed_state.add_constraint(compiled.predicate); + PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state: Some(Box::new(narrowed_state)), + compiled_precondition: Some(summary), + summary_conditioned: false, + } + } else { + PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state: None, + compiled_precondition: Some(summary), + summary_conditioned: false, + } + } + } + SatResult::Unknown | SatResult::Unsat => PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state: None, + compiled_precondition: Some(summary), + summary_conditioned: false, + }, + } +} + +fn find_paths_with_compiled_narrowing<'ctx, F>( + explorer: &mut PathExplorer<'ctx>, + initial_state: SymState<'ctx>, + narrowed_state: Option>>, + mut search: F, +) -> Vec> +where + F: FnMut(&mut PathExplorer<'ctx>, SymState<'ctx>) -> Vec>, +{ + if let Some(narrowed_state) = narrowed_state { + let matches = search(explorer, *narrowed_state); + if !matches.is_empty() || explorer.budget_exhausted() { + return matches; + } + } + search(explorer, initial_state) +} + +fn find_first_path_with_compiled_narrowing<'ctx, F>( + explorer: &mut PathExplorer<'ctx>, + initial_state: SymState<'ctx>, + narrowed_state: Option>>, + mut search: F, +) -> Option> +where + F: FnMut(&mut PathExplorer<'ctx>, SymState<'ctx>) -> Option>, +{ + if let Some(narrowed_state) = narrowed_state { + let matched = search(explorer, *narrowed_state); + if matched.is_some() || explorer.budget_exhausted() { + return matched; + } + } + search(explorer, initial_state) +} + +fn apply_best_compiled_precondition_for_inputs<'ctx>( + explorer: &PathExplorer<'ctx>, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + target_addr: u64, + query_inputs: &TargetQueryRouteInput<'_>, +) -> PreconditionApplication<'ctx> { + let derived_summaries = explorer.derived_call_summary_views(); + let memory_terms = query_inputs.memory_terms.clone(); + let target_is_local = func.get_block(target_addr).is_some(); + let route_summary_conditioned = + route_uses_summary_guidance(&query_inputs.route.execution, !derived_summaries.is_empty()); + match query_inputs.route.execution { + TargetQueryExecutionRoute::ContinuationSeeded { .. } => PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state: None, + compiled_precondition: None, + summary_conditioned: route_summary_conditioned, + }, + TargetQueryExecutionRoute::ArtifactCondition { mode } => { + let compiled = query_inputs.condition_source.and_then(|source| { + compile_branch_precondition_with_summaries( + func, + &initial_state, + source.block_addr, + source.branch_truth, + &derived_summaries, + ) + .map(|compiled| (compiled, mode)) + }); + if compiled.is_none() + && let Some(narrowed_state) = + apply_memory_term_narrowing(&initial_state, &memory_terms) + { + let compiled_precondition = query_inputs + .condition_source + .map(|source| source.summary) + .filter(|summary| !summary.evidence().allows_hard_proof()) + .cloned(); + return PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state: Some(narrowed_state), + compiled_precondition, + summary_conditioned: route_summary_conditioned, + }; + } + let Some((compiled, mode)) = compiled else { + let narrowed_state = apply_memory_term_narrowing(&initial_state, &memory_terms); + return PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state, + compiled_precondition: None, + summary_conditioned: route_summary_conditioned, + }; + }; + match apply_compiled_precondition_with_mode( + explorer, + initial_state, + compiled, + compiled_mode_from_guidance(mode), + query_inputs.allow_exact_proof, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + .. + } => { + let narrowed_state = narrowed_state.or_else(|| { + apply_memory_term_narrowing(initial_state.as_ref(), &memory_terms) + }); + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + summary_conditioned: route_summary_conditioned, + } + } + PreconditionApplication::ExactUnsat { + compiled_precondition, + .. + } => PreconditionApplication::ExactUnsat { + compiled_precondition, + summary_conditioned: route_summary_conditioned, + }, + } + } + TargetQueryExecutionRoute::ArtifactMemoryOnly => { + let narrowed_state = apply_memory_term_narrowing(&initial_state, &memory_terms); + let compiled_precondition = query_inputs + .condition_source + .map(|source| source.summary) + .filter(|summary| !summary.evidence().allows_hard_proof()) + .cloned(); + PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state, + compiled_precondition, + summary_conditioned: false, + } + } + TargetQueryExecutionRoute::DynamicTargetCompile { reason: _, mode } => { + if !target_is_local { + let narrowed_state = apply_memory_term_narrowing(&initial_state, &memory_terms); + return PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state, + compiled_precondition: None, + summary_conditioned: false, + }; + } + let compiled = compile_target_precondition_with_summaries( + func, + &initial_state, + target_addr, + &derived_summaries, + ); + match compiled { + Some(compiled) => match apply_compiled_precondition_with_mode( + explorer, + initial_state, + compiled, + compiled_mode_from_guidance(mode), + query_inputs.allow_exact_proof, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + .. + } => { + let narrowed_state = narrowed_state.or_else(|| { + apply_memory_term_narrowing(initial_state.as_ref(), &memory_terms) + }); + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + summary_conditioned: route_summary_conditioned, + } + } + PreconditionApplication::ExactUnsat { + compiled_precondition, + .. + } => PreconditionApplication::ExactUnsat { + compiled_precondition, + summary_conditioned: route_summary_conditioned, + }, + }, + None => { + let narrowed_state = apply_memory_term_narrowing(&initial_state, &memory_terms); + PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state, + compiled_precondition: None, + summary_conditioned: false, + } + } + } + } + TargetQueryExecutionRoute::VmTargetCompile { reason: _ } => { + if !target_is_local { + let narrowed_state = apply_memory_term_narrowing(&initial_state, &memory_terms); + return PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state, + compiled_precondition: None, + summary_conditioned: false, + }; + } + let compiled = compile_vm_target_precondition_with_summaries( + func, + &initial_state, + target_addr, + &derived_summaries, + ); + match compiled { + Some(compiled) => match apply_compiled_precondition_with_mode( + explorer, + initial_state, + compiled, + CompiledPreconditionMode::Necessary, + query_inputs.allow_exact_proof, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + .. + } => { + let narrowed_state = narrowed_state.or_else(|| { + apply_memory_term_narrowing(initial_state.as_ref(), &memory_terms) + }); + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + summary_conditioned: route_summary_conditioned, + } + } + PreconditionApplication::ExactUnsat { + compiled_precondition, + .. + } => PreconditionApplication::ExactUnsat { + compiled_precondition, + summary_conditioned: route_summary_conditioned, + }, + }, + None => { + let narrowed_state = apply_memory_term_narrowing(&initial_state, &memory_terms); + PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state, + compiled_precondition: None, + summary_conditioned: false, + } + } + } + } + TargetQueryExecutionRoute::ResidualOnly { .. } + | TargetQueryExecutionRoute::Refuse { .. } => PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state: None, + compiled_precondition: None, + summary_conditioned: false, + }, + } +} + +#[cfg(test)] +fn apply_best_compiled_precondition<'ctx>( + explorer: &PathExplorer<'ctx>, + func: &SsaArtifact, + artifact: Option<&SemanticArtifact>, + initial_state: SymState<'ctx>, + target_addr: u64, + assumption_conflicted: bool, +) -> PreconditionApplication<'ctx> { + let query_inputs = artifact + .map(|artifact| artifact.target_query_route_input(target_addr, assumption_conflicted)) + .unwrap_or_else(|| TargetQueryRouteInput { + route: crate::TargetQueryRoutePlan::dynamic_fallback(), + condition_source: None, + memory_terms: Vec::new(), + allow_exact_proof: !assumption_conflicted, + }); + apply_best_compiled_precondition_for_inputs( + explorer, + func, + initial_state, + target_addr, + &query_inputs, + ) +} + +fn execute_target_query_paths_from_inputs<'ctx>( + explorer: &mut PathExplorer<'ctx>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + target_addr: u64, + query_inputs: &TargetQueryRouteInput<'_>, +) -> TargetQueryPaths<'ctx> { + let selected_route = query_inputs.route.clone(); + match apply_best_compiled_precondition_for_inputs( + explorer, + func, + initial_state, + target_addr, + query_inputs, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + summary_conditioned, + } => TargetQueryPaths { + selected_route, + compiled_precondition, + matched_paths: find_paths_with_compiled_narrowing( + explorer, + *initial_state, + narrowed_state, + |explorer, state| explorer.find_paths_to_in_scope(func, scope, state, target_addr), + ), + exact_unsat: false, + summary_conditioned, + }, + PreconditionApplication::ExactUnsat { + compiled_precondition, + summary_conditioned, + } => TargetQueryPaths { + selected_route, + compiled_precondition: Some(compiled_precondition), + matched_paths: Vec::new(), + exact_unsat: true, + summary_conditioned, + }, + } +} + +fn execute_target_query_first_path_from_inputs<'ctx>( + explorer: &mut PathExplorer<'ctx>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + target_addr: u64, + query_inputs: &TargetQueryRouteInput<'_>, +) -> TargetQueryPaths<'ctx> { + let selected_route = query_inputs.route.clone(); + match apply_best_compiled_precondition_for_inputs( + explorer, + func, + initial_state, + target_addr, + query_inputs, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + summary_conditioned, + } => TargetQueryPaths { + selected_route, + compiled_precondition, + matched_paths: find_first_path_with_compiled_narrowing( + explorer, + *initial_state, + narrowed_state, + |explorer, state| explorer.find_path_to_in_scope(func, scope, state, target_addr), + ) + .into_iter() + .collect(), + exact_unsat: false, + summary_conditioned, + }, + PreconditionApplication::ExactUnsat { + compiled_precondition, + summary_conditioned, + } => TargetQueryPaths { + selected_route, + compiled_precondition: Some(compiled_precondition), + matched_paths: Vec::new(), + exact_unsat: true, + summary_conditioned, + }, + } +} + +fn continuation_seed_states<'ctx>( + explorer: &mut PathExplorer<'ctx>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + bridge_paths: Vec>, +) -> Vec> { + let mut seeds = Vec::new(); + for path in bridge_paths { + if let Ok(next_states) = explorer.advance_current_block_in_scope(func, scope, path.state) { + seeds.extend(next_states.into_iter().filter(state_is_continuation_seed)); + } + if explorer.budget_exhausted() { + break; + } + } + seeds +} + +fn rank_continuation_seeds<'ctx>(mut seeds: Vec>) -> Vec> { + seeds.sort_by_key(|state| (state.num_constraints(), state.depth, state.pc)); + seeds.truncate(CONTINUATION_SEEDED_STATE_CAP); + seeds +} + +fn continuation_followup_execution_route( + route: &TargetQueryExecutionRoute, +) -> TargetQueryExecutionRoute { + match route { + TargetQueryExecutionRoute::DynamicTargetCompile { reason, .. } => { + TargetQueryExecutionRoute::ResidualOnly { + reasons: vec![ + format!( + "continuation-seeded follow-up downgraded dynamic target compile: {reason}" + ), + "continuation-seeded runtime execution".to_string(), + ], + } + } + TargetQueryExecutionRoute::VmTargetCompile { reason } => { + TargetQueryExecutionRoute::ResidualOnly { + reasons: vec![ + format!("continuation-seeded follow-up downgraded vm target compile: {reason}"), + "continuation-seeded runtime execution".to_string(), + ], + } + } + other => other.clone(), + } +} + +fn execute_target_query_paths<'ctx>( + explorer: &mut PathExplorer<'ctx>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + artifact: Option<&SemanticArtifact>, + initial_state: SymState<'ctx>, + target_addr: u64, + assumption_conflicted: bool, +) -> TargetQueryPaths<'ctx> { + let query_inputs = build_target_query_inputs( + explorer, + func, + scope, + artifact, + target_addr, + assumption_conflicted, + true, + ); + if let TargetQueryExecutionRoute::ContinuationSeeded { + bridge_target, + route, + } = &query_inputs.route.execution + { + let mut bridge_inputs = build_target_query_inputs( + explorer, + func, + scope, + artifact, + *bridge_target, + assumption_conflicted, + false, + ); + bridge_inputs.route.execution = TargetQueryExecutionRoute::ResidualOnly { + reasons: vec!["continuation bridge search".to_string()], + }; + let bridge_paths = explorer.with_prune_infeasible(false, |explorer| { + execute_target_query_paths_from_inputs( + explorer, + func, + scope, + initial_state, + *bridge_target, + &bridge_inputs, + ) + }); + let mut summary_conditioned = bridge_paths.summary_conditioned; + if bridge_paths.exact_unsat || bridge_paths.matched_paths.is_empty() { + return TargetQueryPaths { + selected_route: query_inputs.route.clone(), + compiled_precondition: bridge_paths.compiled_precondition, + matched_paths: Vec::new(), + exact_unsat: bridge_paths.exact_unsat, + summary_conditioned, + }; + } + + let bridge_compiled_precondition = bridge_paths.compiled_precondition; + let seeded_states = rank_continuation_seeds(continuation_seed_states( + explorer, + func, + scope, + bridge_paths.matched_paths, + )); + if seeded_states.is_empty() { + return TargetQueryPaths { + selected_route: query_inputs.route.clone(), + compiled_precondition: bridge_compiled_precondition, + matched_paths: Vec::new(), + exact_unsat: false, + summary_conditioned, + }; + } + + let mut final_inputs = query_inputs.clone(); + final_inputs.route.execution = continuation_followup_execution_route(route); + let mut matched_paths = Vec::new(); + let mut compiled_precondition = None; + let mut any_exact_unsat = false; + for state in seeded_states { + let next = explorer.with_exception_bridge_guidance(false, |explorer| { + execute_target_query_paths_from_inputs( + explorer, + func, + scope, + state, + target_addr, + &final_inputs, + ) + }); + if compiled_precondition.is_none() { + compiled_precondition = next.compiled_precondition; + } + summary_conditioned |= next.summary_conditioned; + any_exact_unsat |= next.exact_unsat; + matched_paths.extend(next.matched_paths); + if explorer.budget_exhausted() { + break; + } + } + let no_matches = matched_paths.is_empty(); + + return TargetQueryPaths { + selected_route: query_inputs.route, + compiled_precondition: compiled_precondition.or(bridge_compiled_precondition), + matched_paths, + exact_unsat: no_matches && any_exact_unsat, + summary_conditioned, + }; + } + + execute_target_query_paths_from_inputs( + explorer, + func, + scope, + initial_state, + target_addr, + &query_inputs, + ) +} + +fn execute_target_query_first_path<'ctx>( + explorer: &mut PathExplorer<'ctx>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + artifact: Option<&SemanticArtifact>, + initial_state: SymState<'ctx>, + target_addr: u64, + assumption_conflicted: bool, +) -> TargetQueryPaths<'ctx> { + let query_inputs = build_target_query_inputs( + explorer, + func, + scope, + artifact, + target_addr, + assumption_conflicted, + true, + ); + if let TargetQueryExecutionRoute::ContinuationSeeded { + bridge_target, + route, + } = &query_inputs.route.execution + { + debug_query_phase_log(&format!( + "continuation:start target=0x{target_addr:x} bridge=0x{bridge_target:x}" + )); + let mut bridge_inputs = build_target_query_inputs( + explorer, + func, + scope, + artifact, + *bridge_target, + assumption_conflicted, + false, + ); + bridge_inputs.route.execution = TargetQueryExecutionRoute::ResidualOnly { + reasons: vec!["continuation bridge search".to_string()], + }; + debug_query_phase_log(&format!( + "continuation:bridge_search target=0x{bridge_target:x} route={:?}", + bridge_inputs.route.execution + )); + let bridge_selected_route = bridge_inputs.route.clone(); + let bridge_paths = explorer.with_prune_infeasible(false, |explorer| TargetQueryPaths { + selected_route: bridge_selected_route, + compiled_precondition: None, + matched_paths: explorer + .find_path_to_in_scope(func, scope, initial_state, *bridge_target) + .into_iter() + .collect(), + exact_unsat: false, + summary_conditioned: false, + }); + debug_query_phase_log(&format!( + "continuation:bridge_done target=0x{bridge_target:x} matched={} exact_unsat={} budget_exhausted={}", + bridge_paths.matched_paths.len(), + bridge_paths.exact_unsat, + explorer.budget_exhausted(), + )); + let mut summary_conditioned = bridge_paths.summary_conditioned; + let Some(bridge_path) = bridge_paths.matched_paths.into_iter().next() else { + return TargetQueryPaths { + selected_route: query_inputs.route.clone(), + compiled_precondition: bridge_paths.compiled_precondition, + matched_paths: Vec::new(), + exact_unsat: bridge_paths.exact_unsat, + summary_conditioned, + }; + }; + + let bridge_compiled_precondition = bridge_paths.compiled_precondition; + let seeded_states = rank_continuation_seeds(continuation_seed_states( + explorer, + func, + scope, + vec![bridge_path], + )); + debug_query_phase_log(&format!( + "continuation:seeded count={} budget_exhausted={}", + seeded_states.len(), + explorer.budget_exhausted(), + )); + let Some(state) = seeded_states.into_iter().next() else { + return TargetQueryPaths { + selected_route: query_inputs.route.clone(), + compiled_precondition: bridge_compiled_precondition, + matched_paths: Vec::new(), + exact_unsat: false, + summary_conditioned, + }; + }; + + let mut final_inputs = query_inputs.clone(); + final_inputs.route.execution = continuation_followup_execution_route(route); + debug_query_phase_log(&format!( + "continuation:final_search target=0x{target_addr:x} route={:?}", + final_inputs.route.execution + )); + let next = explorer.with_exception_bridge_guidance(false, |explorer| { + execute_target_query_first_path_from_inputs( + explorer, + func, + scope, + state, + target_addr, + &final_inputs, + ) + }); + debug_query_phase_log(&format!( + "continuation:final_done target=0x{target_addr:x} matched={} exact_unsat={} budget_exhausted={}", + next.matched_paths.len(), + next.exact_unsat, + explorer.budget_exhausted(), + )); + summary_conditioned |= next.summary_conditioned; + return TargetQueryPaths { + selected_route: query_inputs.route, + compiled_precondition: next.compiled_precondition.or(bridge_compiled_precondition), + matched_paths: next.matched_paths, + exact_unsat: next.exact_unsat, + summary_conditioned, + }; + } + + execute_target_query_first_path_from_inputs( + explorer, + func, + scope, + initial_state, + target_addr, + &query_inputs, + ) +} + +impl<'ctx> PathExplorer<'ctx> { + /// Ask whether a target address is reachable and collect the matching paths. + pub fn can_reach( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> ReachabilityResult<'ctx> { + self.can_reach_with_artifact_in_scope(func, None, None, initial_state, target_addr) + } + + pub fn can_reach_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> ReachabilityResult<'ctx> { + self.can_reach_with_artifact_in_scope(func, scope, None, initial_state, target_addr) + } + + pub fn can_reach_with_artifact( + &mut self, + func: &SsaArtifact, + artifact: Option<&SemanticArtifact>, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> ReachabilityResult<'ctx> { + self.can_reach_with_artifact_in_scope(func, None, artifact, initial_state, target_addr) + } + + pub fn can_reach_with_artifact_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + artifact: Option<&SemanticArtifact>, + mut initial_state: SymState<'ctx>, + target_addr: u64, + ) -> ReachabilityResult<'ctx> { + let assumption_outcome = assumption_usage_for_query(self, func, &mut initial_state); + let query = execute_target_query_paths( + self, + func, + scope, + artifact, + initial_state, + target_addr, + assumption_outcome.conflicted, + ); + let stats = self.stats().clone(); + let solver_stats = self.solver().stats(); + let summary_conditioned = query.summary_conditioned || stats_show_summary_guidance(&stats); + let status = if !query.matched_paths.is_empty() { + ReachabilityStatus::Reachable + } else if self.budget_exhausted() { + ReachabilityStatus::BudgetExhausted + } else { + ReachabilityStatus::Unreachable + }; + ReachabilityResult { + status, + target_addr, + selected_route: query.selected_route, + compiled_precondition: query.compiled_precondition, + paths: query.matched_paths, + assumption_usage: assumption_outcome.usage, + assumption_conditioned: assumption_outcome.conditioned, + summary_conditioned, + stats, + solver_stats, + } + } + + /// Collect path conditions for states that reach a target PC. + pub fn path_conditions_at( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + target_pc: u64, + ) -> PathConditionResult<'ctx> { + self.path_conditions_at_with_artifact_in_scope(func, None, None, initial_state, target_pc) + } + + pub fn path_conditions_at_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + target_pc: u64, + ) -> PathConditionResult<'ctx> { + self.path_conditions_at_with_artifact_in_scope(func, scope, None, initial_state, target_pc) + } + + pub fn path_conditions_at_with_artifact( + &mut self, + func: &SsaArtifact, + artifact: Option<&SemanticArtifact>, + initial_state: SymState<'ctx>, + target_pc: u64, + ) -> PathConditionResult<'ctx> { + self.path_conditions_at_with_artifact_in_scope( + func, + None, + artifact, + initial_state, + target_pc, + ) + } + + pub fn path_conditions_at_with_artifact_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + artifact: Option<&SemanticArtifact>, + mut initial_state: SymState<'ctx>, + target_pc: u64, + ) -> PathConditionResult<'ctx> { + let assumption_outcome = assumption_usage_for_query(self, func, &mut initial_state); + let query = execute_target_query_paths( + self, + func, + scope, + artifact, + initial_state, + target_pc, + assumption_outcome.conflicted, + ); + let stats = self.stats().clone(); + let solver_stats = self.solver().stats(); + let summary_conditioned = query.summary_conditioned || stats_show_summary_guidance(&stats); + let conditions = query.matched_paths.iter().map(condition_summary).collect(); + PathConditionResult { + completion: completion_from_stats(&stats), + target_pc, + selected_route: query.selected_route, + compiled_precondition: query.compiled_precondition, + conditions, + matching_paths: query.matched_paths, + assumption_usage: assumption_outcome.usage, + assumption_conditioned: assumption_outcome.conditioned, + summary_conditioned, + stats, + solver_stats, + } + } + + /// Solve for a concrete model that reaches a target address. + pub fn solve_for_target( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> SolveResult<'ctx> { + self.solve_for_target_with_artifact_in_scope(func, None, None, initial_state, target_addr) + } + + pub fn solve_for_target_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> SolveResult<'ctx> { + self.solve_for_target_with_artifact_in_scope(func, scope, None, initial_state, target_addr) + } + + pub fn solve_for_target_with_artifact( + &mut self, + func: &SsaArtifact, + artifact: Option<&SemanticArtifact>, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> SolveResult<'ctx> { + self.solve_for_target_with_artifact_in_scope( + func, + None, + artifact, + initial_state, + target_addr, + ) + } + + pub fn solve_for_target_with_artifact_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + artifact: Option<&SemanticArtifact>, + mut initial_state: SymState<'ctx>, + target_addr: u64, + ) -> SolveResult<'ctx> { + let assumption_outcome = assumption_usage_for_query(self, func, &mut initial_state); + let validation_initial_state = initial_state.fork(); + let query = execute_target_query_first_path( + self, + func, + scope, + artifact, + initial_state, + target_addr, + assumption_outcome.conflicted, + ); + let stats = self.stats().clone(); + let solver_stats = self.solver().stats(); + let summary_conditioned = query.summary_conditioned || stats_show_summary_guidance(&stats); + let selected_path_index = query + .matched_paths + .iter() + .enumerate() + .min_by_key(|(idx, path)| (path.num_constraints(), path.depth, *idx)) + .map(|(idx, _)| idx); + let constraint_graph = selected_path_index + .map(|idx| &query.matched_paths[idx]) + .map(|path| { + build_final_constraint_graph_for_path( + func, + path, + &stats.runtime_loop_exact_recurrences, + target_addr, + None, + ) + }) + .unwrap_or_default(); + let solution = if solution_extraction_allowed(&query.selected_route, &stats) { + selected_path_index.and_then(|idx| self.solve_path(&query.matched_paths[idx])) + } else { + None + }; + let tactic_solution = if solution_extraction_allowed(&query.selected_route, &stats) { + selected_path_index.and_then(|idx| { + self.solve_path_with_constraint_graph_tactics( + &query.matched_paths[idx], + &constraint_graph, + &stats.runtime_loop_exact_recurrences, + ) + }) + } else { + None + }; + let solution = tactic_solution.or(solution); + let mut replay_backend = LiftedReplayBackend::new(self); + let (status, verification, witness) = verify_solve_result( + SolveVerificationRequest { + func, + scope, + selected_route: &query.selected_route, + stats: &stats, + validation_initial_state, + target_addr, + exact_unsat: query.exact_unsat, + selected_path_index, + solution: solution.as_ref(), + constraint_graph: &constraint_graph, + }, + &mut replay_backend, + ); + SolveResult { + status, + target_addr, + selected_route: query.selected_route, + compiled_precondition: query.compiled_precondition, + matched_paths: query.matched_paths, + selected_path_index, + solution, + assumption_usage: assumption_outcome.usage, + assumption_conditioned: assumption_outcome.conditioned, + summary_conditioned, + verification, + witness, + stats, + solver_stats, + } + } + + /// Explore a function and return a typed symbolic summary over its paths. + pub fn summarize_function( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + ) -> SymbolicFunctionSummary<'ctx> { + let paths = self.explore(func, initial_state); + let stats = self.stats().clone(); + let solver_stats = self.solver().stats(); + let feasible_paths = paths.iter().filter(|path| path.feasible).count(); + SymbolicFunctionSummary { + completion: completion_from_stats(&stats), + paths, + feasible_paths, + stats, + solver_stats, + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use super::{ + CompiledPreconditionMode, PreconditionApplication, apply_best_compiled_precondition, + apply_compiled_precondition_with_mode, prefer_compiled_precondition, vm_target_case_values, + }; + use crate::{ + ArtifactGranularity, BackwardConditionPrecision, BackwardConditionSummary, + BackwardMemoryCondition, BackwardMemoryRegion, ControlFact, ExecutionModel, Judged, + MemoryFact, NativeArtifactBody, NativeFunctionSummary, RefinementStage, RegionKey, + ResidualReason, SatResult, SemanticArtifact, SemanticArtifactBody, + SemanticArtifactDiagnostics, SemanticEvidence, SemanticEvidenceReason, SemanticRegion, + SliceClass, SymQueryConfig, SymState, TargetFact, TargetQueryExecutionRoute, + TargetQueryPlan, VmBinaryOp, VmGuardCondition, VmGuardedExit, VmStateUpdate, VmStepSummary, + VmTransferArm, VmValueExpr, + }; + use r2il::{R2ILBlock, R2ILOp, SpaceId, Varnode}; + use r2ssa::{ + AnalysisAssumption, AssumptionProvenance, AssumptionScope, AssumptionSet, + AssumptionSubject, AssumptionValue, SsaArtifact, + }; + use z3::Context; + use z3::ast::BV; + + const RDI: u64 = 56; + const TMP0: u64 = 0x80; + const TMP1: u64 = 0x88; + + fn test_semantic_artifact( + stage: RefinementStage, + slice_class: SliceClass, + residual_reasons: Vec, + regions: Vec, + diagnostics: SemanticArtifactDiagnostics, + ) -> SemanticArtifact { + let regions: BTreeMap = regions + .into_iter() + .map(|region| { + ( + RegionKey::new(region.anchor, region.frontier.clone()), + region, + ) + }) + .collect(); + let granularity = if matches!(stage, RefinementStage::Compiled | RefinementStage::Residual) + && !regions.is_empty() + { + ArtifactGranularity::Regioned + } else { + ArtifactGranularity::WholeFunction + }; + SemanticArtifact { + stage, + granularity, + execution: ExecutionModel::Native, + body: SemanticArtifactBody::Native(NativeArtifactBody { + summary: NativeFunctionSummary { + slice_class, + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: crate::sim::DerivedSummaryDiagnostics::default(), + }, + regions, + }), + diagnostics: SemanticArtifactDiagnostics { + residual_reasons, + ..diagnostics + }, + } + } + + fn make_region(anchor: u64, frontier: &[u64]) -> SemanticRegion { + SemanticRegion { + anchor, + frontier: frontier.iter().copied().collect(), + control: Vec::new(), + memory: Vec::new(), + pre: Vec::new(), + post: Vec::new(), + targets: Vec::new(), + } + } + + fn default_diagnostics() -> SemanticArtifactDiagnostics { + SemanticArtifactDiagnostics { + branches_evaluated: 0, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: false, + residual_reasons: Vec::new(), + ambiguous_targets: Vec::new(), + cache_hit: false, + } + } + + fn make_reg(offset: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Register, + offset, + size, + meta: None, + } + } + + fn make_const(value: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: value, + size, + meta: None, + } + } + + fn make_residual_precondition_blocks() -> Vec { + vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(TMP0, 1), + a: make_reg(RDI, 8), + b: make_const(1, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP0, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntCarry { + dst: make_reg(TMP1, 1), + a: make_reg(RDI, 8), + b: make_const(1, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ] + } + + fn make_bounded_copy_loop_blocks() -> Vec { + vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + space: SpaceId::Ram, + addr: make_const(0x2000, 8), + }, + R2ILOp::Store { + space: SpaceId::Ram, + addr: make_const(0x3000, 8), + val: make_reg(TMP0, 1), + }, + R2ILOp::IntLess { + dst: make_reg(TMP1, 1), + a: make_reg(RDI, 8), + b: make_const(0x1000, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1000, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ] + } + + fn make_vm_step_summary_with_transfers(transfers: Vec) -> VmStepSummary { + VmStepSummary { + kind: crate::InterpreterKind::SwitchDispatch, + loop_header: 0x1000, + dispatch_header: 0x1000, + selector: Some("RDI_0".to_string()), + dispatch_targets: vec![0x1004, 0x1008, 0x1010], + default_target: Some(0x1010), + case_values_by_target: BTreeMap::from([ + (0x1004, vec![0]), + (0x1008, vec![1]), + (0x1010, vec![2]), + ]), + loop_latches: vec![0x1000], + state_inputs: vec!["RDI_0".to_string()], + state_outputs: vec!["RDI_1".to_string()], + step_blocks: vec![0x1000, 0x1004, 0x1008, 0x1010], + handler_regions: BTreeMap::new(), + handler_state_inputs: BTreeMap::new(), + handler_state_outputs: BTreeMap::new(), + handler_state_updates: BTreeMap::new(), + handler_exit_guards: BTreeMap::new(), + handler_memory_read_effects: BTreeMap::new(), + handler_memory_write_effects: BTreeMap::new(), + handler_memory_reads: BTreeMap::new(), + handler_memory_writes: BTreeMap::new(), + handler_calls: BTreeMap::new(), + handler_conditional_branches: BTreeMap::new(), + handler_exit_targets: BTreeMap::new(), + redispatch_handlers: Vec::new(), + returning_handlers: Vec::new(), + truncated_handlers: Vec::new(), + transfers, + } + } + + #[test] + fn residual_compiled_preconditions_do_not_constrain_forward_search() { + let blocks = make_residual_precondition_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let original_constraints = state.num_constraints(); + match apply_best_compiled_precondition(&explorer, &func, None, state, 0x1010, false) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition: _, + .. + } => { + assert_eq!(initial_state.num_constraints(), original_constraints); + assert!(narrowed_state.is_none()); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("residual precondition should not shortcut as exact unsat") + } + } + } + + #[test] + fn non_local_targets_skip_dynamic_precondition_compile() { + let blocks = make_residual_precondition_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let original_constraints = state.num_constraints(); + match apply_best_compiled_precondition(&explorer, &func, None, state, 0x4000, false) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + .. + } => { + assert!(compiled_precondition.is_none()); + assert!(narrowed_state.is_none()); + assert_eq!(initial_state.num_constraints(), original_constraints); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("non-local targets should bypass dynamic backward compilation") + } + } + } + + #[test] + fn register_assumptions_condition_query_results() { + let blocks = make_residual_precondition_blocks(); + let base = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let reg_name = base + .graph() + .values + .iter() + .find(|value| value.var.version == 0 && value.var.is_register() && value.var.size == 8) + .expect("version-zero input register") + .var + .name + .clone(); + let func = base.with_assumptions(&AssumptionSet::new(vec![AnalysisAssumption { + id: Some("force-rdi".to_string()), + subject: AssumptionSubject::Register { name: reg_name }, + value: AssumptionValue::Constant { value: 1 }, + scope: AssumptionScope::Query, + provenance: AssumptionProvenance::User, + }])); + let (state_name, bits) = func + .facts() + .applied_assumption_bindings + .iter() + .find_map(|binding| match &binding.binding { + r2ssa::PreparedAssumptionBindingKind::Register { + state_name, bits, .. + } => Some((state_name.clone(), *bits)), + _ => None, + }) + .expect("register assumption binding"); + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic(&state_name, bits); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let reach = explorer.can_reach(&func, state, 0x1010); + assert_eq!(reach.status, super::ReachabilityStatus::Reachable); + assert!(reach.assumption_conditioned); + assert!(!reach.summary_conditioned); + assert_eq!(reach.assumption_usage.applied.len(), 1); + assert!(reach.assumption_usage.ignored.is_empty()); + assert!(reach.assumption_usage.conflicts.is_empty()); + assert!(matches!( + reach.assumption_usage.applied[0].value, + AssumptionValue::Constant { value: 1 } + )); + } + + #[test] + fn likely_compiled_preconditions_seed_narrowed_search_state() { + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + let original_constraints = state.num_constraints(); + + let compiled = crate::CompiledBackwardCondition { + predicate: z3::ast::Bool::from_bool(true), + summary: BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }, + }; + + match apply_compiled_precondition_with_mode( + &explorer, + state, + compiled, + CompiledPreconditionMode::Necessary, + true, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + .. + } => { + let compiled = compiled_precondition.expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::OverApprox); + assert_eq!(initial_state.num_constraints(), original_constraints); + assert_eq!( + narrowed_state.expect("narrowed state").num_constraints(), + original_constraints + 1 + ); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("likely precondition should not shortcut as exact unsat") + } + } + } + + #[test] + fn exact_preconditions_in_narrow_only_mode_do_not_shortcut_unsat() { + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let compiled = crate::CompiledBackwardCondition { + predicate: z3::ast::Bool::from_bool(false), + summary: BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }, + }; + + match apply_compiled_precondition_with_mode( + &explorer, + state, + compiled, + CompiledPreconditionMode::NarrowOnly, + true, + ) { + PreconditionApplication::Continue { + narrowed_state, + compiled_precondition, + .. + } => { + assert!(narrowed_state.is_none()); + assert_eq!( + compiled_precondition + .expect("compiled precondition") + .precision, + BackwardConditionPrecision::Exact + ); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("narrow-only exact precondition should not shortcut as exact unsat") + } + } + } + + #[test] + fn exact_preconditions_in_narrow_only_mode_seed_narrowed_state() { + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + let original_constraints = state.num_constraints(); + + let compiled = crate::CompiledBackwardCondition { + predicate: z3::ast::Bool::from_bool(true), + summary: BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }, + }; + + match apply_compiled_precondition_with_mode( + &explorer, + state, + compiled, + CompiledPreconditionMode::NarrowOnly, + true, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + .. + } => { + assert_eq!(initial_state.num_constraints(), original_constraints); + assert_eq!( + narrowed_state.expect("narrowed state").num_constraints(), + original_constraints + 1 + ); + assert_eq!( + compiled_precondition + .expect("compiled precondition") + .precision, + BackwardConditionPrecision::Exact + ); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("narrow-only exact precondition should not shortcut as exact unsat") + } + } + } + + #[test] + fn exact_preconditions_disable_unsat_shortcuts_when_exact_proof_is_disallowed() { + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + + let compiled = crate::CompiledBackwardCondition { + predicate: z3::ast::Bool::from_bool(false), + summary: BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }, + }; + match apply_compiled_precondition_with_mode( + &explorer, + state, + compiled, + CompiledPreconditionMode::Necessary, + false, + ) { + PreconditionApplication::Continue { + compiled_precondition, + .. + } => { + assert_eq!( + compiled_precondition + .expect("compiled precondition") + .precision, + BackwardConditionPrecision::Exact + ); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("exact-proof downgrade must disable exact unsat shortcuts") + } + } + } + + #[test] + fn bounded_copy_loops_raise_recommended_query_max_depth() { + let blocks = make_bounded_copy_loop_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + + let budget = super::recommended_query_max_depth(&func, &state); + + assert!( + budget >= 16_384, + "bounded copy loop budget should scale with trip count, got {budget}" + ); + } + + #[test] + fn bounded_copy_loops_raise_recommended_query_timeout() { + let blocks = make_bounded_copy_loop_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + + assert!( + super::recommended_query_timeout(&func) >= std::time::Duration::from_secs(120), + "bounded copy loop timeout should exceed default budget" + ); + } + + #[test] + fn continuation_seeded_routes_cap_recommended_query_max_depth() { + let blocks = make_bounded_copy_loop_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let route = crate::TargetQueryRoutePlan { + target_plan: crate::TargetQueryPlan::Ready { + mode: crate::QueryGuidanceMode::NarrowOnly, + }, + execution: crate::TargetQueryExecutionRoute::ContinuationSeeded { + bridge_target: 0x1010, + route: Box::new(crate::TargetQueryExecutionRoute::DynamicTargetCompile { + reason: "test".to_string(), + mode: crate::QueryGuidanceMode::NarrowOnly, + }), + }, + }; + + let budget = super::recommended_query_max_depth_for_route(&func, &state, Some(&route)); + + assert!( + budget <= super::CONTINUATION_QUERY_MAX_DEPTH_CAP, + "continuation-seeded route should cap inflated depth budget, got {budget}" + ); + } + + #[test] + fn continuation_seeded_routes_cap_recommended_query_max_states() { + let route = crate::TargetQueryRoutePlan { + target_plan: crate::TargetQueryPlan::Ready { + mode: crate::QueryGuidanceMode::NarrowOnly, + }, + execution: crate::TargetQueryExecutionRoute::ContinuationSeeded { + bridge_target: 0x1010, + route: Box::new(crate::TargetQueryExecutionRoute::DynamicTargetCompile { + reason: "test".to_string(), + mode: crate::QueryGuidanceMode::NarrowOnly, + }), + }, + }; + + let max_states = super::recommended_query_max_states_for_route(1_000, Some(&route)); + + assert_eq!(max_states, super::CONTINUATION_QUERY_MAX_STATES_CAP); + } + + #[test] + fn continuation_seeded_routes_cap_recommended_query_timeout() { + let blocks = make_bounded_copy_loop_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let route = crate::TargetQueryRoutePlan { + target_plan: crate::TargetQueryPlan::Ready { + mode: crate::QueryGuidanceMode::NarrowOnly, + }, + execution: crate::TargetQueryExecutionRoute::ContinuationSeeded { + bridge_target: 0x1010, + route: Box::new(crate::TargetQueryExecutionRoute::DynamicTargetCompile { + reason: "test".to_string(), + mode: crate::QueryGuidanceMode::NarrowOnly, + }), + }, + }; + + let timeout = super::recommended_query_timeout_for_route(&func, Some(&route)); + + assert!( + timeout <= std::time::Duration::from_secs(30), + "continuation-seeded route should cap inflated timeout, got {timeout:?}" + ); + } + + #[test] + fn continuation_followup_downgrades_dynamic_target_compile() { + let route = crate::TargetQueryExecutionRoute::DynamicTargetCompile { + reason: "LargeCfg".to_string(), + mode: crate::QueryGuidanceMode::NarrowOnly, + }; + + match super::continuation_followup_execution_route(&route) { + crate::TargetQueryExecutionRoute::ResidualOnly { reasons } => { + assert!( + reasons + .iter() + .any(|reason| reason.contains("dynamic target compile")), + "expected dynamic compile downgrade reason, got {reasons:?}" + ); + } + other => panic!("expected residual-only continuation follow-up, got {other:?}"), + } + } + + #[test] + fn continuation_followup_keeps_artifact_guided_routes() { + let route = crate::TargetQueryExecutionRoute::ArtifactCondition { + mode: crate::QueryGuidanceMode::Necessary, + }; + + assert_eq!(super::continuation_followup_execution_route(&route), route); + } + + #[test] + fn artifact_memory_terms_seed_narrowed_state_without_branch_recompile() { + let blocks = make_residual_precondition_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.new_symbolic_input("sym_mem", 8); + let original_constraints = state.num_constraints(); + let target_term = BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: Some("sym_mem".to_string()), + expr: "0x2a".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }; + let other_branch_term = BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: Some("sym_mem".to_string()), + expr: "0x2b".to_string(), + value_expr: Some("0x2b".to_string()), + exact_value: true, + }; + + let artifact = test_semantic_artifact( + RefinementStage::Residual, + SliceClass::Worker, + vec![ResidualReason::LargeCfg], + vec![{ + let mut region = make_region(0xdead, &[0x2000, 0x2010]); + region.control.push(Judged::new( + ControlFact { + target: 0x2000, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: vec![target_term.clone()], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + SemanticEvidence::exact(), + )); + region.control.push(Judged::new( + ControlFact { + target: 0x2010, + status: crate::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + condition: Some("!guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "!guard".to_string(), + terms: vec!["!guard".to_string()], + memory_terms: vec![other_branch_term.clone()], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + SemanticEvidence::exact(), + )); + region.targets.push(Judged::new( + TargetFact { + target: 0x2000, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + }, + SemanticEvidence::exact(), + )); + region.targets.push(Judged::new( + TargetFact { + target: 0x2010, + status: crate::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + }, + SemanticEvidence::exact(), + )); + region.memory.push(Judged::new( + MemoryFact { + term: target_term.clone(), + }, + SemanticEvidence::exact(), + )); + region.memory.push(Judged::new( + MemoryFact { + term: other_branch_term.clone(), + }, + SemanticEvidence::exact(), + )); + region + }], + default_diagnostics(), + ); + + match apply_best_compiled_precondition( + &explorer, + &func, + Some(&artifact), + state, + 0x2000, + false, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + .. + } => { + assert!(compiled_precondition.is_none()); + assert_eq!(initial_state.num_constraints(), original_constraints); + let narrowed_state = narrowed_state.expect("narrowed state"); + assert_eq!(narrowed_state.num_constraints(), original_constraints + 1); + let narrowed_value = narrowed_state + .symbolic_inputs() + .get("sym_mem") + .expect("symbolic input") + .to_bv(&ctx); + assert!(matches!( + explorer.solver().sat_with_constraint( + &narrowed_state, + &narrowed_value.eq(BV::from_u64(0x2a, 8)) + ), + SatResult::Sat + )); + assert!(matches!( + explorer.solver().sat_with_constraint( + &narrowed_state, + &narrowed_value.eq(BV::from_u64(0x2b, 8)) + ), + SatResult::Unsat + )); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("memory-term-only narrowing should not shortcut as exact unsat") + } + } + } + + #[test] + fn artifact_region_memory_terms_seed_narrowed_state_without_symbolic_binding() { + let blocks = make_residual_precondition_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + let symbolic_byte = state.new_symbolic_input("global_byte", 8); + let global_region = state.define_memory_region( + crate::MemoryRegionKind::Global, + "ram:0x2000", + Some(0x2000), + Some(1), + ); + state.mem_write(&crate::SymValue::concrete(0x2000, 64), &symbolic_byte, 1); + let original_constraints = state.num_constraints(); + let target_term = BackwardMemoryCondition { + region: BackwardMemoryRegion::Region(crate::backward::BackwardRegionRef { + id: global_region, + kind: crate::MemoryRegionKind::Global, + name: "ram:0x2000".to_string(), + }), + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: None, + expr: "*(ram:0x2000 + 0)".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }; + let other_branch_term = BackwardMemoryCondition { + region: BackwardMemoryRegion::Region(crate::backward::BackwardRegionRef { + id: global_region, + kind: crate::MemoryRegionKind::Global, + name: "ram:0x2000".to_string(), + }), + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: None, + expr: "*(ram:0x2000 + 0)".to_string(), + value_expr: Some("0x2b".to_string()), + exact_value: true, + }; + + let artifact = test_semantic_artifact( + RefinementStage::Residual, + SliceClass::Worker, + vec![ResidualReason::LargeCfg], + vec![{ + let mut region = make_region(0xdead, &[0x2000, 0x2010]); + region.control.push(Judged::new( + ControlFact { + target: 0x2000, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: vec![target_term.clone()], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + SemanticEvidence::exact(), + )); + region.control.push(Judged::new( + ControlFact { + target: 0x2010, + status: crate::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + condition: Some("!guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "!guard".to_string(), + terms: vec!["!guard".to_string()], + memory_terms: vec![other_branch_term.clone()], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + SemanticEvidence::exact(), + )); + region.targets.push(Judged::new( + TargetFact { + target: 0x2000, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + }, + SemanticEvidence::exact(), + )); + region.targets.push(Judged::new( + TargetFact { + target: 0x2010, + status: crate::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + }, + SemanticEvidence::exact(), + )); + region.memory.push(Judged::new( + MemoryFact { + term: target_term.clone(), + }, + SemanticEvidence::exact(), + )); + region.memory.push(Judged::new( + MemoryFact { + term: other_branch_term.clone(), + }, + SemanticEvidence::exact(), + )); + region + }], + default_diagnostics(), + ); + + match apply_best_compiled_precondition( + &explorer, + &func, + Some(&artifact), + state, + 0x2000, + false, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + .. + } => { + assert!(compiled_precondition.is_none()); + assert_eq!(initial_state.num_constraints(), original_constraints); + let narrowed_state = narrowed_state.expect("narrowed state"); + assert_eq!(narrowed_state.num_constraints(), original_constraints + 1); + let narrowed_value = narrowed_state + .mem_read(&crate::SymValue::concrete(0x2000, 64), 1) + .to_bv(&ctx); + assert!(matches!( + explorer.solver().sat_with_constraint( + &narrowed_state, + &narrowed_value.eq(BV::from_u64(0x2a, 8)) + ), + SatResult::Sat + )); + assert!(matches!( + explorer.solver().sat_with_constraint( + &narrowed_state, + &narrowed_value.eq(BV::from_u64(0x2b, 8)) + ), + SatResult::Unsat + )); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("region-backed memory-term narrowing should not shortcut as exact unsat") + } + } + } + + #[test] + fn region_memory_bag_without_target_local_compiled_memory_does_not_seed_narrowed_state() { + let blocks = make_residual_precondition_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.new_symbolic_input("sym_mem", 8); + let original_constraints = state.num_constraints(); + + let artifact = test_semantic_artifact( + RefinementStage::Residual, + SliceClass::Worker, + vec![ResidualReason::LargeCfg], + vec![{ + let mut region = make_region(0xdead, &[0x2000, 0x2010]); + region.control.push(Judged::new( + ControlFact { + target: 0x2000, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + SemanticEvidence::exact(), + )); + region.memory.push(Judged::new( + MemoryFact { + term: BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: Some("sym_mem".to_string()), + expr: "0x2a".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }, + }, + SemanticEvidence::exact(), + )); + region + }], + default_diagnostics(), + ); + + match apply_best_compiled_precondition( + &explorer, + &func, + Some(&artifact), + state, + 0x2000, + false, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + .. + } => { + assert!(compiled_precondition.is_none()); + assert_eq!(initial_state.num_constraints(), original_constraints); + assert!(narrowed_state.is_none()); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("region-bag-only memory facts should not seed targeted narrowing") + } + } + } + + #[test] + fn worker_island_memory_terms_seed_narrowed_state_without_branch_recompile() { + let blocks = make_residual_precondition_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.new_symbolic_input("sym_mem", 8); + let original_constraints = state.num_constraints(); + let target_term = BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: Some("sym_mem".to_string()), + expr: "0x2a".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }; + + let actionable = BackwardConditionSummary { + simplified: "worker_guard".to_string(), + terms: vec!["worker_guard".to_string()], + memory_terms: vec![target_term.clone()], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }; + let artifact = test_semantic_artifact( + RefinementStage::Compiled, + SliceClass::Worker, + Vec::new(), + vec![{ + let mut region = make_region(0xdead, &[0x2000, 0x2010]); + region.control.push(Judged::new( + ControlFact { + target: 0x2000, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: None, + condition: Some("worker_guard".to_string()), + compiled: Some(actionable.clone()), + }, + SemanticEvidence::likely(SemanticEvidenceReason::DerivedFromRanking), + )); + region.memory.push(Judged::new( + MemoryFact { + term: target_term.clone(), + }, + SemanticEvidence::exact(), + )); + region + }], + default_diagnostics(), + ); + + match apply_best_compiled_precondition( + &explorer, + &func, + Some(&artifact), + state, + 0x2000, + false, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition: _, + .. + } => { + assert_eq!(initial_state.num_constraints(), original_constraints); + let narrowed_state = narrowed_state.expect("narrowed state"); + assert_eq!(narrowed_state.num_constraints(), original_constraints + 1); + let narrowed_value = narrowed_state + .symbolic_inputs() + .get("sym_mem") + .expect("symbolic input") + .to_bv(&ctx); + assert!(matches!( + explorer.solver().sat_with_constraint( + &narrowed_state, + &narrowed_value.eq(BV::from_u64(0x2a, 8)) + ), + SatResult::Sat + )); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("worker-island memory narrowing should not shortcut as exact unsat") + } + } + } + + #[test] + fn necessary_worker_island_still_prefers_real_compiled_precondition() { + let blocks = make_residual_precondition_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + let original_constraints = state.num_constraints(); + + let artifact = test_semantic_artifact( + RefinementStage::Compiled, + SliceClass::Worker, + Vec::new(), + vec![{ + let mut region = make_region(0x1000, &[0x1010, 0x1004]); + region.control.push(Judged::new( + ControlFact { + target: 0x1010, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: None, + condition: Some("guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + SemanticEvidence::exact(), + )); + region.control.push(Judged::new( + ControlFact { + target: 0x1004, + status: crate::SymbolicReachabilityStatus::Unreachable, + branch_truth: None, + condition: None, + compiled: None, + }, + SemanticEvidence::exact(), + )); + region.memory.push(Judged::new( + MemoryFact { + term: BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: Some("reg:56_0".to_string()), + expr: "0x1".to_string(), + value_expr: Some("0x1".to_string()), + exact_value: true, + }, + }, + SemanticEvidence::exact(), + )); + region + }], + default_diagnostics(), + ); + + match apply_best_compiled_precondition( + &explorer, + &func, + Some(&artifact), + state, + 0x1010, + false, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + .. + } => { + assert!(initial_state.num_constraints() >= original_constraints); + assert!(narrowed_state.is_none()); + assert!(compiled_precondition.is_some()); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("necessary worker islands should use the real compiled precondition path") + } + } + } + + #[test] + fn target_condition_source_only_marks_unique_region_targets_as_necessary() { + let artifact = test_semantic_artifact( + RefinementStage::Compiled, + SliceClass::Worker, + Vec::new(), + vec![{ + let mut region = make_region(0x1000, &[0x1010, 0x1004]); + region.control.push(Judged::new( + ControlFact { + target: 0x1010, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + SemanticEvidence::exact(), + )); + region.control.push(Judged::new( + ControlFact { + target: 0x1004, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(false), + condition: Some("other_guard".to_string()), + compiled: None, + }, + SemanticEvidence::exact(), + )); + region + }], + default_diagnostics(), + ); + + let actionable = artifact + .target_condition_source(0x1010, false) + .expect("actionable target condition source"); + assert!( + matches!( + artifact.target_query_plan(0x1010), + crate::TargetQueryPlan::Ready { + mode: crate::QueryGuidanceMode::NarrowOnly + } + ), + "multi-exit regions must not be treated as necessary-for-target" + ); + assert_eq!(actionable.block_addr, 0x1000); + assert!( + artifact.target_condition_source(0x1010, true).is_none(), + "hard-proof target narrowing must reject non-necessary regions" + ); + } + + #[test] + fn target_local_memory_terms_follow_the_authoritative_target_source_region() { + let exact = SemanticEvidence::exact(); + let likely = SemanticEvidence::likely(SemanticEvidenceReason::PartialPathCoverage); + let artifact = test_semantic_artifact( + RefinementStage::Compiled, + SliceClass::Worker, + Vec::new(), + vec![ + { + let mut region = make_region(0x1000, &[0x1010]); + region.control.push(Judged::new( + ControlFact { + target: 0x1010, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("exact_guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "exact_guard".to_string(), + terms: vec!["exact_guard".to_string()], + memory_terms: vec![BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: exact.clone(), + binding: Some("sym_mem".to_string()), + expr: "0x2a".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + exact.clone(), + )); + region + }, + { + let mut region = make_region(0x1004, &[0x1010]); + region.control.push(Judged::new( + ControlFact { + target: 0x1010, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("weaker_guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "weaker_guard".to_string(), + terms: vec!["weaker_guard".to_string()], + memory_terms: vec![BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: likely.clone(), + binding: Some("sym_mem".to_string()), + expr: "0x2b".to_string(), + value_expr: Some("0x2b".to_string()), + exact_value: true, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }), + }, + likely.clone(), + )); + region + }, + ], + default_diagnostics(), + ); + + assert!(matches!( + artifact.target_query_plan(0x1010), + crate::TargetQueryPlan::Residual { .. } + )); + let route = artifact.target_query_route_plan(0x1010); + assert!(matches!( + route.target_plan, + TargetQueryPlan::Residual { .. } + )); + assert!(matches!( + route.execution, + TargetQueryExecutionRoute::ResidualOnly { .. } + )); + assert!( + artifact.target_condition_source(0x1010, false).is_none(), + "materially conflicting target sources must not collapse into one authoritative source" + ); + let memory_terms = artifact.actionable_memory_terms_for_target(0x1010); + assert!( + memory_terms.is_empty(), + "materially conflicting target-local memory terms must refuse narrowing" + ); + } + + #[test] + fn weaker_secondary_target_source_does_not_seed_narrowing() { + let blocks = make_residual_precondition_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + state.new_symbolic_input("sym_mem", 8); + + let exact = SemanticEvidence::exact(); + let likely = SemanticEvidence::likely(SemanticEvidenceReason::PartialPathCoverage); + let artifact = test_semantic_artifact( + RefinementStage::Compiled, + SliceClass::Worker, + Vec::new(), + vec![ + { + let mut region = make_region(0x1000, &[0x1010, 0x1004]); + region.control.push(Judged::new( + ControlFact { + target: 0x1010, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + exact.clone(), + )); + region + }, + { + let mut region = make_region(0x1004, &[0x1010, 0x1008]); + region.control.push(Judged::new( + ControlFact { + target: 0x1010, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("worker_guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "worker_guard".to_string(), + terms: vec!["worker_guard".to_string()], + memory_terms: vec![BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: likely.clone(), + binding: Some("sym_mem".to_string()), + expr: "0x2a".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }), + }, + likely.clone(), + )); + region + }, + ], + default_diagnostics(), + ); + + match apply_best_compiled_precondition( + &explorer, + &func, + Some(&artifact), + state, + 0x1010, + false, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + .. + } => { + assert!(compiled_precondition.is_none()); + assert!( + narrowed_state.is_none(), + "conflicting source regions must refuse targeted narrowing" + ); + let sym_mem = initial_state + .symbolic_inputs() + .get("sym_mem") + .expect("symbolic input") + .to_bv(&ctx); + assert!(matches!( + explorer + .solver() + .sat_with_constraint(&initial_state, &sym_mem.eq(BV::from_u64(0x2a, 8))), + SatResult::Sat + )); + assert!(matches!( + explorer + .solver() + .sat_with_constraint(&initial_state, &sym_mem.eq(BV::from_u64(0x2b, 8))), + SatResult::Sat + )); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("secondary source disagreement should not shortcut as exact unsat") + } + } + } + + #[test] + fn vm_artifacts_preserve_interpreter_slice_class() { + let artifact = SemanticArtifact { + stage: RefinementStage::Compiled, + granularity: ArtifactGranularity::SummaryOnly, + execution: ExecutionModel::Vm, + body: SemanticArtifactBody::Vm(Box::new(crate::VmArtifactBody { + interpreter: None, + step_summary: Some(make_vm_step_summary_with_transfers(Vec::new())), + transfer_summary: None, + })), + diagnostics: default_diagnostics(), + }; + + assert_eq!(artifact.slice_class(), Some(SliceClass::InterpreterSwitch)); + } + + #[test] + fn vm_target_case_values_include_redispatch_cases() { + let vm_step = make_vm_step_summary_with_transfers(vec![ + VmTransferArm { + handler_target: 0x1004, + case_values: vec![0], + region_blocks: vec![0x1004], + exit_targets: vec![0x1000], + exit_guards: Vec::new(), + state_updates: vec![VmStateUpdate { + output: "RDI_1".to_string(), + expr: "0x1".to_string(), + value: VmValueExpr::Const(1), + exact: true, + }], + selector_update: Some(VmStateUpdate { + output: "RDI_1".to_string(), + expr: "0x1".to_string(), + value: VmValueExpr::Const(1), + exact: true, + }), + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, + exact: true, + redispatch: true, + may_return: false, + truncated: false, + }, + VmTransferArm { + handler_target: 0x1008, + case_values: vec![1], + region_blocks: vec![0x1008, 0x1014], + exit_targets: vec![0x1014], + exit_guards: Vec::new(), + state_updates: Vec::new(), + selector_update: None, + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, + exact: true, + redispatch: false, + may_return: true, + truncated: false, + }, + ]); + + assert_eq!(vm_target_case_values(&vm_step, 0x1014), vec![0, 1]); + } + + #[test] + fn vm_target_case_values_follow_exact_state_updates_across_redispatch() { + let vm_step = make_vm_step_summary_with_transfers(vec![ + VmTransferArm { + handler_target: 0x1004, + case_values: vec![0], + region_blocks: vec![0x1004], + exit_targets: vec![0x1000], + exit_guards: Vec::new(), + state_updates: vec![VmStateUpdate { + output: "TMP_1".to_string(), + expr: "0x1".to_string(), + value: VmValueExpr::Const(1), + exact: true, + }], + selector_update: Some(VmStateUpdate { + output: "RDI_1".to_string(), + expr: "TMP_1".to_string(), + value: VmValueExpr::Var("TMP_1".to_string()), + exact: true, + }), + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, + exact: true, + redispatch: true, + may_return: false, + truncated: false, + }, + VmTransferArm { + handler_target: 0x1008, + case_values: vec![1], + region_blocks: vec![0x1008, 0x1014], + exit_targets: vec![0x1014], + exit_guards: Vec::new(), + state_updates: Vec::new(), + selector_update: None, + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, + exact: true, + redispatch: false, + may_return: true, + truncated: false, + }, + ]); + + assert_eq!(vm_target_case_values(&vm_step, 0x1014), vec![0, 1]); + } + + #[test] + fn vm_target_case_values_follow_bounded_selector_algebra() { + let vm_step = make_vm_step_summary_with_transfers(vec![ + VmTransferArm { + handler_target: 0x1004, + case_values: vec![0], + region_blocks: vec![0x1004], + exit_targets: vec![0x1000], + exit_guards: Vec::new(), + state_updates: vec![VmStateUpdate { + output: "RDI_1".to_string(), + expr: "(RDI_0 + 0x1)".to_string(), + value: VmValueExpr::Binary { + op: VmBinaryOp::Add, + lhs: Box::new(VmValueExpr::Var("RDI_0".to_string())), + rhs: Box::new(VmValueExpr::Const(1)), + }, + exact: true, + }], + selector_update: Some(VmStateUpdate { + output: "RDI_1".to_string(), + expr: "(RDI_0 + 0x1)".to_string(), + value: VmValueExpr::Binary { + op: VmBinaryOp::Add, + lhs: Box::new(VmValueExpr::Var("RDI_0".to_string())), + rhs: Box::new(VmValueExpr::Const(1)), + }, + exact: true, + }), + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, + exact: true, + redispatch: true, + may_return: false, + truncated: false, + }, + VmTransferArm { + handler_target: 0x1008, + case_values: vec![1], + region_blocks: vec![0x1008], + exit_targets: vec![0x1000], + exit_guards: Vec::new(), + state_updates: vec![VmStateUpdate { + output: "RDI_1".to_string(), + expr: "(RDI_0 + 0x1)".to_string(), + value: VmValueExpr::Binary { + op: VmBinaryOp::Add, + lhs: Box::new(VmValueExpr::Var("RDI_0".to_string())), + rhs: Box::new(VmValueExpr::Const(1)), + }, + exact: true, + }], + selector_update: Some(VmStateUpdate { + output: "RDI_1".to_string(), + expr: "(RDI_0 + 0x1)".to_string(), + value: VmValueExpr::Binary { + op: VmBinaryOp::Add, + lhs: Box::new(VmValueExpr::Var("RDI_0".to_string())), + rhs: Box::new(VmValueExpr::Const(1)), + }, + exact: true, + }), + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, + exact: true, + redispatch: true, + may_return: false, + truncated: false, + }, + VmTransferArm { + handler_target: 0x1010, + case_values: vec![2], + region_blocks: vec![0x1010, 0x1014], + exit_targets: vec![0x1014], + exit_guards: Vec::new(), + state_updates: Vec::new(), + selector_update: None, + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, + exact: true, + redispatch: false, + may_return: true, + truncated: false, + }, + ]); + + assert_eq!(vm_target_case_values(&vm_step, 0x1014), vec![0, 1, 2]); + } + + #[test] + fn vm_target_case_values_respect_exact_exit_guards() { + let vm_step = make_vm_step_summary_with_transfers(vec![ + VmTransferArm { + handler_target: 0x1004, + case_values: vec![0], + region_blocks: vec![0x1004], + exit_targets: vec![0x1014, 0x1000], + exit_guards: vec![VmGuardedExit { + target: 0x1014, + guard: VmGuardCondition { + expr: "(RDI_0 == 0x1)".to_string(), + value: VmValueExpr::Binary { + op: VmBinaryOp::Eq, + lhs: Box::new(VmValueExpr::Var("RDI_0".to_string())), + rhs: Box::new(VmValueExpr::Const(1)), + }, + expect_nonzero: true, + exact: true, + }, + }], + state_updates: Vec::new(), + selector_update: None, + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, + exact: false, + redispatch: false, + may_return: true, + truncated: false, + }, + VmTransferArm { + handler_target: 0x1008, + case_values: vec![1], + region_blocks: vec![0x1008], + exit_targets: vec![0x1014], + exit_guards: Vec::new(), + state_updates: Vec::new(), + selector_update: None, + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, + exact: true, + redispatch: false, + may_return: true, + truncated: false, + }, + ]); + + assert_eq!(vm_target_case_values(&vm_step, 0x1014), vec![1]); + } + + #[test] + fn vm_target_case_values_follow_exact_memory_writes_across_redispatch() { + let binding = "mem:r7:0:1".to_string(); + let vm_step = make_vm_step_summary_with_transfers(vec![ + VmTransferArm { + handler_target: 0x1004, + case_values: vec![0], + region_blocks: vec![0x1004], + exit_targets: vec![0x1000], + exit_guards: Vec::new(), + state_updates: Vec::new(), + selector_update: Some(VmStateUpdate { + output: "RDI_1".to_string(), + expr: "0x1".to_string(), + value: VmValueExpr::Const(1), + exact: true, + }), + memory_reads: Vec::new(), + memory_writes: vec![crate::VmMemoryCondition { + region: crate::VmMemoryRegionRef { + id: 7, + kind: crate::MemoryRegionKind::Global, + name: "ram:0x4000".to_string(), + }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + binding: Some(binding.clone()), + expr: "*0x4000".to_string(), + value_expr: Some("0x1".to_string()), + value: Some(VmValueExpr::Const(1)), + exact_value: true, + }], + residual_guards: false, + residual_memory_effects: false, + exact: true, + redispatch: true, + may_return: false, + truncated: false, + }, + VmTransferArm { + handler_target: 0x1008, + case_values: vec![2], + region_blocks: vec![0x1008], + exit_targets: vec![0x1000], + exit_guards: Vec::new(), + state_updates: Vec::new(), + selector_update: Some(VmStateUpdate { + output: "RDI_1".to_string(), + expr: "0x1".to_string(), + value: VmValueExpr::Const(1), + exact: true, + }), + memory_reads: Vec::new(), + memory_writes: vec![crate::VmMemoryCondition { + region: crate::VmMemoryRegionRef { + id: 7, + kind: crate::MemoryRegionKind::Global, + name: "ram:0x4000".to_string(), + }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + binding: Some(binding.clone()), + expr: "*0x4000".to_string(), + value_expr: Some("0x0".to_string()), + value: Some(VmValueExpr::Const(0)), + exact_value: true, + }], + residual_guards: false, + residual_memory_effects: false, + exact: true, + redispatch: true, + may_return: false, + truncated: false, + }, + VmTransferArm { + handler_target: 0x1010, + case_values: vec![1], + region_blocks: vec![0x1010], + exit_targets: vec![0x1014], + exit_guards: vec![VmGuardedExit { + target: 0x1014, + guard: VmGuardCondition { + expr: binding.clone(), + value: VmValueExpr::Var(binding.clone()), + expect_nonzero: true, + exact: true, + }, + }], + state_updates: Vec::new(), + selector_update: None, + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, + exact: true, + redispatch: false, + may_return: true, + truncated: false, + }, + ]); + + assert_eq!(vm_target_case_values(&vm_step, 0x1014), vec![0, 1]); + } + + #[test] + fn exact_compiled_preconditions_prefer_fewer_residual_fallbacks() { + let current = crate::CompiledBackwardCondition { + predicate: z3::ast::Bool::from_bool(true), + summary: BackwardConditionSummary { + simplified: "x".to_string(), + terms: vec!["x".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 2, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }, + }; + let candidate = crate::CompiledBackwardCondition { + predicate: z3::ast::Bool::from_bool(true), + summary: BackwardConditionSummary { + simplified: "y".to_string(), + terms: vec!["y".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 1, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }, + }; + + assert!(prefer_compiled_precondition(Some(¤t), &candidate)); + assert!(!prefer_compiled_precondition(Some(&candidate), ¤t)); + } +} diff --git a/crates/r2sym/src/replay.rs b/crates/r2sym/src/replay.rs new file mode 100644 index 0000000..00264d7 --- /dev/null +++ b/crates/r2sym/src/replay.rs @@ -0,0 +1,750 @@ +use std::borrow::Cow; +use std::collections::BTreeMap; + +use r2il::ArchSpec; +use r2ssa::SsaArtifact; + +use crate::memory::MemoryRegionKind; +use crate::runtime::seed_memory_regions_for_arch; +use crate::state::SymState; + +#[derive(Debug, Clone, Default)] +pub struct ReplaySeed { + pub checkpoint_id: Option, + pub entry_pc: Option, + pub registers: Vec, + pub memory: Vec, + pub register_overlays: Vec, + pub memory_overlays: Vec, + pub tty_fds: Vec, + pub skip_sleep_calls: bool, +} + +#[derive(Debug, Clone)] +pub struct ReplayRegisterValue { + pub name: String, + pub value: u64, +} + +#[derive(Debug, Clone)] +pub struct ReplayMemoryWindow { + pub addr: u64, + pub bytes: Vec, + pub label: Option, +} + +#[derive(Debug, Clone)] +pub struct ReplayRegisterOverlay { + pub name: String, + pub symbol: String, +} + +#[derive(Debug, Clone)] +pub struct ReplayMemoryOverlay { + pub addr: u64, + pub size: u32, + pub name: String, +} + +const REPLAY_FINGERPRINT_OFFSET_BASIS: u64 = 0xcbf29ce484222325; +const REPLAY_FINGERPRINT_PRIME: u64 = 0x100000001b3; + +fn replay_fingerprint_update(state: &mut u64, bytes: &[u8]) { + for byte in bytes { + *state ^= u64::from(*byte); + *state = state.wrapping_mul(REPLAY_FINGERPRINT_PRIME); + } +} + +fn replay_fingerprint_tag(state: &mut u64, tag: u8) { + replay_fingerprint_update(state, &[tag]); +} + +fn replay_fingerprint_u64(state: &mut u64, tag: u8, value: u64) { + replay_fingerprint_tag(state, tag); + replay_fingerprint_update(state, &value.to_le_bytes()); +} + +fn replay_fingerprint_bool(state: &mut u64, tag: u8, value: bool) { + replay_fingerprint_tag(state, tag); + replay_fingerprint_update(state, &[u8::from(value)]); +} + +fn replay_fingerprint_str(state: &mut u64, tag: u8, value: &str) { + replay_fingerprint_tag(state, tag); + replay_fingerprint_update(state, &(value.len() as u64).to_le_bytes()); + replay_fingerprint_update(state, value.as_bytes()); +} + +fn replay_fingerprint_bytes(state: &mut u64, tag: u8, value: &[u8]) { + replay_fingerprint_tag(state, tag); + replay_fingerprint_update(state, &(value.len() as u64).to_le_bytes()); + replay_fingerprint_update(state, value); +} + +fn canonical_replay_register_name(name: &str) -> String { + name.trim().to_ascii_uppercase() +} + +fn canonical_replay_label(label: &Option) -> Option<&str> { + label + .as_deref() + .map(str::trim) + .filter(|label| !label.is_empty()) +} + +pub fn stable_replay_seed_fingerprint(seed: &ReplaySeed) -> u64 { + let mut state = REPLAY_FINGERPRINT_OFFSET_BASIS; + replay_fingerprint_tag(&mut state, 0x01); + match seed.checkpoint_id { + Some(value) => replay_fingerprint_u64(&mut state, 0x02, value), + None => replay_fingerprint_tag(&mut state, 0x03), + } + match seed.entry_pc { + Some(value) => replay_fingerprint_u64(&mut state, 0x04, value), + None => replay_fingerprint_tag(&mut state, 0x05), + } + + let mut registers: Vec<_> = seed + .registers + .iter() + .map(|register| { + ( + canonical_replay_register_name(®ister.name), + register.value, + ) + }) + .collect(); + registers.sort_unstable(); + replay_fingerprint_u64(&mut state, 0x06, registers.len() as u64); + for (name, value) in registers { + replay_fingerprint_str(&mut state, 0x07, &name); + replay_fingerprint_u64(&mut state, 0x08, value); + } + + let mut memory: Vec<_> = seed + .memory + .iter() + .map(|window| { + ( + window.addr, + canonical_replay_label(&window.label).map(str::to_string), + window.bytes.clone(), + ) + }) + .collect(); + memory.sort_unstable_by(|left, right| { + left.0 + .cmp(&right.0) + .then(left.1.cmp(&right.1)) + .then(left.2.cmp(&right.2)) + }); + replay_fingerprint_u64(&mut state, 0x09, memory.len() as u64); + for (addr, label, bytes) in memory { + replay_fingerprint_u64(&mut state, 0x0a, addr); + match label { + Some(label) => replay_fingerprint_str(&mut state, 0x0b, &label), + None => replay_fingerprint_tag(&mut state, 0x0c), + } + replay_fingerprint_bytes(&mut state, 0x0d, &bytes); + } + + let mut register_overlays: Vec<_> = seed + .register_overlays + .iter() + .map(|overlay| { + ( + canonical_replay_register_name(&overlay.name), + overlay.symbol.trim().to_string(), + ) + }) + .collect(); + register_overlays.sort_unstable(); + replay_fingerprint_u64(&mut state, 0x0e, register_overlays.len() as u64); + for (name, symbol) in register_overlays { + replay_fingerprint_str(&mut state, 0x0f, &name); + replay_fingerprint_str(&mut state, 0x10, &symbol); + } + + let mut memory_overlays: Vec<_> = seed + .memory_overlays + .iter() + .map(|overlay| (overlay.addr, overlay.size, overlay.name.trim().to_string())) + .collect(); + memory_overlays.sort_unstable(); + replay_fingerprint_u64(&mut state, 0x11, memory_overlays.len() as u64); + for (addr, size, name) in memory_overlays { + replay_fingerprint_u64(&mut state, 0x12, addr); + replay_fingerprint_u64(&mut state, 0x13, u64::from(size)); + replay_fingerprint_str(&mut state, 0x14, &name); + } + + let mut tty_fds = seed.tty_fds.clone(); + tty_fds.sort_unstable(); + replay_fingerprint_u64(&mut state, 0x15, tty_fds.len() as u64); + for fd in tty_fds { + replay_fingerprint_u64(&mut state, 0x16, fd as u64); + } + replay_fingerprint_bool(&mut state, 0x17, seed.skip_sleep_calls); + state +} + +pub fn seed_replay_state_for_arch<'ctx>( + state: &mut SymState<'ctx>, + prepared: Option<&SsaArtifact>, + arch: Option<&ArchSpec>, + seed: &ReplaySeed, +) { + if let Some(prepared) = prepared { + seed_memory_regions_for_arch(state, prepared, arch); + } + apply_replay_seed_to_state(state, prepared, arch, seed); +} + +pub fn apply_replay_seed_to_state<'ctx>( + state: &mut SymState<'ctx>, + prepared: Option<&SsaArtifact>, + arch: Option<&ArchSpec>, + seed: &ReplaySeed, +) { + if let Some(entry_pc) = seed.entry_pc { + state.pc = entry_pc; + } + + let register_layout = ReplayRegisterLayout::from_prepared(prepared); + for register in &seed.registers { + let (seed_name, bits) = register_layout + .resolve_register(®ister.name) + .unwrap_or_else(|| { + ( + register.name.to_ascii_uppercase(), + default_register_bits(arch), + ) + }); + state.set_concrete(&seed_name, register.value, bits); + } + + for (index, window) in seed.memory.iter().enumerate() { + if window.bytes.is_empty() { + continue; + } + let name = window + .label + .clone() + .filter(|label| !label.is_empty()) + .unwrap_or_else(|| format!("replay_{index:x}_{:x}", window.addr)); + let region_id = state.define_memory_region( + MemoryRegionKind::Replay, + &name, + Some(window.addr), + Some(window.bytes.len() as u64), + ); + state.seed_region_bytes(region_id, 0, &window.bytes); + } + + for overlay in &seed.register_overlays { + let (seed_name, bits) = register_layout + .resolve_register(&overlay.name) + .unwrap_or_else(|| { + ( + overlay.name.to_ascii_uppercase(), + default_register_bits(arch), + ) + }); + state.make_symbolic_named(&seed_name, &overlay.symbol, bits); + } + + for overlay in &seed.memory_overlays { + if overlay.size == 0 { + continue; + } + state.make_symbolic_memory(overlay.addr, overlay.size, &overlay.name); + } + + for fd in &seed.tty_fds { + state.set_tty_fd(*fd, true); + } + state.set_skip_sleep_calls(seed.skip_sleep_calls); +} + +fn default_register_bits(arch: Option<&ArchSpec>) -> u32 { + arch.map(|arch| arch.addr_size.max(1) * 8).unwrap_or(64) +} + +#[derive(Default)] +struct ReplayRegisterLayout { + by_name: BTreeMap, + alias_candidates: BTreeMap, +} + +impl ReplayRegisterLayout { + fn from_prepared(prepared: Option<&SsaArtifact>) -> Self { + let Some(prepared) = prepared else { + return Self::default(); + }; + + let mut by_name = BTreeMap::new(); + let mut alias_candidates = BTreeMap::new(); + let mut record_var = |var: &r2ssa::SSAVar| { + if !var.is_register() || var.version != 0 { + return; + } + let bits = var.size * 8; + let display_name = var.display_name(); + by_name + .entry(display_name.to_ascii_uppercase()) + .or_insert_with(|| (display_name.clone(), bits)); + let base_name = var.name.strip_prefix("reg:").unwrap_or(&var.name); + by_name + .entry(base_name.to_ascii_uppercase()) + .or_insert_with(|| (display_name.clone(), bits)); + if let Some(alias) = register_alias_spec(base_name) { + alias_candidates + .entry(display_name) + .or_insert((bits, alias)); + } + }; + + for block in prepared.blocks() { + block.for_each_def(|def| record_var(def.var)); + block.for_each_source(|src| record_var(src.var)); + } + + Self { + by_name, + alias_candidates, + } + } + + fn resolve_register(&self, name: &str) -> Option<(String, u32)> { + let key = name.trim().to_ascii_uppercase(); + self.by_name + .get(&key) + .cloned() + .or_else(|| self.resolve_alias_register(&key)) + } + + fn resolve_alias_register(&self, name: &str) -> Option<(String, u32)> { + let requested = register_alias_spec(name)?; + let requested_low = requested.offset_bits; + let requested_high = requested.offset_bits + requested.width_bits; + + let mut best: Option<(u8, u32, String, u32)> = None; + for (display_name, (bits, candidate)) in &self.alias_candidates { + if candidate.family != requested.family { + continue; + } + + let candidate_low = candidate.offset_bits; + let candidate_high = candidate.offset_bits + candidate.width_bits; + let relation_score = + if candidate_low == requested_low && candidate.width_bits == requested.width_bits { + 3 + } else if candidate_low <= requested_low && candidate_high >= requested_high { + 2 + } else if candidate_low == requested_low { + 1 + } else { + 0 + }; + if relation_score == 0 { + continue; + } + + let width_distance = candidate.width_bits.abs_diff(requested.width_bits); + let should_replace = + best.as_ref() + .is_none_or(|(best_score, best_distance, _, best_bits)| { + relation_score > *best_score + || (relation_score == *best_score && width_distance < *best_distance) + || (relation_score == *best_score + && width_distance == *best_distance + && *bits > *best_bits) + }); + if should_replace { + best = Some((relation_score, width_distance, display_name.clone(), *bits)); + } + } + + best.map(|(_, _, display_name, bits)| (display_name, bits)) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct RegisterAliasSpec { + family: Cow<'static, str>, + offset_bits: u32, + width_bits: u32, +} + +fn register_alias_spec(base: &str) -> Option { + let upper = base.to_ascii_uppercase(); + let base = upper.as_str(); + let fixed = match base { + "AL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 0, + width_bits: 8, + }), + "AH" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 8, + width_bits: 8, + }), + "AX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 0, + width_bits: 16, + }), + "EAX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 0, + width_bits: 32, + }), + "RAX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 0, + width_bits: 64, + }), + "BL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 0, + width_bits: 8, + }), + "BH" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 8, + width_bits: 8, + }), + "BX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 0, + width_bits: 16, + }), + "EBX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 0, + width_bits: 32, + }), + "RBX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 0, + width_bits: 64, + }), + "CL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 0, + width_bits: 8, + }), + "CH" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 8, + width_bits: 8, + }), + "CX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 0, + width_bits: 16, + }), + "ECX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 0, + width_bits: 32, + }), + "RCX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 0, + width_bits: 64, + }), + "DL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 0, + width_bits: 8, + }), + "DH" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 8, + width_bits: 8, + }), + "DX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 0, + width_bits: 16, + }), + "EDX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 0, + width_bits: 32, + }), + "RDX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 0, + width_bits: 64, + }), + "SIL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSI"), + offset_bits: 0, + width_bits: 8, + }), + "SI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSI"), + offset_bits: 0, + width_bits: 16, + }), + "ESI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSI"), + offset_bits: 0, + width_bits: 32, + }), + "RSI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSI"), + offset_bits: 0, + width_bits: 64, + }), + "DIL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDI"), + offset_bits: 0, + width_bits: 8, + }), + "DI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDI"), + offset_bits: 0, + width_bits: 16, + }), + "EDI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDI"), + offset_bits: 0, + width_bits: 32, + }), + "RDI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDI"), + offset_bits: 0, + width_bits: 64, + }), + "BPL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBP"), + offset_bits: 0, + width_bits: 8, + }), + "BP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBP"), + offset_bits: 0, + width_bits: 16, + }), + "EBP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBP"), + offset_bits: 0, + width_bits: 32, + }), + "RBP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBP"), + offset_bits: 0, + width_bits: 64, + }), + "SPL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSP"), + offset_bits: 0, + width_bits: 8, + }), + "SP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSP"), + offset_bits: 0, + width_bits: 16, + }), + "ESP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSP"), + offset_bits: 0, + width_bits: 32, + }), + "RSP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSP"), + offset_bits: 0, + width_bits: 64, + }), + _ => None, + }; + if fixed.is_some() { + return fixed; + } + + parse_numbered_x86_register_alias(base) +} + +fn parse_numbered_x86_register_alias(base: &str) -> Option { + let (family, width_bits) = if let Some(family) = base.strip_suffix('B') { + (family.to_string(), 8) + } else if let Some(family) = base.strip_suffix('W') { + (family.to_string(), 16) + } else if let Some(family) = base.strip_suffix('D') { + (family.to_string(), 32) + } else { + (base.to_string(), 64) + }; + + if !family.starts_with('R') { + return None; + } + let digits = &family[1..]; + if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) { + return None; + } + + Some(RegisterAliasSpec { + family: Cow::Owned(family), + offset_bits: 0, + width_bits, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::value::SymValue; + use z3::Context; + + #[test] + fn replay_seed_imports_replay_regions_and_symbolic_overlays() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let seed = ReplaySeed { + checkpoint_id: Some(7), + entry_pc: Some(0x4141), + registers: vec![ReplayRegisterValue { + name: "rax".to_string(), + value: 0x1122, + }], + memory: vec![ReplayMemoryWindow { + addr: 0x5000, + bytes: vec![0x41, 0x42, 0x43, 0x44], + label: Some("input_window".to_string()), + }], + register_overlays: vec![ReplayRegisterOverlay { + name: "rbx".to_string(), + symbol: "replay_rbx".to_string(), + }], + memory_overlays: vec![ReplayMemoryOverlay { + addr: 0x5001, + size: 2, + name: "user_buf".to_string(), + }], + tty_fds: vec![0], + skip_sleep_calls: true, + }; + + seed_replay_state_for_arch(&mut state, None, None, &seed); + + assert_eq!(state.pc, 0x4141); + assert_eq!(state.get_register("RAX").as_concrete(), Some(0x1122)); + assert!(state.get_register("RBX").is_symbolic()); + let concrete = state.mem_read(&SymValue::concrete(0x5000, 64), 1); + assert_eq!(concrete.as_concrete(), Some(0x41)); + let symbolic = state.mem_read(&SymValue::concrete(0x5001, 64), 2); + assert!(symbolic.is_symbolic()); + assert!(state.is_tty_fd(0)); + assert!(state.skip_sleep_calls()); + } + + #[test] + fn stable_replay_seed_fingerprint_is_order_and_case_stable() { + let seed_a = ReplaySeed { + checkpoint_id: Some(7), + entry_pc: Some(0x4141), + registers: vec![ + ReplayRegisterValue { + name: "rax".to_string(), + value: 0x1122, + }, + ReplayRegisterValue { + name: "rdi".to_string(), + value: 0x3344, + }, + ], + memory: vec![ + ReplayMemoryWindow { + addr: 0x5000, + bytes: vec![0x41, 0x42], + label: Some("input".to_string()), + }, + ReplayMemoryWindow { + addr: 0x6000, + bytes: vec![0x51, 0x52], + label: None, + }, + ], + register_overlays: vec![ReplayRegisterOverlay { + name: "rbx".to_string(), + symbol: "replay_rbx".to_string(), + }], + memory_overlays: vec![ReplayMemoryOverlay { + addr: 0x5001, + size: 2, + name: "user_buf".to_string(), + }], + tty_fds: vec![1, 0], + skip_sleep_calls: true, + }; + let seed_b = ReplaySeed { + checkpoint_id: Some(7), + entry_pc: Some(0x4141), + registers: vec![ + ReplayRegisterValue { + name: "RDI".to_string(), + value: 0x3344, + }, + ReplayRegisterValue { + name: "RAX".to_string(), + value: 0x1122, + }, + ], + memory: vec![ + ReplayMemoryWindow { + addr: 0x6000, + bytes: vec![0x51, 0x52], + label: Some(String::new()), + }, + ReplayMemoryWindow { + addr: 0x5000, + bytes: vec![0x41, 0x42], + label: Some(" input ".to_string()), + }, + ], + register_overlays: vec![ReplayRegisterOverlay { + name: "RBX".to_string(), + symbol: "replay_rbx".to_string(), + }], + memory_overlays: vec![ReplayMemoryOverlay { + addr: 0x5001, + size: 2, + name: "user_buf".to_string(), + }], + tty_fds: vec![0, 1], + skip_sleep_calls: true, + }; + + assert_eq!( + stable_replay_seed_fingerprint(&seed_a), + stable_replay_seed_fingerprint(&seed_b) + ); + } + + #[test] + fn stable_replay_seed_fingerprint_changes_with_seed_content() { + let base = ReplaySeed { + entry_pc: Some(0x4141), + registers: vec![ReplayRegisterValue { + name: "rax".to_string(), + value: 0x1122, + }], + ..ReplaySeed::default() + }; + let different = ReplaySeed { + entry_pc: Some(0x4142), + ..base.clone() + }; + + assert_ne!( + stable_replay_seed_fingerprint(&base), + stable_replay_seed_fingerprint(&different) + ); + } +} diff --git a/crates/r2sym/src/runtime.rs b/crates/r2sym/src/runtime.rs index 172c81b..fa1010f 100644 --- a/crates/r2sym/src/runtime.rs +++ b/crates/r2sym/src/runtime.rs @@ -1,60 +1,327 @@ -use std::collections::HashSet; +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::fs::OpenOptions; +use std::io::Write; use r2il::ArchSpec; -use r2ssa::{SSAVar, SsaArtifact}; +use r2ssa::{ObjectKind, SSAVar, SsaArtifact}; -use crate::state::SymState; +use crate::PathExplorer; +use crate::executor::{CallHookResult, CallHookTag}; +use crate::memory::MemoryRegionKind; +use crate::sim::{CallConv, PreparedFunctionScope}; +use crate::state::{ExitStatus, SymState}; +use crate::value::SymValue; -pub fn seed_default_state_for_arch<'ctx>( - state: &mut SymState<'ctx>, - prepared: &SsaArtifact, - arch: Option<&ArchSpec>, -) { - let Some(arch) = arch else { - return; - }; +struct ArchSeedProfile { + arg_regs: &'static [&'static str], + stack_regs: &'static [&'static str], + stack_value: u64, +} +fn arch_seed_profile(arch: &ArchSpec) -> Option { let arch_name = arch.name.to_ascii_lowercase(); let looks_riscv = arch_name.contains("riscv") || arch_name.starts_with("rv"); - let (arg_regs, stack_regs, stack_value) = if arch_name == "x86-64" - || arch_name == "x86_64" - || (arch_name == "x86" && arch.addr_size == 8) + if arch_name == "x86-64" || arch_name == "x86_64" || (arch_name == "x86" && arch.addr_size == 8) { - ( - [ + Some(ArchSeedProfile { + arg_regs: &[ "RDI", "RSI", "RDX", "RCX", "R8", "R9", "EDI", "ESI", "EDX", "ECX", "R8D", "R9D", - ] - .as_slice(), - ["RSP", "RBP"].as_slice(), - 0x7fff_ffff_0000u64, - ) + ], + stack_regs: &["RSP", "RBP"], + stack_value: 0x7fff_ffff_0000u64, + }) } else if arch_name == "x86" { - ( - ["EAX", "EBX", "ECX", "EDX", "ESI", "EDI"].as_slice(), - ["ESP", "EBP"].as_slice(), - 0x7fff_0000u64, - ) + Some(ArchSeedProfile { + arg_regs: &["EAX", "EBX", "ECX", "EDX", "ESI", "EDI"], + stack_regs: &["ESP", "EBP"], + stack_value: 0x7fff_0000u64, + }) } else if looks_riscv && (arch.addr_size == 8 || arch_name.contains("64")) { - ( - [ + Some(ArchSeedProfile { + arg_regs: &[ "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "X10", "X11", "X12", "X13", "X14", "X15", "X16", "X17", - ] - .as_slice(), - ["SP", "S0", "FP", "X2", "X8"].as_slice(), - 0x7fff_ffff_0000u64, - ) + ], + stack_regs: &["SP", "S0", "FP", "X2", "X8"], + stack_value: 0x7fff_ffff_0000u64, + }) } else if looks_riscv { - ( - [ + Some(ArchSeedProfile { + arg_regs: &[ "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "X10", "X11", "X12", "X13", "X14", "X15", "X16", "X17", - ] - .as_slice(), - ["SP", "S0", "FP", "X2", "X8"].as_slice(), - 0x7fff_0000u64, - ) + ], + stack_regs: &["SP", "S0", "FP", "X2", "X8"], + stack_value: 0x7fff_0000u64, + }) } else { + None + } +} + +#[derive(Clone, Copy)] +struct MainArgRegisterProfile { + argc: &'static [&'static str], + argv: &'static [&'static str], + envp: &'static [&'static str], +} + +fn collect_registers(prepared: &SsaArtifact, only_version_zero: bool) -> BTreeMap { + let mut registers = BTreeMap::new(); + let mut record = |var: &SSAVar| { + if !var.is_register() || (only_version_zero && var.version != 0) { + return; + } + registers + .entry(var.display_name()) + .or_insert(var.size.saturating_mul(8)); + }; + for block in prepared.blocks() { + block.for_each_def(|def| record(def.var)); + block.for_each_source(|src| record(src.var)); + } + registers +} + +fn collect_version_zero_registers(prepared: &SsaArtifact) -> BTreeMap { + collect_registers(prepared, true) +} + +fn normalized_main_like_name(name: &str) -> String { + name.rsplit('.') + .next() + .unwrap_or(name) + .trim_start_matches('_') + .to_ascii_lowercase() +} + +fn debug_main_seed_enabled() -> bool { + std::env::var_os("R2SLEIGH_DEBUG_MAIN_SEED").is_some() +} + +fn debug_main_seed_log(message: &str) { + if !debug_main_seed_enabled() { + return; + } + let path = std::env::var("R2SLEIGH_DEBUG_MAIN_SEED_LOG") + .unwrap_or_else(|_| "/tmp/r2sleigh_main_seed.log".to_string()); + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { + let _ = writeln!(file, "{message}"); + } +} + +fn root_name_looks_like_main(scope: &PreparedFunctionScope, prepared: &SsaArtifact) -> bool { + let name = scope + .root() + .and_then(|function| function.name.as_deref()) + .or(prepared.function().name.as_deref()); + let Some(name) = name else { + return false; + }; + let normalized = normalized_main_like_name(name); + matches!(normalized.as_str(), "main" | "wmain") +} + +fn detect_main_arg_register_profile( + prepared: &SsaArtifact, + arch: Option<&ArchSpec>, +) -> Option { + let registers = collect_version_zero_registers(prepared); + let arch_looks_x64 = arch.is_some_and(|arch| { + let name = arch.name.to_ascii_lowercase(); + (name.contains("x86") || name == "x64" || name == "amd64") + && (arch.addr_size == 8 || name.contains("64")) + }); + let has_windows = registers.keys().any(|name| { + name.starts_with("RCX_0") + || name.starts_with("ECX_0") + || name.starts_with("RDX_0") + || name.starts_with("EDX_0") + }); + let has_sysv = registers.keys().any(|name| { + name.starts_with("RDI_0") + || name.starts_with("EDI_0") + || name.starts_with("RSI_0") + || name.starts_with("ESI_0") + }); + + if !(arch_looks_x64 || has_windows || has_sysv) { + return None; + } + + if has_windows || !has_sysv { + Some(MainArgRegisterProfile { + argc: &["RCX", "ECX"], + argv: &["RDX", "EDX"], + envp: &["R8", "R8D"], + }) + } else { + Some(MainArgRegisterProfile { + argc: &["RDI", "EDI"], + argv: &["RSI", "ESI"], + envp: &["RDX", "EDX"], + }) + } +} + +fn infer_main_pointer_bits( + prepared: &SsaArtifact, + arch: Option<&ArchSpec>, + profile: MainArgRegisterProfile, +) -> u32 { + let registers = collect_registers(prepared, false); + let from_registers = registers + .iter() + .filter_map(|(reg_name, bits)| { + let (prefix, _) = reg_name.rsplit_once('_')?; + (profile + .argc + .iter() + .chain(profile.argv.iter()) + .chain(profile.envp.iter()) + .any(|candidate| prefix.eq_ignore_ascii_case(candidate))) + .then_some(*bits) + }) + .max(); + if let Some(bits) = from_registers { + return bits; + } + + arch.map(|spec| spec.addr_size.saturating_mul(8)) + .filter(|bits| *bits > 0) + .or_else(|| { + registers + .into_iter() + .filter_map(|(reg_name, bits)| { + let (prefix, _) = reg_name.rsplit_once('_')?; + (profile + .argc + .iter() + .chain(profile.argv.iter()) + .chain(profile.envp.iter()) + .any(|candidate| prefix.eq_ignore_ascii_case(candidate))) + .then_some(bits) + }) + .max() + }) + .unwrap_or(64) +} + +fn set_register_family_concrete<'ctx>( + state: &mut SymState<'ctx>, + prepared: &SsaArtifact, + family: &[&str], + value: u64, +) { + let registers = collect_version_zero_registers(prepared); + for (reg_name, bits) in registers { + let Some((prefix, _)) = reg_name.rsplit_once('_') else { + continue; + }; + if family + .iter() + .any(|candidate| prefix.eq_ignore_ascii_case(candidate)) + { + state.set_concrete(®_name, value, bits); + } + } +} + +fn seed_concrete_bytes<'ctx>(state: &mut SymState<'ctx>, addr: u64, bytes: &[u8]) { + for (offset, byte) in bytes.iter().copied().enumerate() { + let dst = SymValue::concrete(addr + offset as u64, 64); + let value = SymValue::concrete(u64::from(byte), 8); + state.mem_write(&dst, &value, 1); + } +} + +fn seed_process_like_main_arguments<'ctx>( + state: &mut SymState<'ctx>, + prepared: &SsaArtifact, + scope: &PreparedFunctionScope, + arch: Option<&ArchSpec>, +) { + let is_main = root_name_looks_like_main(scope, prepared); + debug_main_seed_log(&format!( + "root_name={:?} prepared_name={:?} is_main={}", + scope.root().and_then(|function| function.name.as_deref()), + prepared.function().name.as_deref(), + is_main + )); + if !is_main { + return; + } + let Some(profile) = detect_main_arg_register_profile(prepared, arch) else { + debug_main_seed_log("no compatible arg-register profile"); + return; + }; + debug_main_seed_log(&format!( + "profile argc={:?} argv={:?} envp={:?}", + profile.argc, profile.argv, profile.envp + )); + + let (_, argv0_addr) = state.allocate_heap_region("main_argv0", 0x10); + seed_concrete_bytes(state, argv0_addr, b"prog\0"); + + let (_, argv1_addr) = state.allocate_heap_region("main_argv1", MAIN_ARGV1_SYMBOLIC_BYTES + 1); + for index in 0..MAIN_ARGV1_SYMBOLIC_BYTES { + let byte = state.new_symbolic_input(&format!("argv1_byte_{index}"), 8); + state.constrain_ne(&byte, 0); + let dst = SymValue::concrete(argv1_addr + index, 64); + state.mem_write(&dst, &byte, 1); + } + seed_concrete_bytes(state, argv1_addr + MAIN_ARGV1_SYMBOLIC_BYTES, &[0]); + + let (_, argv_table_addr) = state.allocate_heap_region("main_argv", 0x20); + let ptr_bits = infer_main_pointer_bits(prepared, arch, profile); + let ptr_size = (ptr_bits / 8) as usize; + for (index, value) in [argv0_addr, argv1_addr, 0].into_iter().enumerate() { + let dst = SymValue::concrete(argv_table_addr + (index * ptr_size) as u64, 64); + let value = SymValue::concrete(value, ptr_bits); + state.mem_write(&dst, &value, ptr_size as u32); + } + let argv0_slot = state + .mem_read(&SymValue::concrete(argv_table_addr, 64), ptr_size as u32) + .as_concrete() + .unwrap_or_default(); + let argv1_slot = state + .mem_read( + &SymValue::concrete(argv_table_addr + ptr_size as u64, 64), + ptr_size as u32, + ) + .as_concrete() + .unwrap_or_default(); + debug_main_seed_log(&format!( + "ptr_bits={ptr_bits} ptr_size={ptr_size} argv0_addr={argv0_addr:#x} argv1_addr={argv1_addr:#x} argv_table_addr={argv_table_addr:#x} argv_slots=[{argv0_slot:#x}, {argv1_slot:#x}]" + )); + + set_register_family_concrete(state, prepared, profile.argc, 2); + set_register_family_concrete(state, prepared, profile.argv, argv_table_addr); + set_register_family_concrete(state, prepared, profile.envp, 0); + let mut seeded = state + .registers() + .iter() + .filter_map(|(name, value)| { + let (prefix, _) = name.rsplit_once('_')?; + (profile + .argc + .iter() + .chain(profile.argv.iter()) + .chain(profile.envp.iter()) + .any(|candidate| prefix.eq_ignore_ascii_case(candidate))) + .then(|| format!("{name}={:#x}", value.as_concrete().unwrap_or_default())) + }) + .collect::>(); + seeded.sort(); + debug_main_seed_log(&format!("seeded registers {:?}", seeded)); +} + +pub fn seed_default_state_for_arch<'ctx>( + state: &mut SymState<'ctx>, + prepared: &SsaArtifact, + arch: Option<&ArchSpec>, +) { + let Some(profile) = arch.and_then(arch_seed_profile) else { return; }; @@ -72,12 +339,12 @@ pub fn seed_default_state_for_arch<'ctx>( } let bits = var.size * 8; - if stack_regs.contains(&base.as_str()) { - state.set_concrete(®_name, stack_value, bits); + if profile.stack_regs.contains(&base.as_str()) { + state.set_concrete(®_name, profile.stack_value, bits); return; } - if arg_regs.contains(&base.as_str()) { + if profile.arg_regs.contains(&base.as_str()) { let sym_name = base_name.to_ascii_lowercase(); state.make_symbolic_named(®_name, &sym_name, bits); } @@ -87,4 +354,479 @@ pub fn seed_default_state_for_arch<'ctx>( block.for_each_def(|def| maybe_seed(def.var)); block.for_each_source(|src| maybe_seed(src.var)); } + + seed_memory_regions_for_arch(state, prepared, arch); +} + +pub fn seed_scope_state_for_arch<'ctx>( + state: &mut SymState<'ctx>, + prepared: &SsaArtifact, + scope: &PreparedFunctionScope, + arch: Option<&ArchSpec>, +) { + seed_default_state_for_arch(state, prepared, arch); + seed_process_like_main_arguments(state, prepared, scope, arch); +} + +const STACK_WINDOW_BELOW: u64 = 0x8000; +const STACK_WINDOW_SIZE: u64 = 0x10000; +const GLOBAL_TAIL_EXTENT: u64 = 0x1000; +const MAIN_ARGV1_SYMBOLIC_BYTES: u64 = 0x40; + +pub fn seed_memory_regions_for_arch<'ctx>( + state: &mut SymState<'ctx>, + prepared: &SsaArtifact, + arch: Option<&ArchSpec>, +) { + let stack_value = arch + .and_then(arch_seed_profile) + .map(|profile| profile.stack_value) + .unwrap_or(0); + let has_stack_objects = prepared.objects().objects.values().any(|object| { + matches!( + object.kind, + ObjectKind::StackSlot { .. } | ObjectKind::FrameObject { .. } + ) + }); + if has_stack_objects && stack_value != 0 { + let stack_base = stack_value.saturating_sub(STACK_WINDOW_BELOW); + state.define_memory_region( + MemoryRegionKind::Stack, + "stack_window", + Some(stack_base), + Some(STACK_WINDOW_SIZE), + ); + } + + let mut globals = prepared + .objects() + .objects + .values() + .filter_map(|object| match &object.kind { + ObjectKind::Global { address, .. } => Some(*address), + _ => None, + }) + .collect::>(); + globals.sort_unstable(); + globals.dedup(); + + for (index, address) in globals.iter().copied().enumerate() { + let next = globals.get(index + 1).copied(); + let extent = next + .and_then(|next| next.checked_sub(address)) + .filter(|extent| *extent > 0) + .unwrap_or(GLOBAL_TAIL_EXTENT); + state.define_memory_region( + MemoryRegionKind::Global, + &format!("global_{address:x}"), + Some(address), + Some(extent), + ); + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum WindowsRuntimeHook { + AddVectoredExceptionHandler, + RaiseException, + VirtualAlloc, + VirtualProtect, + HeapAlloc, +} + +impl WindowsRuntimeHook { + fn call_hook_tag(self) -> CallHookTag { + match self { + WindowsRuntimeHook::AddVectoredExceptionHandler => { + CallHookTag::WindowsAddVectoredExceptionHandler + } + WindowsRuntimeHook::RaiseException => CallHookTag::WindowsRaiseException, + WindowsRuntimeHook::VirtualAlloc => CallHookTag::WindowsVirtualAlloc, + WindowsRuntimeHook::VirtualProtect => CallHookTag::WindowsVirtualProtect, + WindowsRuntimeHook::HeapAlloc => CallHookTag::WindowsHeapAlloc, + } + } +} + +fn arch_supports_windows_runtime(arch: &ArchSpec) -> bool { + let name = arch.name.to_ascii_lowercase(); + (name.contains("x86") || name == "x64" || name == "amd64") + && (arch.addr_size == 8 || name.contains("64")) +} + +fn windows_x64_callconv() -> CallConv { + CallConv::new(vec!["RCX", "RDX", "R8", "R9"], "RAX", 64, 64) +} + +fn normalize_windows_runtime_hook(name: &str) -> Option { + let lower = name.to_ascii_lowercase(); + let normalized = lower.trim(); + if normalized.ends_with("addvectoredexceptionhandler") { + Some(WindowsRuntimeHook::AddVectoredExceptionHandler) + } else if normalized.ends_with("raiseexception") { + Some(WindowsRuntimeHook::RaiseException) + } else if normalized.ends_with("virtualalloc") { + Some(WindowsRuntimeHook::VirtualAlloc) + } else if normalized.ends_with("virtualprotect") { + Some(WindowsRuntimeHook::VirtualProtect) + } else if normalized.ends_with("heapalloc") { + Some(WindowsRuntimeHook::HeapAlloc) + } else { + None + } +} + +fn page_protection_is_executable(value: u64) -> bool { + matches!(value, 0x10 | 0x20 | 0x40 | 0x80) +} + +fn seed_exception_continuation<'ctx>( + state: &mut SymState<'ctx>, + exception_code: u64, + handler_addr: u64, +) -> CallHookResult { + state.seed_exception_continuation(exception_code, handler_addr); + CallHookResult::Jump(handler_addr) +} + +fn apply_windows_runtime_hook<'ctx>( + state: &mut SymState<'ctx>, + hook: WindowsRuntimeHook, +) -> CallHookResult { + let callconv = windows_x64_callconv(); + let call = callconv.collect_call_info(state, 4); + match hook { + WindowsRuntimeHook::AddVectoredExceptionHandler => { + let handler = call.args.get(1).and_then(SymValue::as_concrete); + if let Some(handler) = handler { + state.register_exception_handler(handler); + callconv.write_return(state, SymValue::concrete(handler, callconv.ret_bits())); + CallHookResult::Fallthrough + } else { + CallHookResult::Terminate(ExitStatus::RuntimeBlocked( + crate::state::RuntimeBlockReason::MissingContinuationSeed, + )) + } + } + WindowsRuntimeHook::RaiseException => { + let Some(exception_code) = call.args.first().and_then(SymValue::as_concrete) else { + return CallHookResult::Terminate(ExitStatus::RuntimeBlocked( + crate::state::RuntimeBlockReason::MissingContinuationSeed, + )); + }; + let Some(handler_addr) = state.primary_exception_handler() else { + return CallHookResult::Terminate(ExitStatus::RuntimeBlocked( + crate::state::RuntimeBlockReason::MissingExceptionHandler, + )); + }; + seed_exception_continuation(state, exception_code, handler_addr) + } + WindowsRuntimeHook::VirtualAlloc => { + let size = call + .args + .get(1) + .and_then(SymValue::as_concrete) + .unwrap_or(0x1000) + .max(1); + let executable = call + .args + .get(3) + .and_then(SymValue::as_concrete) + .is_some_and(page_protection_is_executable); + let (_region_id, base_addr) = + state.allocate_heap_region(&format!("virtualalloc_{:x}", state.pc), size); + state.register_runtime_region_alias(base_addr, size, executable); + callconv.write_return(state, SymValue::concrete(base_addr, callconv.ret_bits())); + CallHookResult::Fallthrough + } + WindowsRuntimeHook::VirtualProtect => { + let addr = call + .args + .first() + .and_then(SymValue::as_concrete) + .unwrap_or(0); + let size = call + .args + .get(1) + .and_then(SymValue::as_concrete) + .unwrap_or(0) + .max(1); + let executable = call + .args + .get(2) + .and_then(SymValue::as_concrete) + .is_some_and(page_protection_is_executable); + if executable { + state.mark_runtime_region_executable(addr, size); + } + callconv.write_return(state, SymValue::concrete(1, callconv.ret_bits())); + CallHookResult::Fallthrough + } + WindowsRuntimeHook::HeapAlloc => { + let size = call + .args + .get(2) + .and_then(SymValue::as_concrete) + .unwrap_or(0x100) + .max(1); + let (_region_id, base_addr) = + state.allocate_heap_region(&format!("heapalloc_{:x}", state.pc), size); + callconv.write_return(state, SymValue::concrete(base_addr, callconv.ret_bits())); + CallHookResult::Fallthrough + } + } +} + +pub fn install_runtime_hooks_for_scope<'ctx>( + explorer: &mut PathExplorer<'ctx>, + scope: &PreparedFunctionScope, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, +) { + let Some(arch) = arch else { + return; + }; + if !arch_supports_windows_runtime(arch) { + return; + } + + let mut targets = BTreeMap::new(); + for function in scope.functions().values() { + for call in function.prepared.call_sites().by_id.values() { + if let Some(target) = function.prepared.resolved_call_target(call) + && let Some(raw_name) = symbol_map.get(&target) + && let Some(kind) = normalize_windows_runtime_hook(raw_name) + { + targets.entry(target).or_insert(kind); + } + } + } + + for (&target, raw_name) in symbol_map { + let Some(kind) = normalize_windows_runtime_hook(raw_name) else { + continue; + }; + targets.entry(target).or_insert(kind); + } + + for (target, kind) in targets { + explorer.register_tagged_call_hook(target, kind.call_hook_tag(), move |state| { + apply_windows_runtime_hook(state, kind) + }); + } +} + +#[cfg(test)] +mod tests { + use super::{ + WindowsRuntimeHook, apply_windows_runtime_hook, install_runtime_hooks_for_scope, + seed_scope_state_for_arch, + }; + use crate::executor::CallHookResult; + use crate::path::PathExplorer; + use crate::sim::{PreparedFunctionScope, ScopedPreparedFunction}; + use crate::state::SymState; + use r2il::{AddressSpace, ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; + use r2ssa::{InterprocFunctionId, SsaArtifact}; + use std::collections::HashMap; + use z3::Context; + + fn make_main_seed_arch(addr_size: u32) -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = addr_size; + arch.add_space(AddressSpace::ram(8)); + arch.add_register(RegisterDef::new("RCX", 0x80, 8)); + arch.add_register(RegisterDef::new("ECX", 0x80, 4)); + arch.add_register(RegisterDef::new("RDX", 0x88, 8)); + arch.add_register(RegisterDef::new("EDX", 0x88, 4)); + arch.add_register(RegisterDef::new("R8", 0x90, 8)); + arch.add_register(RegisterDef::new("R8D", 0x90, 4)); + arch + } + + fn make_main_seed_scope(arch: &ArchSpec) -> (SsaArtifact, PreparedFunctionScope) { + let mut block = R2ILBlock::new(0x1000, 1); + block.push(R2ILOp::Copy { + dst: Varnode::unique(0x10, 8), + src: Varnode::register(0x80, 8), + }); + block.push(R2ILOp::Copy { + dst: Varnode::unique(0x20, 8), + src: Varnode::register(0x88, 8), + }); + block.push(R2ILOp::Copy { + dst: Varnode::unique(0x30, 8), + src: Varnode::register(0x90, 8), + }); + block.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + let prepared = SsaArtifact::for_symbolic(&[block], Some(arch)) + .expect("ssa") + .with_name("main"); + let scope = PreparedFunctionScope::new( + 0x1000, + vec![ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("main".to_string()), + prepared: prepared.clone(), + }], + ) + .expect("scope"); + (prepared, scope) + } + + #[test] + fn test_virtualalloc_hook_writes_current_return_alias() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1400_1000); + state.set_concrete("RCX_4", 0, 64); + state.set_concrete("RDX_4", 0x1000, 64); + state.set_concrete("R8_4", 0x3000, 64); + state.set_concrete("R9_4", 0x40, 64); + state.set_concrete("RAX_7", 0xdead_beef, 64); + + let result = apply_windows_runtime_hook(&mut state, WindowsRuntimeHook::VirtualAlloc); + assert_eq!(result, CallHookResult::Fallthrough); + + let base_addr = state + .get_register("RAX_7") + .as_concrete() + .expect("virtualalloc return should be concrete"); + assert_ne!(base_addr, 0xdead_beef); + let region = state + .runtime_region_for_pc(base_addr) + .expect("allocated runtime region should be registered"); + assert!(region.executable); + assert_eq!(region.size, 0x1000); + } + + #[test] + fn main_seed_uses_register_width_for_argv_table_layout() { + let ctx = Context::thread_local(); + let arch = make_main_seed_arch(4); + let (prepared, scope) = make_main_seed_scope(&arch); + let mut state = SymState::new(&ctx, 0x1000); + + seed_scope_state_for_arch(&mut state, &prepared, &scope, Some(&arch)); + + let argv_addr = state + .get_register("RDX_0") + .as_concrete() + .expect("argv register should be concrete"); + let argv0_addr = state + .mem_read(&crate::SymValue::concrete(argv_addr, 64), 8) + .as_concrete() + .expect("argv[0] should be present"); + let argv1_addr = state + .mem_read(&crate::SymValue::concrete(argv_addr + 8, 64), 8) + .as_concrete() + .expect("argv[1] should be written at pointer-width stride"); + + assert_ne!(argv0_addr, 0); + assert_ne!(argv1_addr, 0); + assert_ne!(argv0_addr, argv1_addr); + assert_eq!( + state + .mem_read( + &crate::SymValue::concrete(argv1_addr + super::MAIN_ARGV1_SYMBOLIC_BYTES, 64,), + 1, + ) + .as_concrete(), + Some(0), + "argv[1] should remain NUL-terminated", + ); + } + + #[test] + fn main_seed_only_initializes_version_zero_argument_registers() { + let ctx = Context::thread_local(); + let arch = make_main_seed_arch(8); + let (prepared, scope) = make_main_seed_scope(&arch); + let mut state = SymState::new(&ctx, 0x1000); + + seed_scope_state_for_arch(&mut state, &prepared, &scope, Some(&arch)); + + let argv_entry = state + .get_register("RDX_0") + .as_concrete() + .expect("argv register should seed the entry SSA version"); + assert_ne!(argv_entry, 0); + assert!( + state.get_register("RDX_1").as_concrete().is_none(), + "later SSA versions must be left for execution to define" + ); + assert!( + state.get_register("RCX_1").as_concrete().is_none(), + "later argc versions must not be pre-seeded either" + ); + } + + #[test] + fn install_runtime_hooks_uses_symbol_map_imports_even_without_scope_calls() { + let ctx = Context::thread_local(); + let arch = make_main_seed_arch(8); + let (_prepared, scope) = make_main_seed_scope(&arch); + let blocks = vec![ + R2ILBlock { + addr: 0x2000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Call { + target: Varnode { + space: SpaceId::Ram, + offset: 0x401000, + size: 8, + meta: None, + }, + }, + R2ILOp::IntNotEqual { + dst: Varnode::unique(0x20, 1), + a: Varnode::register(0x100, 8), + b: Varnode::constant(0, 8), + }, + R2ILOp::CBranch { + target: Varnode::constant(0x2010, 8), + cond: Varnode::unique(0x20, 1), + }, + ], + }, + R2ILBlock { + addr: 0x2004, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: Varnode::constant(0, 8), + }], + }, + R2ILBlock { + addr: 0x2010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: Varnode::constant(0, 8), + }], + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("ssa"); + let mut explorer = PathExplorer::new(&ctx); + let symbol_map = + HashMap::from([(0x401000, "sym.imp.KERNEL32.dll_VirtualAlloc".to_string())]); + install_runtime_hooks_for_scope(&mut explorer, &scope, Some(&arch), &symbol_map); + + let mut state = SymState::new(&ctx, 0x2000); + state.set_concrete("RDX_0", 0x1000, 64); + state.set_concrete("R9_0", 0x40, 64); + state.set_concrete("RAX_0", 0, 64); + + let paths = explorer.find_paths_to(&func, state, 0x2010); + assert!( + !paths.is_empty(), + "symbol-map runtime imports should install hooks even when scope call discovery is absent" + ); + } } diff --git a/crates/r2sym/src/semantics/artifact.rs b/crates/r2sym/src/semantics/artifact.rs new file mode 100644 index 0000000..34c6b87 --- /dev/null +++ b/crates/r2sym/src/semantics/artifact.rs @@ -0,0 +1,871 @@ +use serde::{Deserialize, Serialize}; + +use super::plan::{ + ArtifactBuildPlan, DecompilePlan, QueryPlan, TargetQueryExecutionRoute, TargetQueryPlan, + TargetQueryRoutePlan, TypePlan, derive_artifact_build_plan, derive_decompile_plan, + derive_query_plan, derive_target_query_plan, derive_target_query_route_plan, derive_type_plan, +}; +use super::region::{ + ArtifactGranularity, ExecutionModel, NativeArtifactBody, RefinementStage, + SemanticArtifactDiagnostics, SemanticRegion, SemanticTargetConditionSource, VmArtifactBody, +}; +use super::vm::InterpreterKind; + +#[derive(Debug, Clone)] +pub(crate) struct TargetQueryRouteInput<'a> { + pub(crate) route: TargetQueryRoutePlan, + pub(crate) condition_source: Option>, + pub(crate) memory_terms: Vec<&'a crate::backward::BackwardMemoryCondition>, + pub(crate) allow_exact_proof: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum SliceClass { + Wrapper, + Worker, + RecursiveGroup, + InterpreterSwitch, + InterpreterIndirect, + GenericLarge, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum ResidualReason { + MissingArch, + LargeCfg, + SummaryBudgetExhausted, + SccBudgetExhausted, + InterpreterRequiresStepSummary, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum SemanticConfidence { + Exact, + Likely, + Heuristic, + Residual, +} + +impl SemanticConfidence { + pub fn is_reliable(self) -> bool { + matches!(self, Self::Exact | Self::Likely) + } + + pub fn is_usable(self) -> bool { + !matches!(self, Self::Residual) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum SemanticEvidenceSoundness { + Proven, + UnderApprox, + OverApprox, + Ranked, + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum SemanticEvidenceCoverage { + Full, + Partial, + Bounded, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum SemanticEvidenceProvenance { + Stable, + Normalized, + Ranked, + Unstable, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum SemanticEvidenceAmbiguity { + Single, + Bounded, + Ranked, + Multiple, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum SemanticEvidenceReason { + LargeCfg, + SummaryBudget, + AliasAmbiguity, + ConflictingTargetSources, + ReplayOverlap, + HeapIdentityWeak, + GuardOpaque, + ValueOpaque, + TruncatedTransfer, + DerivedFromRanking, + PartialPathCoverage, + ResidualSearchRequired, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct SemanticEvidence { + pub tier: SemanticConfidence, + pub soundness: SemanticEvidenceSoundness, + pub coverage: SemanticEvidenceCoverage, + pub provenance: SemanticEvidenceProvenance, + pub ambiguity: SemanticEvidenceAmbiguity, + pub budget_limited: bool, + pub reasons: Vec, +} + +impl Default for SemanticEvidence { + fn default() -> Self { + Self::exact() + } +} + +impl SemanticEvidence { + pub fn is_default_exact(&self) -> bool { + *self == Self::exact() + } + + pub fn exact() -> Self { + Self { + tier: SemanticConfidence::Exact, + soundness: SemanticEvidenceSoundness::Proven, + coverage: SemanticEvidenceCoverage::Full, + provenance: SemanticEvidenceProvenance::Stable, + ambiguity: SemanticEvidenceAmbiguity::Single, + budget_limited: false, + reasons: Vec::new(), + } + } + + pub fn likely(reason: SemanticEvidenceReason) -> Self { + Self { + tier: SemanticConfidence::Likely, + soundness: SemanticEvidenceSoundness::OverApprox, + coverage: SemanticEvidenceCoverage::Full, + provenance: SemanticEvidenceProvenance::Normalized, + ambiguity: SemanticEvidenceAmbiguity::Single, + budget_limited: false, + reasons: vec![reason], + } + } + + pub fn heuristic(reason: SemanticEvidenceReason) -> Self { + Self { + tier: SemanticConfidence::Heuristic, + soundness: SemanticEvidenceSoundness::Ranked, + coverage: SemanticEvidenceCoverage::Partial, + provenance: SemanticEvidenceProvenance::Ranked, + ambiguity: SemanticEvidenceAmbiguity::Ranked, + budget_limited: false, + reasons: vec![reason], + } + } + + pub fn residual(reason: SemanticEvidenceReason) -> Self { + Self { + tier: SemanticConfidence::Residual, + soundness: SemanticEvidenceSoundness::Unknown, + coverage: SemanticEvidenceCoverage::Partial, + provenance: SemanticEvidenceProvenance::Unstable, + ambiguity: SemanticEvidenceAmbiguity::Multiple, + budget_limited: false, + reasons: vec![reason], + } + } + + pub fn with_coverage(mut self, coverage: SemanticEvidenceCoverage) -> Self { + self.coverage = coverage; + self + } + + pub fn with_provenance(mut self, provenance: SemanticEvidenceProvenance) -> Self { + self.provenance = provenance; + self + } + + pub fn with_ambiguity(mut self, ambiguity: SemanticEvidenceAmbiguity) -> Self { + self.ambiguity = ambiguity; + self + } + + pub fn with_budget_limited(mut self, budget_limited: bool) -> Self { + self.budget_limited = budget_limited; + self + } + + pub fn with_reason(mut self, reason: SemanticEvidenceReason) -> Self { + if !self.reasons.contains(&reason) { + self.reasons.push(reason); + } + self + } + + pub fn is_reliable(&self) -> bool { + self.tier.is_reliable() + && matches!( + self.soundness, + SemanticEvidenceSoundness::Proven | SemanticEvidenceSoundness::OverApprox + ) + && !matches!(self.ambiguity, SemanticEvidenceAmbiguity::Multiple) + } + + pub fn is_usable(&self) -> bool { + self.tier.is_usable() && !matches!(self.soundness, SemanticEvidenceSoundness::Unknown) + } + + pub fn allows_hard_proof(&self) -> bool { + matches!(self.tier, SemanticConfidence::Exact) + && matches!(self.soundness, SemanticEvidenceSoundness::Proven) + && matches!(self.coverage, SemanticEvidenceCoverage::Full) + && !matches!(self.ambiguity, SemanticEvidenceAmbiguity::Multiple) + } + + pub fn allows_narrowing(&self) -> bool { + matches!( + self.tier, + SemanticConfidence::Exact | SemanticConfidence::Likely + ) && matches!( + self.soundness, + SemanticEvidenceSoundness::Proven | SemanticEvidenceSoundness::OverApprox + ) && !matches!(self.coverage, SemanticEvidenceCoverage::Partial) + && !matches!(self.ambiguity, SemanticEvidenceAmbiguity::Multiple) + } + + pub fn allows_guarded_structuring(&self) -> bool { + matches!( + self.tier, + SemanticConfidence::Exact | SemanticConfidence::Likely + ) && matches!( + self.soundness, + SemanticEvidenceSoundness::Proven + | SemanticEvidenceSoundness::UnderApprox + | SemanticEvidenceSoundness::OverApprox + ) && matches!( + self.coverage, + SemanticEvidenceCoverage::Full | SemanticEvidenceCoverage::Bounded + ) && !matches!(self.ambiguity, SemanticEvidenceAmbiguity::Multiple) + && !self + .reasons + .contains(&SemanticEvidenceReason::ConflictingTargetSources) + } + + pub fn allows_ranking(&self) -> bool { + !matches!(self.tier, SemanticConfidence::Residual) + && !matches!(self.soundness, SemanticEvidenceSoundness::Unknown) + } + + pub fn combined_with(&self, other: &Self) -> Self { + let mut reasons = self.reasons.clone(); + reasons.extend(other.reasons.iter().copied()); + reasons.sort_unstable(); + reasons.dedup(); + Self { + tier: weaker_confidence(self.tier, other.tier), + soundness: weaker_soundness(self.soundness, other.soundness), + coverage: weaker_coverage(self.coverage, other.coverage), + provenance: weaker_provenance(self.provenance, other.provenance), + ambiguity: weaker_ambiguity(self.ambiguity, other.ambiguity), + budget_limited: self.budget_limited || other.budget_limited, + reasons, + } + } + + pub fn downgraded_for_conflict(&self) -> Self { + self.combined_with( + &Self::residual(SemanticEvidenceReason::ConflictingTargetSources) + .with_budget_limited(self.budget_limited), + ) + } + + pub fn refused(reason: SemanticEvidenceReason) -> Self { + Self::residual(reason) + } + + pub fn strength_rank(&self) -> (u8, u8, u8, u8, u8, u8) { + let tier_rank = match self.tier { + SemanticConfidence::Exact => 4, + SemanticConfidence::Likely => 3, + SemanticConfidence::Heuristic => 2, + SemanticConfidence::Residual => 1, + }; + let soundness_rank = match self.soundness { + SemanticEvidenceSoundness::Proven => 5, + SemanticEvidenceSoundness::UnderApprox => 4, + SemanticEvidenceSoundness::OverApprox => 3, + SemanticEvidenceSoundness::Ranked => 2, + SemanticEvidenceSoundness::Unknown => 1, + }; + let coverage_rank = match self.coverage { + SemanticEvidenceCoverage::Full => 3, + SemanticEvidenceCoverage::Bounded => 2, + SemanticEvidenceCoverage::Partial => 1, + }; + let provenance_rank = match self.provenance { + SemanticEvidenceProvenance::Stable => 4, + SemanticEvidenceProvenance::Normalized => 3, + SemanticEvidenceProvenance::Ranked => 2, + SemanticEvidenceProvenance::Unstable => 1, + }; + let ambiguity_rank = match self.ambiguity { + SemanticEvidenceAmbiguity::Single => 4, + SemanticEvidenceAmbiguity::Bounded => 3, + SemanticEvidenceAmbiguity::Ranked => 2, + SemanticEvidenceAmbiguity::Multiple => 1, + }; + ( + self.allows_hard_proof() as u8, + self.allows_narrowing() as u8, + soundness_rank, + tier_rank, + coverage_rank, + provenance_rank * 4 + ambiguity_rank - u8::from(self.budget_limited), + ) + } +} + +fn weaker_confidence(left: SemanticConfidence, right: SemanticConfidence) -> SemanticConfidence { + match (left, right) { + (SemanticConfidence::Residual, _) | (_, SemanticConfidence::Residual) => { + SemanticConfidence::Residual + } + (SemanticConfidence::Heuristic, _) | (_, SemanticConfidence::Heuristic) => { + SemanticConfidence::Heuristic + } + (SemanticConfidence::Likely, _) | (_, SemanticConfidence::Likely) => { + SemanticConfidence::Likely + } + _ => SemanticConfidence::Exact, + } +} + +fn weaker_soundness( + left: SemanticEvidenceSoundness, + right: SemanticEvidenceSoundness, +) -> SemanticEvidenceSoundness { + use SemanticEvidenceSoundness as Soundness; + match (left, right) { + (Soundness::Unknown, _) | (_, Soundness::Unknown) => Soundness::Unknown, + (Soundness::Ranked, _) | (_, Soundness::Ranked) => Soundness::Ranked, + (Soundness::OverApprox, Soundness::UnderApprox) + | (Soundness::UnderApprox, Soundness::OverApprox) => Soundness::Ranked, + (Soundness::OverApprox, _) | (_, Soundness::OverApprox) => Soundness::OverApprox, + (Soundness::UnderApprox, _) | (_, Soundness::UnderApprox) => Soundness::UnderApprox, + _ => Soundness::Proven, + } +} + +fn weaker_coverage( + left: SemanticEvidenceCoverage, + right: SemanticEvidenceCoverage, +) -> SemanticEvidenceCoverage { + use SemanticEvidenceCoverage as Coverage; + match (left, right) { + (Coverage::Partial, _) | (_, Coverage::Partial) => Coverage::Partial, + (Coverage::Bounded, _) | (_, Coverage::Bounded) => Coverage::Bounded, + _ => Coverage::Full, + } +} + +fn weaker_provenance( + left: SemanticEvidenceProvenance, + right: SemanticEvidenceProvenance, +) -> SemanticEvidenceProvenance { + left.max(right) +} + +fn weaker_ambiguity( + left: SemanticEvidenceAmbiguity, + right: SemanticEvidenceAmbiguity, +) -> SemanticEvidenceAmbiguity { + left.max(right) +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SemanticArtifactBody { + Native(NativeArtifactBody), + Vm(Box), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SemanticArtifact { + pub stage: RefinementStage, + pub granularity: ArtifactGranularity, + pub execution: ExecutionModel, + pub body: SemanticArtifactBody, + pub diagnostics: SemanticArtifactDiagnostics, +} + +impl SemanticArtifact { + pub fn normalized(mut self) -> Self { + self.diagnostics = self.diagnostics.normalized(); + self + } + + fn downgrade_query_route_to_residual( + route: TargetQueryRoutePlan, + reason: impl Into, + ) -> TargetQueryRoutePlan { + let mut reasons = match route.execution { + TargetQueryExecutionRoute::ResidualOnly { reasons } => reasons, + TargetQueryExecutionRoute::Refuse { reason } => vec![reason], + _ => Vec::new(), + }; + reasons.push(reason.into()); + TargetQueryRoutePlan { + target_plan: route.target_plan, + execution: TargetQueryExecutionRoute::ResidualOnly { reasons }, + } + } + + fn has_native_semantics(&self) -> bool { + self.native_body() + .is_some_and(|body| !body.regions.is_empty()) + } + + fn supports_native_semantic_structuring(&self) -> bool { + self.native_body() + .is_some_and(NativeArtifactBody::supports_guarded_structuring) + } + + fn has_query_support(&self) -> bool { + self.native_body() + .is_some_and(NativeArtifactBody::supports_query_guidance) + } + + pub fn slice_class(&self) -> Option { + match &self.body { + SemanticArtifactBody::Native(body) => Some(body.summary.slice_class), + SemanticArtifactBody::Vm(body) => body + .step_summary + .as_ref() + .map(|summary| slice_class_for_interpreter_kind(summary.kind)) + .or_else(|| { + body.transfer_summary + .as_ref() + .map(|summary| slice_class_for_interpreter_kind(summary.kind)) + }) + .or_else(|| { + body.interpreter + .as_ref() + .map(|summary| slice_class_for_interpreter_kind(summary.kind)) + }), + } + } + + pub fn build_plan(&self) -> ArtifactBuildPlan { + derive_artifact_build_plan(self.stage, &self.diagnostics) + } + + pub fn query_plan(&self) -> QueryPlan { + derive_query_plan( + self.stage, + self.execution, + &self.diagnostics, + self.has_query_support(), + ) + } + + pub fn target_query_plan(&self, target_addr: u64) -> TargetQueryPlan { + let query_plan = self.query_plan(); + let has_guidance = self + .native_body() + .is_some_and(|body| body.has_target_guidance(target_addr, false)); + let necessary_for_target = self + .native_body() + .is_some_and(|body| body.target_guidance_is_necessary(target_addr, false)); + let has_source_conflict = self.target_has_ambiguous_sources(target_addr); + derive_target_query_plan( + &query_plan, + has_guidance, + has_source_conflict, + necessary_for_target, + ) + } + + pub fn target_query_route_plan(&self, target_addr: u64) -> TargetQueryRoutePlan { + let query_plan = self.query_plan(); + let target_plan = self.target_query_plan(target_addr); + let condition_source = self.target_condition_source(target_addr, false); + let authoritative_memory_region = self.authoritative_memory_region_for_target(target_addr); + let has_memory_guidance = authoritative_memory_region.is_some_and(|region| { + !region + .actionable_memory_terms_for_target(target_addr) + .is_empty() + }); + derive_target_query_route_plan( + &query_plan, + &target_plan, + self.execution, + condition_source.is_some(), + has_memory_guidance, + ) + } + + pub(crate) fn target_query_route_input<'a>( + &'a self, + target_addr: u64, + assumption_conflicted: bool, + ) -> TargetQueryRouteInput<'a> { + let mut route = self.target_query_route_plan(target_addr); + let canonical_source = self.target_condition_source(target_addr, false); + let memory_terms = self + .authoritative_memory_region_for_target(target_addr) + .map(|region| region.actionable_memory_terms_for_target(target_addr)) + .unwrap_or_default(); + if assumption_conflicted { + route = Self::downgrade_query_route_to_residual(route, "assumption conflict"); + } + if self.target_has_ambiguous_sources(target_addr) { + route = Self::downgrade_query_route_to_residual( + route, + "conflicting target guidance sources", + ); + } + let allow_exact_proof = + !assumption_conflicted && !self.target_has_ambiguous_sources(target_addr); + TargetQueryRouteInput { + route, + condition_source: canonical_source, + memory_terms, + allow_exact_proof, + } + } + + pub fn type_plan(&self) -> TypePlan { + derive_type_plan( + self.stage, + self.execution, + &self.diagnostics, + self.has_native_semantics(), + ) + } + + pub fn decompile_plan(&self) -> DecompilePlan { + derive_decompile_plan( + self.stage, + self.execution, + &self.diagnostics, + self.has_native_semantics(), + self.supports_native_semantic_structuring(), + ) + } + + pub fn native_body(&self) -> Option<&NativeArtifactBody> { + match &self.body { + SemanticArtifactBody::Native(body) => Some(body), + SemanticArtifactBody::Vm(_) => None, + } + } + + pub fn vm_body(&self) -> Option<&VmArtifactBody> { + match &self.body { + SemanticArtifactBody::Native(_) => None, + SemanticArtifactBody::Vm(body) => Some(body.as_ref()), + } + } + + pub fn exact_reachable_target_for_block(&self, block_addr: u64) -> Option { + self.native_body()? + .exact_reachable_target_for_block(block_addr) + } + + pub fn actionable_reachable_target_for_block(&self, block_addr: u64) -> Option { + self.native_body()? + .actionable_reachable_target_for_block(block_addr) + } + + pub fn exact_branch_truth_for_block(&self, block_addr: u64) -> Option { + self.native_body()?.exact_branch_truth_for_block(block_addr) + } + + pub fn exact_compiled_condition_for_block( + &self, + block_addr: u64, + ) -> Option<&crate::backward::BackwardConditionSummary> { + self.native_body()? + .exact_compiled_condition_for_block(block_addr) + } + + pub fn actionable_compiled_condition_for_block( + &self, + block_addr: u64, + ) -> Option<&crate::backward::BackwardConditionSummary> { + self.native_body()? + .actionable_compiled_condition_for_block(block_addr) + } + + pub fn actionable_memory_terms_for_block( + &self, + block_addr: u64, + ) -> Vec<&crate::backward::BackwardMemoryCondition> { + self.native_body() + .map(|body| body.actionable_memory_terms_for_block(block_addr)) + .unwrap_or_default() + } + + pub fn exact_control_count(&self) -> usize { + self.native_body() + .map(NativeArtifactBody::exact_control_count) + .unwrap_or_default() + } + + pub fn actionable_control_count(&self) -> usize { + self.native_body() + .map(NativeArtifactBody::actionable_control_count) + .unwrap_or_default() + } + + pub fn actionable_regions(&self) -> Vec<&SemanticRegion> { + self.native_body() + .map(|body| body.actionable_regions().collect()) + .unwrap_or_default() + } + + pub fn target_condition_source( + &self, + target_addr: u64, + proof_only: bool, + ) -> Option> { + self.native_body()? + .target_condition_source(target_addr, proof_only) + } + + pub fn actionable_compiled_condition_for_target( + &self, + target_addr: u64, + ) -> Option<&crate::backward::BackwardConditionSummary> { + self.native_body()? + .actionable_compiled_condition_for_target(target_addr) + } + + pub fn exact_compiled_condition_for_target( + &self, + target_addr: u64, + ) -> Option<&crate::backward::BackwardConditionSummary> { + self.native_body()? + .exact_compiled_condition_for_target(target_addr) + } + + pub fn actionable_memory_terms_for_target( + &self, + target_addr: u64, + ) -> Vec<&crate::backward::BackwardMemoryCondition> { + self.native_body() + .map(|body| body.actionable_memory_terms_for_target(target_addr)) + .unwrap_or_default() + } + + pub fn authoritative_memory_region_for_target( + &self, + target_addr: u64, + ) -> Option<&SemanticRegion> { + self.native_body()? + .authoritative_memory_region_for_target(target_addr) + } + + pub fn authoritative_region_for_target( + &self, + target_addr: u64, + proof_only: bool, + ) -> Option<&SemanticRegion> { + self.native_body()? + .authoritative_region_for_target(target_addr, proof_only) + } + + pub fn ambiguous_targets(&self) -> Vec { + if !self.diagnostics.ambiguous_targets.is_empty() { + return self.diagnostics.ambiguous_targets.clone(); + } + self.native_body() + .map(|body| body.conflicting_targets(false).into_iter().collect()) + .unwrap_or_default() + } + + pub fn target_has_ambiguous_sources(&self, target_addr: u64) -> bool { + self.diagnostics.ambiguous_targets.contains(&target_addr) + || self + .native_body() + .is_some_and(|body| body.target_source_conflict(target_addr, false)) + } + + pub fn vm_step_for_dispatch_header( + &self, + dispatch_header: u64, + ) -> Option<&crate::VmStepSummary> { + self.vm_body()? + .step_summary + .as_ref() + .filter(|summary| summary.dispatch_header == dispatch_header) + } + + pub fn vm_transfer_for_dispatch_header( + &self, + dispatch_header: u64, + ) -> Option<&crate::VmStepSummary> { + self.vm_body()? + .transfer_summary + .as_ref() + .filter(|summary| summary.dispatch_header == dispatch_header) + } + + pub fn supports_guarded_structuring(&self) -> bool { + self.decompile_plan().allows_native_structuring() + } + + pub fn supports_native_semantic_linearization(&self) -> bool { + self.decompile_plan().allows_native_linearization() + } + + pub fn vm_summary_only_type_plan(&self) -> bool { + self.type_plan().is_vm_summary_only() + } + + pub fn vm_summary_only_decompile_plan(&self) -> bool { + self.decompile_plan().is_vm_summary_only() + } +} + +fn slice_class_for_interpreter_kind(kind: InterpreterKind) -> SliceClass { + match kind { + InterpreterKind::SwitchDispatch => SliceClass::InterpreterSwitch, + InterpreterKind::IndirectDispatch => SliceClass::InterpreterIndirect, + } +} + +impl SemanticArtifactDiagnostics { + pub fn normalized(mut self) -> Self { + self.residual_reasons.sort_unstable(); + self.residual_reasons.dedup(); + self.ambiguous_targets.sort_unstable(); + self.ambiguous_targets.dedup(); + self + } +} + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + + use super::{ + ResidualReason, SemanticArtifact, SemanticArtifactBody, SemanticArtifactDiagnostics, + SemanticConfidence, SemanticEvidence, SemanticEvidenceAmbiguity, SemanticEvidenceCoverage, + SemanticEvidenceProvenance, SemanticEvidenceReason, SemanticEvidenceSoundness, SliceClass, + }; + use crate::sim::DerivedSummaryDiagnostics; + use crate::{ + ArtifactGranularity, ExecutionModel, NativeArtifactBody, NativeFunctionSummary, + RefinementStage, + }; + + fn native_artifact(diagnostics: SemanticArtifactDiagnostics) -> SemanticArtifact { + SemanticArtifact { + stage: RefinementStage::Compiled, + granularity: ArtifactGranularity::WholeFunction, + execution: ExecutionModel::Native, + body: SemanticArtifactBody::Native(NativeArtifactBody { + summary: NativeFunctionSummary { + slice_class: SliceClass::Worker, + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: DerivedSummaryDiagnostics::default(), + }, + regions: Default::default(), + }), + diagnostics, + } + } + + fn evidence_from_seed(seed: u8) -> SemanticEvidence { + let tier = match seed % 4 { + 0 => SemanticConfidence::Exact, + 1 => SemanticConfidence::Likely, + 2 => SemanticConfidence::Heuristic, + _ => SemanticConfidence::Residual, + }; + let soundness = match (seed / 4) % 5 { + 0 => SemanticEvidenceSoundness::Proven, + 1 => SemanticEvidenceSoundness::UnderApprox, + 2 => SemanticEvidenceSoundness::OverApprox, + 3 => SemanticEvidenceSoundness::Ranked, + _ => SemanticEvidenceSoundness::Unknown, + }; + let coverage = match (seed / 20) % 3 { + 0 => SemanticEvidenceCoverage::Full, + 1 => SemanticEvidenceCoverage::Bounded, + _ => SemanticEvidenceCoverage::Partial, + }; + let provenance = match (seed / 60) % 4 { + 0 => SemanticEvidenceProvenance::Stable, + 1 => SemanticEvidenceProvenance::Normalized, + 2 => SemanticEvidenceProvenance::Ranked, + _ => SemanticEvidenceProvenance::Unstable, + }; + let ambiguity = match (seed / 120) % 4 { + 0 => SemanticEvidenceAmbiguity::Single, + 1 => SemanticEvidenceAmbiguity::Bounded, + 2 => SemanticEvidenceAmbiguity::Ranked, + _ => SemanticEvidenceAmbiguity::Multiple, + }; + SemanticEvidence { + tier, + soundness, + coverage, + provenance, + ambiguity, + budget_limited: seed.is_multiple_of(2), + reasons: vec![ + SemanticEvidenceReason::LargeCfg, + SemanticEvidenceReason::AliasAmbiguity, + ], + } + } + + proptest! { + #[test] + fn evidence_combine_is_monotone(left in any::(), right in any::()) { + let left = evidence_from_seed(left); + let right = evidence_from_seed(right); + let combined = left.combined_with(&right); + prop_assert!(combined.strength_rank() <= left.strength_rank()); + prop_assert!(combined.strength_rank() <= right.strength_rank()); + } + + #[test] + fn artifact_normalization_is_idempotent( + residuals in proptest::collection::vec(prop_oneof![ + Just(ResidualReason::MissingArch), + Just(ResidualReason::LargeCfg), + Just(ResidualReason::SummaryBudgetExhausted), + Just(ResidualReason::SccBudgetExhausted), + Just(ResidualReason::InterpreterRequiresStepSummary), + ], 0..8), + ambiguous in proptest::collection::vec(any::(), 0..8), + ) { + let artifact = native_artifact(SemanticArtifactDiagnostics { + branches_evaluated: 0, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: false, + residual_reasons: residuals, + ambiguous_targets: ambiguous, + cache_hit: false, + }); + let normalized = artifact.clone().normalized(); + prop_assert_eq!(normalized.clone().normalized(), normalized); + } + } + + #[test] + fn conflict_downgrade_refuses_narrowing() { + let evidence = SemanticEvidence::exact().downgraded_for_conflict(); + assert!(!evidence.allows_narrowing()); + assert!( + evidence + .reasons + .contains(&SemanticEvidenceReason::ConflictingTargetSources) + ); + } +} diff --git a/crates/r2sym/src/semantics/cache.rs b/crates/r2sym/src/semantics/cache.rs new file mode 100644 index 0000000..078097f --- /dev/null +++ b/crates/r2sym/src/semantics/cache.rs @@ -0,0 +1,446 @@ +use std::collections::HashMap; +use std::fmt::Write as _; +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::sync::{Arc, OnceLock, RwLock}; + +use r2il::ArchSpec; +use r2ssa::{SSAOp, SsaArtifact}; + +use crate::sim::{PreparedFunctionScope, SummaryProfile}; + +use super::artifact::SemanticArtifact; + +const SEMANTIC_CACHE_LIMIT: usize = 128; +pub const SEMANTIC_ARTIFACT_SCHEMA_VERSION: u32 = 2; + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SemanticSeedMode { + Static, + Replay, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum SemanticCacheScopeKind { + Exact, + CoarseLargeSlice, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct SemanticCacheKey { + pub schema_version: u32, + pub root_addr: u64, + pub scope_hash: u64, + pub arch_hash: u64, + pub summary_profile: SummaryProfile, + pub seed_mode: SemanticSeedMode, + pub replay_seed_fingerprint: u64, + pub scope_kind: SemanticCacheScopeKind, +} + +#[derive(Debug, Clone)] +pub struct SemanticCompilationResult { + pub artifact: Arc, + pub cache_hit: bool, + pub seed_mode: SemanticSeedMode, + pub replay_seed_fingerprint: u64, +} + +fn hash_debug_value(value: &T) -> u64 { + let mut hasher = DefaultHasher::new(); + let _ = write!(&mut HasherAdapter(&mut hasher), "{value:?}"); + hasher.finish() +} + +struct HasherAdapter<'a>(&'a mut DefaultHasher); + +impl std::fmt::Write for HasherAdapter<'_> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.0.write(s.as_bytes()); + Ok(()) + } +} + +fn arch_hash(arch: Option<&ArchSpec>) -> u64 { + arch.map(hash_debug_value).unwrap_or(0) +} + +fn strip_ssa_version_suffix(name: &str) -> &str { + name.rsplit_once('_') + .filter(|(_, version)| !version.is_empty() && version.chars().all(|ch| ch.is_ascii_digit())) + .map(|(base, _)| base) + .unwrap_or(name) +} + +fn hash_ssa_var_shape(hasher: &mut H, var: &r2ssa::SSAVar) { + strip_ssa_version_suffix(&var.name).hash(hasher); + var.size.hash(hasher); +} + +fn hash_ssa_op_shape(hasher: &mut H, op: &SSAOp) { + std::mem::discriminant(op).hash(hasher); + if let Some(dst) = op.dst() { + hash_ssa_var_shape(hasher, dst); + } + let sources = op.sources(); + sources.len().hash(hasher); + for src in sources { + hash_ssa_var_shape(hasher, src); + } + + match op { + SSAOp::Subpiece { offset, .. } => offset.hash(hasher), + SSAOp::PtrAdd { element_size, .. } | SSAOp::PtrSub { element_size, .. } => { + element_size.hash(hasher); + } + SSAOp::Load { space, .. } + | SSAOp::Store { space, .. } + | SSAOp::LoadLinked { space, .. } + | SSAOp::StoreConditional { space, .. } + | SSAOp::AtomicCAS { space, .. } + | SSAOp::LoadGuarded { space, .. } + | SSAOp::StoreGuarded { space, .. } => { + space.hash(hasher); + } + _ => {} + } +} + +fn ssa_shape_hash(func: &SsaArtifact) -> u64 { + let mut hasher = DefaultHasher::new(); + for block in func.local_ssa_blocks() { + block.addr.hash(&mut hasher); + block.size.hash(&mut hasher); + block.ops.len().hash(&mut hasher); + for op in &block.ops { + hash_ssa_op_shape(&mut hasher, op); + } + } + hasher.finish() +} + +fn function_hash(func: &SsaArtifact) -> u64 { + let mut hasher = DefaultHasher::new(); + func.entry.hash(&mut hasher); + ssa_shape_hash(func).hash(&mut hasher); + hasher.finish() +} + +pub fn stable_scope_hash(scope: Option<&PreparedFunctionScope>) -> u64 { + let Some(scope) = scope else { + return 0; + }; + let mut hasher = DefaultHasher::new(); + scope.root_id().hash(&mut hasher); + for function in scope.functions().values() { + function.id.hash(&mut hasher); + function.prepared.function().entry.hash(&mut hasher); + ssa_shape_hash(&function.prepared).hash(&mut hasher); + } + hasher.finish() +} + +pub(crate) fn semantic_cache_key( + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + summary_profile: SummaryProfile, + seed_mode: SemanticSeedMode, + replay_seed_fingerprint: u64, +) -> SemanticCacheKey { + SemanticCacheKey { + schema_version: SEMANTIC_ARTIFACT_SCHEMA_VERSION, + root_addr: func.entry, + scope_hash: if scope.is_some() { + stable_scope_hash(scope) + } else { + function_hash(func) + }, + arch_hash: arch_hash(arch), + summary_profile, + seed_mode, + replay_seed_fingerprint, + scope_kind: SemanticCacheScopeKind::Exact, + } +} + +pub(crate) fn coarse_large_slice_cache_key( + root_addr: u64, + scope: &PreparedFunctionScope, + arch: Option<&ArchSpec>, + summary_profile: SummaryProfile, + seed_mode: SemanticSeedMode, + replay_seed_fingerprint: u64, +) -> SemanticCacheKey { + SemanticCacheKey { + schema_version: SEMANTIC_ARTIFACT_SCHEMA_VERSION, + root_addr, + scope_hash: stable_scope_hash(Some(scope)), + arch_hash: arch_hash(arch), + summary_profile, + seed_mode, + replay_seed_fingerprint, + scope_kind: SemanticCacheScopeKind::CoarseLargeSlice, + } +} + +fn semantic_cache() -> &'static RwLock>> { + static CACHE: OnceLock>>> = + OnceLock::new(); + CACHE.get_or_init(|| RwLock::new(HashMap::new())) +} + +pub(crate) fn lookup_semantic_cache(key: &SemanticCacheKey) -> Option> { + semantic_cache() + .read() + .expect("semantic cache read lock poisoned") + .get(key) + .cloned() +} + +pub(crate) fn cache_insert_bounded( + key: SemanticCacheKey, + value: Arc, +) -> Arc { + let mut guard = semantic_cache() + .write() + .expect("semantic cache write lock poisoned"); + if guard.len() >= SEMANTIC_CACHE_LIMIT { + guard.clear(); + } + guard.insert(key, value.clone()); + value +} + +#[cfg(test)] +mod display_name_tests { + use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; + use r2ssa::{InterprocFunctionId, SsaArtifact}; + + use crate::sim::{PreparedFunctionScope, ScopedPreparedFunction}; + + use super::{ + SEMANTIC_ARTIFACT_SCHEMA_VERSION, SemanticCacheScopeKind, SemanticSeedMode, + coarse_large_slice_cache_key, semantic_cache_key, stable_scope_hash, + }; + + const RAX: u64 = 0; + + fn test_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("RAX", RAX, 8)); + arch + } + + fn make_const(value: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: value, + size, + meta: None, + } + } + + #[test] + fn semantic_cache_keys_use_explicit_schema_version() { + let func = SsaArtifact::for_symbolic( + &[R2ILBlock { + addr: 0x401000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }], + Some(&test_arch()), + ) + .expect("ssa"); + let exact_key = semantic_cache_key( + &func, + None, + Some(&test_arch()), + crate::sim::SummaryProfile::Default, + SemanticSeedMode::Static, + 0, + ); + let coarse_key = coarse_large_slice_cache_key( + func.entry, + &PreparedFunctionScope::new( + 0x401000, + vec![ScopedPreparedFunction { + id: InterprocFunctionId(0x401000), + name: Some("sym.main".to_string()), + prepared: func.clone(), + }], + ) + .expect("scope"), + Some(&test_arch()), + crate::sim::SummaryProfile::Default, + SemanticSeedMode::Static, + 0, + ); + assert_eq!(exact_key.schema_version, SEMANTIC_ARTIFACT_SCHEMA_VERSION); + assert_eq!(coarse_key.schema_version, SEMANTIC_ARTIFACT_SCHEMA_VERSION); + assert_eq!(exact_key.replay_seed_fingerprint, 0); + assert!(matches!( + exact_key.scope_kind, + SemanticCacheScopeKind::Exact + )); + assert!(matches!( + coarse_key.scope_kind, + SemanticCacheScopeKind::CoarseLargeSlice + )); + } + + #[test] + fn semantic_cache_keys_partition_replay_seed_identity() { + let func = SsaArtifact::for_symbolic( + &[R2ILBlock { + addr: 0x401000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }], + Some(&test_arch()), + ) + .expect("ssa"); + let static_key = semantic_cache_key( + &func, + None, + Some(&test_arch()), + crate::sim::SummaryProfile::Default, + SemanticSeedMode::Static, + 0, + ); + let replay_key_a = semantic_cache_key( + &func, + None, + Some(&test_arch()), + crate::sim::SummaryProfile::Default, + SemanticSeedMode::Replay, + 0x11, + ); + let replay_key_b = semantic_cache_key( + &func, + None, + Some(&test_arch()), + crate::sim::SummaryProfile::Default, + SemanticSeedMode::Replay, + 0x22, + ); + + assert_eq!(static_key.seed_mode, SemanticSeedMode::Static); + assert_eq!(static_key.replay_seed_fingerprint, 0); + assert_ne!(replay_key_a, replay_key_b); + } + + fn simple_function(entry: u64) -> SsaArtifact { + let blocks = vec![R2ILBlock { + addr: entry, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("ssa") + } + + #[test] + fn stable_scope_hash_ignores_non_semantic_names() { + let root = ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("sym.root".to_string()), + prepared: simple_function(0x1000), + }; + let helper_named = ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("dbg.helper".to_string()), + prepared: simple_function(0x2000), + }; + let helper_renamed = ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("fcn.2000".to_string()), + prepared: simple_function(0x2000), + }; + + let left = + PreparedFunctionScope::new(0x1000, vec![root.clone(), helper_named]).expect("scope"); + let right = PreparedFunctionScope::new(0x1000, vec![root, helper_renamed]).expect("scope"); + + assert_eq!( + stable_scope_hash(Some(&left)), + stable_scope_hash(Some(&right)) + ); + } +} + +#[cfg(test)] +mod tests { + use r2il::{R2ILBlock, R2ILOp, SpaceId, Varnode}; + use r2ssa::{InterprocFunctionId, SsaArtifact}; + + use crate::sim::{PreparedFunctionScope, ScopedPreparedFunction}; + + use super::stable_scope_hash; + + fn make_const(value: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: value, + size, + meta: None, + } + } + + fn make_leaf(entry: u64) -> SsaArtifact { + SsaArtifact::for_symbolic( + &[R2ILBlock { + addr: entry, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }], + None, + ) + .expect("ssa") + } + + fn make_scope(root_name: &str, helper_name: &str) -> PreparedFunctionScope { + PreparedFunctionScope::new( + 0x1000, + vec![ + ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some(root_name.to_string()), + prepared: make_leaf(0x1000).with_name(root_name), + }, + ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some(helper_name.to_string()), + prepared: make_leaf(0x2000).with_name(helper_name), + }, + ], + ) + .expect("scope") + } + + #[test] + fn stable_scope_hash_ignores_display_name_drift() { + let left = make_scope("sym.worker", "sym.helper"); + let right = make_scope("dbg.worker", "fcn.2000"); + assert_eq!( + stable_scope_hash(Some(&left)), + stable_scope_hash(Some(&right)) + ); + } +} diff --git a/crates/r2sym/src/semantics/classify.rs b/crates/r2sym/src/semantics/classify.rs new file mode 100644 index 0000000..9658606 --- /dev/null +++ b/crates/r2sym/src/semantics/classify.rs @@ -0,0 +1,32 @@ +use r2ssa::SsaArtifact; + +use crate::sim::DerivedSummaryDiagnostics; + +use super::artifact::SliceClass; +use super::vm::{InterpreterDispatchSummary, InterpreterKind}; + +pub(super) fn classify_slice( + func: &SsaArtifact, + helper_functions: usize, + derived_diagnostics: &DerivedSummaryDiagnostics, + interpreter: Option<&InterpreterDispatchSummary>, +) -> SliceClass { + if let Some(interpreter) = interpreter { + return match interpreter.kind { + InterpreterKind::SwitchDispatch => SliceClass::InterpreterSwitch, + InterpreterKind::IndirectDispatch => SliceClass::InterpreterIndirect, + }; + } + + let cfg = func.function().cfg_risk_summary(); + if derived_diagnostics.max_scc_size > 1 { + return SliceClass::RecursiveGroup; + } + if cfg.block_count <= 12 && cfg.loop_count == 0 && helper_functions <= 2 { + return SliceClass::Wrapper; + } + if cfg.block_count > 64 || cfg.loop_count > 0 || helper_functions > 3 { + return SliceClass::Worker; + } + SliceClass::GenericLarge +} diff --git a/crates/r2sym/src/semantics/compiler.rs b/crates/r2sym/src/semantics/compiler.rs new file mode 100644 index 0000000..42786ae --- /dev/null +++ b/crates/r2sym/src/semantics/compiler.rs @@ -0,0 +1,1794 @@ +use std::collections::{BTreeSet, HashMap}; +use std::sync::Arc; + +use r2il::ArchSpec; +use r2ssa::{InterprocFunctionId, SsaArtifact}; +use z3::Context; + +use crate::replay::{ReplaySeed, stable_replay_seed_fingerprint}; +use crate::sim::{ + DerivedSummaryDiagnostics, PreparedFunctionScope, SummaryProfile, SummaryRegistry, +}; + +use super::artifact::{ResidualReason, SemanticArtifact, SemanticArtifactBody}; +use super::cache::{ + SemanticCompilationResult, SemanticSeedMode, cache_insert_bounded, + coarse_large_slice_cache_key, lookup_semantic_cache, semantic_cache_key, +}; +use super::classify::classify_slice; +use super::facts::{ + CollectedNativeSemanticRegions, SymbolicFunctionFactDiagnostics, + collect_canonical_semantic_regions_with_derived, + collect_canonical_semantic_regions_with_scope_and_profile, + collect_large_cfg_canonical_semantic_regions_with_limit, +}; +use super::region::{ + ArtifactGranularity, ExecutionModel, NativeArtifactBody, NativeFunctionSummary, + RefinementStage, SemanticArtifactDiagnostics, +}; +use super::vm::{build_vm_step_summary, classify_interpreter_like}; + +fn residual_reasons( + diagnostics: &DerivedSummaryDiagnostics, + fact_diagnostics: &SymbolicFunctionFactDiagnostics, + interpreter_detected: bool, + vm_step_ready: bool, +) -> Vec { + let mut reasons = Vec::new(); + if fact_diagnostics.skipped_missing_arch { + reasons.push(ResidualReason::MissingArch); + } + if fact_diagnostics.skipped_large_cfg { + reasons.push(ResidualReason::LargeCfg); + } + if diagnostics.budget_exhausted > 0 { + reasons.push(ResidualReason::SummaryBudgetExhausted); + } + if diagnostics.scc_budget_exhausted > 0 { + reasons.push(ResidualReason::SccBudgetExhausted); + } + if interpreter_detected && !vm_step_ready { + reasons.push(ResidualReason::InterpreterRequiresStepSummary); + } + reasons +} + +fn normalized_residual_reasons( + suppress_large_cfg_reason: bool, + reasons: Vec, +) -> Vec { + if suppress_large_cfg_reason { + reasons + .into_iter() + .filter(|reason| !matches!(reason, ResidualReason::LargeCfg)) + .collect() + } else { + reasons + } +} + +fn semantic_stage_for( + helper_functions: usize, + derived_summaries: usize, + diagnostics: &DerivedSummaryDiagnostics, + collected: &CollectedNativeSemanticRegions, + interpreter_detected: bool, + vm_step_ready: bool, +) -> RefinementStage { + if vm_step_ready { + return RefinementStage::Compiled; + } + if interpreter_detected { + return RefinementStage::Residual; + } + if derived_summaries > 0 && !collected.diagnostics.skipped_large_cfg { + return RefinementStage::Compiled; + } + if collected.diagnostics.skipped_large_cfg && has_island_compiled_semantics(collected) { + return RefinementStage::Compiled; + } + if helper_functions > 0 + || collected.diagnostics.skipped_large_cfg + || diagnostics.budget_exhausted > 0 + || diagnostics.scc_budget_exhausted > 0 + { + return RefinementStage::Residual; + } + if collected.diagnostics.skipped_missing_arch { + return RefinementStage::Residual; + } + RefinementStage::Raw +} + +fn has_island_compiled_semantics(collected: &CollectedNativeSemanticRegions) -> bool { + collected + .regions + .values() + .any(|region| region.supports_guarded_structuring()) +} + +fn semantic_granularity_for( + stage: RefinementStage, + execution: ExecutionModel, + regions: &std::collections::BTreeMap, + has_island_compiled_regions: bool, +) -> ArtifactGranularity { + if matches!(execution, ExecutionModel::Vm) { + return ArtifactGranularity::SummaryOnly; + } + if has_island_compiled_regions { + return ArtifactGranularity::Regioned; + } + if matches!(stage, RefinementStage::Residual) && !regions.is_empty() { + ArtifactGranularity::Regioned + } else { + ArtifactGranularity::WholeFunction + } +} + +struct BuildSemanticArtifactInput { + stage: RefinementStage, + granularity: ArtifactGranularity, + execution: ExecutionModel, + suppress_large_cfg_reason: bool, + slice_class: crate::SliceClass, + closure_functions: usize, + helper_functions: usize, + derived_summaries: usize, + derived_diagnostics: DerivedSummaryDiagnostics, + collected: CollectedNativeSemanticRegions, + interpreter: Option, + vm_step: Option, + vm_transfer: Option, +} + +fn build_semantic_artifact(input: BuildSemanticArtifactInput) -> SemanticArtifact { + let BuildSemanticArtifactInput { + stage, + granularity, + execution, + suppress_large_cfg_reason, + slice_class, + closure_functions, + helper_functions, + derived_summaries, + derived_diagnostics, + collected, + interpreter, + vm_step, + vm_transfer, + } = input; + let interpreter_detected = interpreter.is_some(); + let vm_step_ready = vm_step.is_some(); + let body = match execution { + ExecutionModel::Vm => SemanticArtifactBody::Vm(Box::new(super::region::VmArtifactBody { + interpreter, + step_summary: vm_step, + transfer_summary: vm_transfer, + })), + ExecutionModel::Native => SemanticArtifactBody::Native(NativeArtifactBody { + summary: NativeFunctionSummary { + slice_class, + closure_functions, + helper_functions, + derived_summaries, + derived_diagnostics: derived_diagnostics.clone(), + }, + regions: collected.regions, + }), + }; + let ambiguous_targets = match &body { + SemanticArtifactBody::Native(body) => body.conflicting_targets(false).into_iter().collect(), + SemanticArtifactBody::Vm(_) => Vec::new(), + }; + SemanticArtifact { + stage, + granularity, + execution, + body, + diagnostics: SemanticArtifactDiagnostics { + branches_evaluated: collected.diagnostics.branches_evaluated, + branches_pruned: collected.diagnostics.branches_pruned, + branches_unknown: collected.diagnostics.branches_unknown, + skipped_missing_arch: collected.diagnostics.skipped_missing_arch, + skipped_large_cfg: collected.diagnostics.skipped_large_cfg, + residual_reasons: normalized_residual_reasons( + suppress_large_cfg_reason, + residual_reasons( + &derived_diagnostics, + &collected.diagnostics, + interpreter_detected, + vm_step_ready, + ), + ), + ambiguous_targets, + cache_hit: false, + }, + } + .normalized() +} + +fn bounded_large_cfg_branch_limit(func: &SsaArtifact) -> usize { + let summary = func.function().cfg_risk_summary(); + if summary.block_count > 160 || summary.back_edge_count > 8 || summary.switch_block_count > 8 { + 2 + } else if summary.block_count > 96 || summary.back_edge_count > 6 { + 3 + } else { + 4 + } +} + +fn bounded_large_cfg_helper_limit(func: &SsaArtifact) -> usize { + let summary = func.function().cfg_risk_summary(); + if summary.block_count > 160 || summary.back_edge_count > 8 || summary.switch_block_count > 8 { + 1 + } else if summary.block_count > 96 || summary.back_edge_count > 6 { + 2 + } else { + 3 + } +} + +fn bounded_large_cfg_scope( + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, +) -> Option { + let scope = scope?; + let root = scope.root()?.clone(); + let helper_limit = bounded_large_cfg_helper_limit(func); + if helper_limit == 0 { + return PreparedFunctionScope::new(scope.root_id().0, vec![root]); + } + + let mut functions = vec![root]; + let mut seen = BTreeSet::from([scope.root_id()]); + + for call in func.call_sites().by_id.values() { + if functions.len().saturating_sub(1) >= helper_limit { + break; + } + let Some(target) = call.direct_target else { + continue; + }; + let function_id = InterprocFunctionId(target); + if !seen.insert(function_id) { + continue; + } + let Some(helper) = scope.functions().get(&function_id).cloned() else { + continue; + }; + functions.push(helper); + } + + PreparedFunctionScope::new(scope.root_id().0, functions) +} + +fn bounded_query_scope( + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + target_addr: u64, +) -> Option { + let scope = scope?; + let target_is_cross_function = scope + .function_containing_block(target_addr) + .is_some_and(|function| function.id != scope.root_id()); + let helper_limit = bounded_large_cfg_helper_limit(func); + let has_many_helpers = scope.helper_functions().count() > helper_limit; + + if !(target_is_cross_function || has_many_helpers) { + return None; + } + + bounded_large_cfg_scope(func, Some(scope)) +} + +fn compile_function_semantics_uncached( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, +) -> SemanticArtifact { + let closure_functions = scope.map(|scope| scope.functions().len()).unwrap_or(1); + let helper_functions = scope + .map(|scope| scope.helper_functions().count()) + .unwrap_or(0); + let mut derived_diagnostics = DerivedSummaryDiagnostics::default(); + let mut derived_summaries = 0usize; + let mut collected = CollectedNativeSemanticRegions::default(); + let interpreter = classify_interpreter_like(func); + let vm_step = interpreter + .as_ref() + .and_then(|dispatch| build_vm_step_summary(func, dispatch)); + let vm_transfer = vm_step.clone(); + + if let Some(arch) = arch { + let cfg_summary = func.function().cfg_risk_summary(); + let skip_expensive_branch_compilation = cfg_summary.block_count > 48 + || cfg_summary.back_edge_count > 4 + || cfg_summary.switch_block_count > 4; + + if skip_expensive_branch_compilation { + if interpreter.is_none() { + let bounded_scope = bounded_large_cfg_scope(func, scope); + collected = collect_large_cfg_canonical_semantic_regions_with_limit( + ctx, + func, + bounded_scope.as_ref(), + arch, + symbol_map, + summary_profile, + bounded_large_cfg_branch_limit(func), + ); + } else { + collected.diagnostics.skipped_large_cfg = true; + } + + let execution = if interpreter.is_some() || vm_step.is_some() || vm_transfer.is_some() { + ExecutionModel::Vm + } else { + ExecutionModel::Native + }; + let stage = semantic_stage_for( + helper_functions, + derived_summaries, + &derived_diagnostics, + &collected, + interpreter.is_some(), + vm_transfer.is_some(), + ); + let has_island_compiled_regions = matches!(execution, ExecutionModel::Native) + && collected.diagnostics.skipped_large_cfg + && has_island_compiled_semantics(&collected); + let granularity = semantic_granularity_for( + stage, + execution, + &collected.regions, + has_island_compiled_regions, + ); + let slice_class = classify_slice( + func, + helper_functions, + &derived_diagnostics, + interpreter.as_ref(), + ); + return build_semantic_artifact(BuildSemanticArtifactInput { + stage, + granularity, + execution, + suppress_large_cfg_reason: matches!(execution, ExecutionModel::Vm) + || has_island_compiled_regions, + slice_class, + closure_functions, + helper_functions, + derived_summaries, + derived_diagnostics: derived_diagnostics.clone(), + collected, + interpreter: interpreter.clone(), + vm_step: vm_step.clone(), + vm_transfer: vm_transfer.clone(), + }); + } + + if let Some(scope) = scope + && let Some(registry) = SummaryRegistry::with_profile_for_arch_and_symbols( + arch, + symbol_map, + summary_profile, + ) + { + let derived = registry.derive_symbolic_summaries(ctx, scope, Some(arch), symbol_map); + derived_summaries = derived.summaries.len(); + derived_diagnostics = derived.diagnostics.clone(); + collected = collect_canonical_semantic_regions_with_derived( + ctx, + func, + Some(scope), + arch, + symbol_map, + summary_profile, + ®istry, + &derived, + ); + } else { + collected = collect_canonical_semantic_regions_with_scope_and_profile( + ctx, + func, + None, + Some(arch), + symbol_map, + summary_profile, + ); + } + } else { + collected.diagnostics.skipped_missing_arch = true; + } + let execution = if interpreter.is_some() || vm_step.is_some() || vm_transfer.is_some() { + ExecutionModel::Vm + } else { + ExecutionModel::Native + }; + let stage = semantic_stage_for( + helper_functions, + derived_summaries, + &derived_diagnostics, + &collected, + interpreter.is_some(), + vm_transfer.is_some(), + ); + let has_island_compiled_regions = matches!(execution, ExecutionModel::Native) + && collected.diagnostics.skipped_large_cfg + && has_island_compiled_semantics(&collected); + let granularity = semantic_granularity_for( + stage, + execution, + &collected.regions, + has_island_compiled_regions, + ); + let slice_class = classify_slice( + func, + helper_functions, + &derived_diagnostics, + interpreter.as_ref(), + ); + build_semantic_artifact(BuildSemanticArtifactInput { + stage, + granularity, + execution, + suppress_large_cfg_reason: matches!(execution, ExecutionModel::Vm) + || has_island_compiled_regions, + slice_class, + closure_functions, + helper_functions, + derived_summaries, + derived_diagnostics: derived_diagnostics.clone(), + collected, + interpreter, + vm_step, + vm_transfer, + }) +} + +fn semantic_seed_identity(replay_seed: Option<&ReplaySeed>) -> (SemanticSeedMode, u64) { + match replay_seed { + Some(seed) => ( + SemanticSeedMode::Replay, + stable_replay_seed_fingerprint(seed), + ), + None => (SemanticSeedMode::Static, 0), + } +} + +fn compile_function_semantics_cached_with_scope_impl( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, + replay_seed: Option<&ReplaySeed>, +) -> SemanticCompilationResult { + let (seed_mode, replay_seed_fingerprint) = semantic_seed_identity(replay_seed); + let key = semantic_cache_key( + func, + scope, + arch, + summary_profile, + seed_mode, + replay_seed_fingerprint, + ); + if let Some(existing) = lookup_semantic_cache(&key) { + return SemanticCompilationResult { + artifact: existing, + cache_hit: true, + seed_mode, + replay_seed_fingerprint, + }; + } + let coarse_key = scope.map(|scope| { + coarse_large_slice_cache_key( + func.entry, + scope, + arch, + summary_profile, + seed_mode, + replay_seed_fingerprint, + ) + }); + if let Some(coarse_key) = coarse_key.as_ref() + && let Some(existing) = lookup_semantic_cache(coarse_key) + { + return SemanticCompilationResult { + artifact: existing, + cache_hit: true, + seed_mode, + replay_seed_fingerprint, + }; + } + + let artifact = Arc::new(compile_function_semantics_uncached( + ctx, + func, + scope, + arch, + symbol_map, + summary_profile, + )); + let artifact = cache_insert_bounded(key, artifact); + let should_cache_coarsely = scope.is_some() + && (matches!(artifact.execution, ExecutionModel::Vm) + || artifact.diagnostics.skipped_large_cfg); + let artifact = if should_cache_coarsely { + let coarse_key = + coarse_key.expect("coarse large-slice cache key should exist when scope is present"); + cache_insert_bounded(coarse_key, artifact) + } else { + artifact + }; + SemanticCompilationResult { + artifact, + cache_hit: false, + seed_mode, + replay_seed_fingerprint, + } +} + +pub(crate) fn compile_function_semantics_cached_with_scope( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, +) -> SemanticCompilationResult { + compile_function_semantics_cached_with_scope_impl( + ctx, + func, + scope, + arch, + symbol_map, + summary_profile, + None, + ) +} + +pub fn compile_function_semantics_with_scope( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, +) -> SemanticArtifact { + let result = compile_function_semantics_cached_with_scope( + ctx, + func, + scope, + arch, + symbol_map, + summary_profile, + ); + let mut artifact = (*result.artifact).clone(); + artifact.diagnostics.cache_hit = result.cache_hit; + artifact +} + +pub fn compile_function_semantics_with_scope_and_replay_seed( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, + replay_seed: Option<&ReplaySeed>, +) -> SemanticArtifact { + let result = compile_function_semantics_cached_with_scope_impl( + ctx, + func, + scope, + arch, + symbol_map, + summary_profile, + replay_seed, + ); + let mut artifact = (*result.artifact).clone(); + artifact.diagnostics.cache_hit = result.cache_hit; + artifact +} + +pub fn compile_semantic_artifact_with_scope( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, +) -> SemanticArtifact { + compile_function_semantics_with_scope(ctx, func, scope, arch, symbol_map, summary_profile) +} + +pub fn compile_semantic_artifact_with_scope_and_replay_seed( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, + replay_seed: Option<&ReplaySeed>, +) -> SemanticArtifact { + compile_function_semantics_with_scope_and_replay_seed( + ctx, + func, + scope, + arch, + symbol_map, + summary_profile, + replay_seed, + ) +} + +pub fn compile_query_semantic_artifact_with_scope( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + target_addr: u64, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, +) -> SemanticArtifact { + let bounded_scope = bounded_query_scope(func, scope, target_addr); + compile_function_semantics_with_scope( + ctx, + func, + bounded_scope.as_ref().or(scope), + arch, + symbol_map, + summary_profile, + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn compile_query_semantic_artifact_with_scope_and_replay_seed( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + target_addr: u64, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, + replay_seed: Option<&ReplaySeed>, +) -> SemanticArtifact { + let bounded_scope = bounded_query_scope(func, scope, target_addr); + compile_function_semantics_with_scope_and_replay_seed( + ctx, + func, + bounded_scope.as_ref().or(scope), + arch, + symbol_map, + summary_profile, + replay_seed, + ) +} + +pub fn compile_semantic_artifact_default_with_scope( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, +) -> SemanticArtifact { + let rebound_scope = scope.and_then(|scope| scope.with_prepared_root(func)); + compile_semantic_artifact_with_scope( + ctx, + func, + rebound_scope.as_ref(), + arch, + &HashMap::new(), + SummaryProfile::Default, + ) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use r2il::{ + ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, SwitchCase, SwitchInfo, Varnode, + }; + use r2ssa::SsaArtifact; + use z3::Context; + + use crate::replay::{ReplayRegisterValue, ReplaySeed}; + + use super::*; + const RAX: u64 = 0; + + fn test_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("RAX", RAX, 8)); + arch + } + + fn make_reg(offset: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Register, + offset, + size, + meta: None, + } + } + + fn make_const(value: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: value, + size, + meta: None, + } + } + + #[test] + fn compile_semantics_cache_hits_on_repeat() { + let blocks = vec![R2ILBlock { + addr: 0x1000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("ssa"); + let ctx = Context::thread_local(); + let first = compile_function_semantics_cached_with_scope( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + let second = compile_function_semantics_cached_with_scope( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + assert!(!first.cache_hit); + assert!(second.cache_hit); + } + + #[test] + fn compile_semantics_replay_seeds_partition_cache_identity() { + let blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("ssa"); + let ctx = Context::thread_local(); + let replay_a = ReplaySeed { + checkpoint_id: Some(1), + entry_pc: Some(0x2000), + registers: vec![ReplayRegisterValue { + name: "rax".to_string(), + value: 0x1111, + }], + ..ReplaySeed::default() + }; + let replay_b = ReplaySeed { + checkpoint_id: Some(2), + entry_pc: Some(0x2000), + registers: vec![ReplayRegisterValue { + name: "rax".to_string(), + value: 0x2222, + }], + ..ReplaySeed::default() + }; + + let first = compile_function_semantics_cached_with_scope_impl( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + Some(&replay_a), + ); + let second = compile_function_semantics_cached_with_scope_impl( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + Some(&replay_a), + ); + let third = compile_function_semantics_cached_with_scope_impl( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + Some(&replay_b), + ); + + assert_eq!(first.seed_mode, SemanticSeedMode::Replay); + assert_eq!(second.seed_mode, SemanticSeedMode::Replay); + assert_eq!(third.seed_mode, SemanticSeedMode::Replay); + assert_eq!( + first.replay_seed_fingerprint, + second.replay_seed_fingerprint + ); + assert_ne!(first.replay_seed_fingerprint, third.replay_seed_fingerprint); + assert!(!first.cache_hit); + assert!(second.cache_hit); + assert!(!third.cache_hit); + } + + #[test] + fn compile_semantics_cache_ignores_scope_display_names() { + let blocks = vec![R2ILBlock { + addr: 0x1000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("ssa"); + let scope_a = crate::PreparedFunctionScope::new( + 0x1000, + vec![crate::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("sym.display_a".to_string()), + prepared: func.clone(), + }], + ) + .expect("scope a"); + let scope_b = crate::PreparedFunctionScope::new( + 0x1000, + vec![crate::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("dbg.display_b".to_string()), + prepared: func.clone(), + }], + ) + .expect("scope b"); + let ctx = Context::thread_local(); + + let first = compile_function_semantics_cached_with_scope( + &ctx, + &func, + Some(&scope_a), + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + let second = compile_function_semantics_cached_with_scope( + &ctx, + &func, + Some(&scope_b), + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + + assert!(!first.cache_hit); + assert!(second.cache_hit); + } + + #[test] + fn large_cfg_cache_does_not_alias_distinct_scopes() { + let mut blocks = vec![R2ILBlock { + addr: 0x3000, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let mut addr = 0x3004; + for _ in 0..64 { + blocks.push(R2ILBlock { + addr, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(addr + 4, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + addr += 4; + } + blocks.push(R2ILBlock { + addr, + size: 4, + ops: vec![R2ILOp::Return { + target: make_reg(RAX, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + let root = SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("root"); + let helper = SsaArtifact::for_symbolic( + &[R2ILBlock { + addr: 0x5000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }], + Some(&test_arch()), + ) + .expect("helper"); + let scope_a = crate::PreparedFunctionScope::new( + 0x3000, + vec![crate::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x3000), + name: Some("sym.root".to_string()), + prepared: root.clone(), + }], + ) + .expect("scope a"); + let scope_b = crate::PreparedFunctionScope::new( + 0x3000, + vec![ + crate::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x3000), + name: Some("sym.root".to_string()), + prepared: root.clone(), + }, + crate::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x5000), + name: Some("sym.helper".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope b"); + let ctx = Context::thread_local(); + + let first = compile_function_semantics_cached_with_scope( + &ctx, + &root, + Some(&scope_a), + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + let second = compile_function_semantics_cached_with_scope( + &ctx, + &root, + Some(&scope_b), + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + + assert!(first.artifact.diagnostics.skipped_large_cfg); + assert!(second.artifact.diagnostics.skipped_large_cfg); + assert!(!first.cache_hit); + assert!(!second.cache_hit); + } + + #[test] + fn bounded_query_scope_omits_cross_function_target_helpers() { + let root = SsaArtifact::for_symbolic( + &[R2ILBlock { + addr: 0x1000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }], + Some(&test_arch()), + ) + .expect("root"); + let helper = SsaArtifact::for_symbolic( + &[R2ILBlock { + addr: 0x2000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }], + Some(&test_arch()), + ) + .expect("helper"); + let scope = crate::PreparedFunctionScope::new( + 0x1000, + vec![ + crate::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + crate::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let bounded = bounded_query_scope(&root, Some(&scope), 0x2000).expect("bounded scope"); + assert_eq!(bounded.functions().len(), 1); + assert_eq!(bounded.root_id(), r2ssa::InterprocFunctionId(0x1000)); + assert!( + bounded + .functions() + .contains_key(&r2ssa::InterprocFunctionId(0x1000)) + ); + assert!( + !bounded + .functions() + .contains_key(&r2ssa::InterprocFunctionId(0x2000)) + ); + } + + #[test] + fn bounded_query_scope_keeps_same_function_targets_unbounded() { + let root = SsaArtifact::for_symbolic( + &[R2ILBlock { + addr: 0x1000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }], + Some(&test_arch()), + ) + .expect("root"); + let scope = crate::PreparedFunctionScope::new( + 0x1000, + vec![crate::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }], + ) + .expect("scope"); + + assert!(bounded_query_scope(&root, Some(&scope), 0x1000).is_none()); + } + + #[test] + fn interpreter_classifier_marks_switch_loop_vm_summary() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: Some(SwitchInfo { + switch_addr: 0x1004, + min_val: 0, + max_val: 4, + default_target: Some(0x1018), + cases: vec![ + SwitchCase { + value: 0, + target: 0x1008, + }, + SwitchCase { + value: 1, + target: 0x100c, + }, + SwitchCase { + value: 2, + target: 0x1010, + }, + SwitchCase { + value: 3, + target: 0x1014, + }, + ], + }), + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1008, + size: 4, + ops: vec![ + R2ILOp::IntAdd { + dst: make_reg(RAX, 8), + a: make_reg(RAX, 8), + b: make_const(1, 8), + }, + R2ILOp::Branch { + target: make_const(0x1004, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x100c, + size: 4, + ops: vec![ + R2ILOp::IntSub { + dst: make_reg(RAX, 8), + a: make_reg(RAX, 8), + b: make_const(1, 8), + }, + R2ILOp::Branch { + target: make_const(0x1004, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(RAX, 8), + a: make_reg(RAX, 8), + b: make_const(0x55, 8), + }, + R2ILOp::Branch { + target: make_const(0x1004, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1014, + size: 4, + ops: vec![ + R2ILOp::IntLeft { + dst: make_reg(RAX, 8), + a: make_reg(RAX, 8), + b: make_const(1, 8), + }, + R2ILOp::Branch { + target: make_const(0x1004, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1018, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("ssa"); + let ctx = Context::thread_local(); + let artifact = compile_function_semantics_with_scope( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + assert_eq!(artifact.execution, ExecutionModel::Vm); + assert_eq!( + artifact.stage, + super::super::region::RefinementStage::Compiled + ); + assert_eq!( + artifact.granularity, + super::super::region::ArtifactGranularity::SummaryOnly + ); + assert!(matches!(artifact.query_plan(), crate::QueryPlan::Ready)); + let vm_body = artifact.vm_body().expect("vm artifact body"); + assert!(vm_body.interpreter.is_some()); + let vm_step = vm_body.step_summary.as_ref().expect("vm step"); + assert_eq!(vm_step.loop_header, 0x1004); + assert_eq!(vm_step.default_target, Some(0x1018)); + assert_eq!(vm_step.case_values_by_target.get(&0x1008), Some(&vec![0])); + assert!(!vm_step.handler_state_updates.is_empty()); + assert!(!vm_step.transfers.is_empty()); + assert!( + vm_step + .handler_state_updates + .values() + .flat_map(|updates| updates.iter()) + .any(|update| update.output.starts_with("RAX_")) + ); + assert!( + vm_step + .transfers + .iter() + .any(|transfer| !transfer.case_values.is_empty()) + ); + } + + #[test] + fn interpreter_without_step_summary_stays_residual() { + let blocks = vec![ + R2ILBlock { + addr: 0x2000, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x2004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x2004, + size: 4, + ops: vec![], + switch_info: Some(SwitchInfo { + switch_addr: 0x2004, + min_val: 0, + max_val: 4, + default_target: Some(0x2018), + cases: vec![ + SwitchCase { + value: 0, + target: 0x2008, + }, + SwitchCase { + value: 1, + target: 0x200c, + }, + SwitchCase { + value: 2, + target: 0x2010, + }, + SwitchCase { + value: 3, + target: 0x2014, + }, + ], + }), + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x2008, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x2004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x200c, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x2004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x2010, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x2004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x2014, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x2004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x2018, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x2004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("ssa"); + let ctx = Context::thread_local(); + let artifact = compile_function_semantics_with_scope( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + assert_eq!(artifact.execution, ExecutionModel::Vm); + assert_eq!( + artifact.stage, + super::super::region::RefinementStage::Residual + ); + let vm_body = artifact.vm_body().expect("vm artifact body"); + assert!(vm_body.interpreter.is_some()); + assert!(vm_body.step_summary.is_none()); + assert!( + artifact + .diagnostics + .residual_reasons + .contains(&ResidualReason::InterpreterRequiresStepSummary) + ); + } + + #[test] + fn vm_step_summary_tracks_case_values_and_handler_regions() { + let blocks = vec![ + R2ILBlock { + addr: 0x3000, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x3004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x3004, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: Some(SwitchInfo { + switch_addr: 0x3004, + min_val: 0, + max_val: 4, + default_target: Some(0x3018), + cases: vec![ + SwitchCase { + value: 0, + target: 0x3008, + }, + SwitchCase { + value: 1, + target: 0x300c, + }, + SwitchCase { + value: 2, + target: 0x3010, + }, + SwitchCase { + value: 3, + target: 0x3014, + }, + ], + }), + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x3008, + size: 4, + ops: vec![ + R2ILOp::Load { + dst: make_reg(RAX, 8), + space: SpaceId::Ram, + addr: make_reg(RAX, 8), + }, + R2ILOp::Branch { + target: make_const(0x3004, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x300c, + size: 4, + ops: vec![R2ILOp::Return { + target: make_reg(RAX, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x3010, + size: 4, + ops: vec![ + R2ILOp::IntAdd { + dst: make_reg(RAX, 8), + a: make_reg(RAX, 8), + b: make_const(1, 8), + }, + R2ILOp::Branch { + target: make_const(0x3004, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x3014, + size: 4, + ops: vec![ + R2ILOp::IntSub { + dst: make_reg(RAX, 8), + a: make_reg(RAX, 8), + b: make_const(1, 8), + }, + R2ILOp::Branch { + target: make_const(0x3004, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x3018, + size: 4, + ops: vec![R2ILOp::CBranch { + target: make_const(0x3004, 8), + cond: make_reg(RAX, 1), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x301c, + size: 4, + ops: vec![R2ILOp::Return { + target: make_reg(RAX, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("ssa"); + let ctx = Context::thread_local(); + let artifact = compile_function_semantics_with_scope( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + + let vm_step = artifact + .vm_body() + .and_then(|body| body.step_summary.as_ref()) + .expect("vm step summary"); + assert_eq!(artifact.execution, ExecutionModel::Vm); + assert_eq!( + artifact.stage, + super::super::region::RefinementStage::Compiled + ); + assert!(matches!(artifact.query_plan(), crate::QueryPlan::Ready)); + assert_eq!(vm_step.loop_header, 0x3004); + assert_eq!(vm_step.dispatch_header, 0x3004); + assert_eq!(vm_step.default_target, Some(0x3018)); + assert_eq!(vm_step.case_values_by_target.get(&0x3008), Some(&vec![0])); + assert_eq!(vm_step.case_values_by_target.get(&0x300c), Some(&vec![1])); + assert_eq!(vm_step.case_values_by_target.get(&0x3010), Some(&vec![2])); + assert_eq!(vm_step.case_values_by_target.get(&0x3014), Some(&vec![3])); + assert_eq!(vm_step.handler_regions.get(&0x3008), Some(&vec![0x3008])); + assert_eq!(vm_step.handler_memory_reads.get(&0x3008), Some(&1)); + assert_eq!(vm_step.handler_memory_writes.get(&0x3008), Some(&0)); + assert!( + vm_step + .handler_memory_read_effects + .get(&0x3008) + .is_some_and(|effects| !effects.is_empty()) + ); + assert!( + vm_step + .handler_state_updates + .get(&0x3008) + .is_some_and(|updates| !updates.is_empty()) + ); + assert!( + vm_step + .handler_state_updates + .get(&0x3008) + .is_some_and(|updates| updates.iter().any(|update| { + update.exact + && matches!(update.value, crate::semantics::vm::VmValueExpr::Var(_)) + })) + ); + assert!(vm_step.redispatch_handlers.contains(&0x3008)); + assert!(vm_step.redispatch_handlers.contains(&0x3010)); + assert!(vm_step.redispatch_handlers.contains(&0x3014)); + assert!(vm_step.redispatch_handlers.contains(&0x3018)); + assert!(vm_step.returning_handlers.contains(&0x300c)); + assert!(vm_step.returning_handlers.contains(&0x3018)); + assert_eq!(vm_step.handler_conditional_branches.get(&0x3018), Some(&1)); + assert_eq!( + vm_step.handler_regions.get(&0x3018), + Some(&vec![0x3018, 0x301c]) + ); + assert_eq!( + vm_step.handler_exit_targets.get(&0x3018), + Some(&vec![0x3004]) + ); + assert!( + vm_step + .handler_exit_guards + .get(&0x3018) + .is_some_and(|guards| !guards.is_empty()) + ); + assert!(vm_step.truncated_handlers.is_empty()); + assert_eq!(vm_step.transfers.len(), 5); + assert!( + vm_step + .transfers + .iter() + .any(|transfer| transfer.handler_target == 0x3008 && transfer.redispatch) + ); + assert!(vm_step.transfers.iter().any(|transfer| { + transfer.handler_target == 0x3008 + && transfer.state_updates.iter().any(|update| { + update.exact + && matches!(update.value, crate::semantics::vm::VmValueExpr::Var(_)) + }) + })); + assert!( + vm_step + .transfers + .iter() + .any(|transfer| !transfer.state_updates.is_empty()) + ); + assert!( + vm_step + .transfers + .iter() + .any(|transfer| !transfer.exit_guards.is_empty()) + ); + assert!( + vm_step + .transfers + .iter() + .any(|transfer| !transfer.memory_reads.is_empty()) + ); + } + + #[test] + fn large_cfg_shortcuts_to_bounded_residual_artifact() { + let mut blocks = vec![ + R2ILBlock { + addr: 0x4000, + size: 4, + ops: vec![R2ILOp::CBranch { + target: make_const(0x4008, 8), + cond: make_reg(RAX, 1), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x4004, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x400c, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x4008, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x400c, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + + let mut addr = 0x400c; + for _ in 0..64 { + blocks.push(R2ILBlock { + addr, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(addr + 4, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + addr += 4; + } + blocks.push(R2ILBlock { + addr, + size: 4, + ops: vec![R2ILOp::Return { + target: make_reg(RAX, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + + let func = SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("ssa"); + let ctx = Context::thread_local(); + let artifact = compile_function_semantics_with_scope( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + + let native = artifact.native_body().expect("native body"); + assert_eq!( + artifact.stage, + super::super::region::RefinementStage::Residual + ); + assert!(artifact.diagnostics.skipped_large_cfg); + assert_eq!(native.regions.len(), 1); + assert_eq!(native.actionable_control_count(), 2); + assert!(matches!( + artifact.type_plan(), + crate::TypePlan::NativeAugmentation + )); + assert!(matches!( + artifact.decompile_plan(), + crate::DecompilePlan::NativeStructured | crate::DecompilePlan::NativeLinear { .. } + )); + assert_eq!(artifact.diagnostics.branches_evaluated, 1); + } + + #[test] + fn large_cfg_exact_branch_frontier_with_memory_support_is_decompile_ready() { + let compiled = crate::backward::BackwardConditionSummary { + simplified: "flag == 0".to_string(), + terms: vec!["flag == 0".to_string()], + memory_terms: vec![crate::backward::BackwardMemoryCondition { + region: crate::backward::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: crate::SemanticEvidence::exact(), + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x0:8".to_string()), + exact_value: true, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: crate::backward::BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }; + let collected = CollectedNativeSemanticRegions { + regions: std::iter::once({ + let region = crate::SemanticRegion { + anchor: 0x401000, + frontier: std::collections::BTreeSet::from([0x401010, 0x401020]), + control: vec![ + crate::Judged::new( + crate::ControlFact { + target: 0x401010, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("flag == 0".to_string()), + compiled: Some(compiled.clone()), + }, + crate::SemanticEvidence::exact(), + ), + crate::Judged::new( + crate::ControlFact { + target: 0x401020, + status: crate::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + condition: Some("flag != 0".to_string()), + compiled: None, + }, + crate::SemanticEvidence::exact(), + ), + ], + memory: vec![crate::Judged::new( + crate::MemoryFact { + term: compiled.memory_terms[0].clone(), + }, + crate::SemanticEvidence::exact(), + )], + pre: Vec::new(), + post: Vec::new(), + targets: vec![ + crate::Judged::new( + crate::TargetFact { + target: 0x401010, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + }, + crate::SemanticEvidence::exact(), + ), + crate::Judged::new( + crate::TargetFact { + target: 0x401020, + status: crate::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + }, + crate::SemanticEvidence::exact(), + ), + ], + }; + (region.key(), region) + }) + .collect(), + diagnostics: SymbolicFunctionFactDiagnostics { + skipped_large_cfg: true, + ..Default::default() + }, + }; + + let stage = semantic_stage_for( + 0, + 0, + &DerivedSummaryDiagnostics::default(), + &collected, + false, + false, + ); + let artifact = build_semantic_artifact(BuildSemanticArtifactInput { + stage, + granularity: ArtifactGranularity::Regioned, + execution: ExecutionModel::Native, + suppress_large_cfg_reason: true, + slice_class: crate::SliceClass::Worker, + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: DerivedSummaryDiagnostics::default(), + collected, + interpreter: None, + vm_step: None, + vm_transfer: None, + }); + assert!(matches!(artifact.query_plan(), crate::QueryPlan::Ready)); + assert!(matches!( + artifact.type_plan(), + crate::TypePlan::NativeAugmentation + )); + assert!(matches!( + artifact.decompile_plan(), + crate::DecompilePlan::NativeStructured + )); + let reasons = artifact.diagnostics.residual_reasons; + assert!( + !reasons.contains(&ResidualReason::LargeCfg), + "island-compiled workers should keep large_cfg as provenance, not a residual reason" + ); + } +} diff --git a/crates/r2sym/src/semantics/facts.rs b/crates/r2sym/src/semantics/facts.rs new file mode 100644 index 0000000..c8b9f7a --- /dev/null +++ b/crates/r2sym/src/semantics/facts.rs @@ -0,0 +1,954 @@ +use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque}; + +use r2il::ArchSpec; +use r2ssa::{InterprocFunctionId, SsaArtifact}; +use serde::{Deserialize, Serialize}; +use z3::Context; + +use crate::SymState; +use crate::backward::{ + BackwardConditionPrecision, BackwardConditionSummary, BackwardMemoryCondition, + compile_branch_precondition_with_summaries, +}; +use crate::path::{ExploreConfig, PathExplorer}; +use crate::runtime::seed_default_state_for_arch; +use crate::semantics::{ + SemanticEvidence, SemanticEvidenceCoverage, SemanticEvidenceProvenance, SemanticEvidenceReason, +}; +use crate::sim::{ + DerivedSummaryCompletion, DerivedSummarySet, PreparedFunctionScope, SummaryProfile, + SummaryRegistry, +}; +use crate::solver::SatResult; + +use super::region::{ControlFact, Judged, MemoryFact, RegionKey, SemanticRegion, TargetFact}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum SymbolicReachabilityStatus { + Reachable, + Unreachable, + Unknown, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct SymbolicFunctionFactDiagnostics { + pub branches_evaluated: usize, + pub branches_pruned: usize, + pub branches_unknown: usize, + pub skipped_missing_arch: bool, + pub skipped_large_cfg: bool, +} + +#[derive(Debug, Clone, Default)] +pub(super) struct CollectedNativeSemanticRegions { + pub regions: BTreeMap, + pub diagnostics: SymbolicFunctionFactDiagnostics, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct BranchObservation { + block_addr: u64, + true_target: u64, + false_target: u64, + true_status: SymbolicReachabilityStatus, + false_status: SymbolicReachabilityStatus, + true_condition: Option, + false_condition: Option, + true_compiled: Option, + false_compiled: Option, +} + +fn target_status_evidence(status: SymbolicReachabilityStatus) -> SemanticEvidence { + match status { + SymbolicReachabilityStatus::Reachable | SymbolicReachabilityStatus::Unreachable => { + SemanticEvidence::exact() + } + SymbolicReachabilityStatus::Unknown => { + SemanticEvidence::residual(SemanticEvidenceReason::ResidualSearchRequired) + } + } +} + +fn push_unique_judged(dst: &mut Vec>, value: Judged) { + if !dst.contains(&value) { + dst.push(value); + } +} + +fn ensure_semantic_region( + by_anchor: &mut BTreeMap, + anchor: u64, +) -> &mut SemanticRegion { + by_anchor.entry(anchor).or_insert_with(|| SemanticRegion { + anchor, + frontier: BTreeSet::new(), + control: Vec::new(), + memory: Vec::new(), + pre: Vec::new(), + post: Vec::new(), + targets: Vec::new(), + }) +} + +fn push_region_memory_terms( + region: &mut SemanticRegion, + terms: impl IntoIterator, +) { + for term in terms { + push_unique_judged( + &mut region.memory, + Judged::new(MemoryFact { term: term.clone() }, term.evidence().clone()), + ); + } +} + +fn push_branch_observation_region( + by_anchor: &mut BTreeMap, + branch: &BranchObservation, + diagnostics: &SymbolicFunctionFactDiagnostics, +) { + let region = ensure_semantic_region(by_anchor, branch.block_addr); + region.frontier.insert(branch.true_target); + region.frontier.insert(branch.false_target); + + let true_evidence = if branch.true_compiled.is_some() || branch.true_condition.is_some() { + control_fact_evidence( + branch.true_compiled.as_ref(), + branch.true_condition.as_deref(), + diagnostics, + ) + } else { + target_status_evidence(branch.true_status) + }; + let false_evidence = if branch.false_compiled.is_some() || branch.false_condition.is_some() { + control_fact_evidence( + branch.false_compiled.as_ref(), + branch.false_condition.as_deref(), + diagnostics, + ) + } else { + target_status_evidence(branch.false_status) + }; + + push_unique_judged( + &mut region.control, + Judged::new( + ControlFact { + target: branch.true_target, + status: branch.true_status, + branch_truth: Some(true), + condition: branch.true_condition.clone(), + compiled: branch.true_compiled.clone(), + }, + true_evidence.clone(), + ), + ); + push_unique_judged( + &mut region.control, + Judged::new( + ControlFact { + target: branch.false_target, + status: branch.false_status, + branch_truth: Some(false), + condition: branch.false_condition.clone(), + compiled: branch.false_compiled.clone(), + }, + false_evidence.clone(), + ), + ); + push_unique_judged( + &mut region.targets, + Judged::new( + TargetFact { + target: branch.true_target, + status: branch.true_status, + branch_truth: Some(true), + }, + true_evidence, + ), + ); + push_unique_judged( + &mut region.targets, + Judged::new( + TargetFact { + target: branch.false_target, + status: branch.false_status, + branch_truth: Some(false), + }, + false_evidence, + ), + ); + if let Some(compiled) = branch + .true_compiled + .as_ref() + .filter(|compiled| compiled.evidence().allows_narrowing()) + { + push_region_memory_terms(region, compiled.memory_terms.iter().cloned()); + } + if let Some(compiled) = branch + .false_compiled + .as_ref() + .filter(|compiled| compiled.evidence().allows_narrowing()) + { + push_region_memory_terms(region, compiled.memory_terms.iter().cloned()); + } +} + +fn build_canonical_regions( + branch_observations: &[BranchObservation], + summary_memory_terms: &BTreeMap>, + diagnostics: &SymbolicFunctionFactDiagnostics, +) -> BTreeMap { + let mut by_anchor = BTreeMap::::new(); + for branch in branch_observations { + push_branch_observation_region(&mut by_anchor, branch, diagnostics); + } + for (&anchor, terms) in summary_memory_terms { + let region = ensure_semantic_region(&mut by_anchor, anchor); + push_region_memory_terms(region, terms.iter().cloned()); + } + by_anchor + .into_values() + .map(|region| (region.key(), region)) + .collect() +} + +fn control_fact_evidence( + summary: Option<&BackwardConditionSummary>, + condition: Option<&str>, + diagnostics: &SymbolicFunctionFactDiagnostics, +) -> SemanticEvidence { + let branch_budget_limited = diagnostics.skipped_large_cfg; + match summary { + Some(summary) => match summary.precision { + BackwardConditionPrecision::Exact => summary.evidence(), + BackwardConditionPrecision::OverApprox => summary + .evidence() + .with_budget_limited(branch_budget_limited) + .with_reason(SemanticEvidenceReason::PartialPathCoverage), + BackwardConditionPrecision::ResidualSearchRequired => { + let simplified = summary.simplified.trim(); + let has_guard = !simplified.is_empty() && simplified != "true" && simplified != "1"; + if summary.supported_paths > 0 + && has_guard + && summary.backward_memory_residual_fallbacks == 0 + { + SemanticEvidence::likely(SemanticEvidenceReason::ResidualSearchRequired) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized) + .with_budget_limited(branch_budget_limited) + .with_reason(SemanticEvidenceReason::PartialPathCoverage) + } else if summary.supported_paths > 0 && has_guard { + SemanticEvidence::heuristic(SemanticEvidenceReason::ResidualSearchRequired) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized) + .with_budget_limited(branch_budget_limited) + } else if condition.is_some() { + SemanticEvidence::heuristic(SemanticEvidenceReason::GuardOpaque) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_budget_limited(branch_budget_limited) + } else { + SemanticEvidence::residual(SemanticEvidenceReason::ResidualSearchRequired) + .with_budget_limited(branch_budget_limited) + } + } + BackwardConditionPrecision::Unsupported => { + if condition.is_some() { + SemanticEvidence::heuristic(SemanticEvidenceReason::GuardOpaque) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_budget_limited(branch_budget_limited) + } else { + SemanticEvidence::residual(SemanticEvidenceReason::ValueOpaque) + .with_budget_limited(branch_budget_limited) + } + } + }, + None => condition + .map(|_| { + SemanticEvidence::heuristic(SemanticEvidenceReason::GuardOpaque) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_budget_limited(branch_budget_limited) + }) + .unwrap_or_else(|| { + SemanticEvidence::residual(SemanticEvidenceReason::GuardOpaque) + .with_budget_limited(branch_budget_limited) + }), + } +} + +fn derived_summary_memory_term_evidence( + completion: DerivedSummaryCompletion, + exact_value: bool, +) -> SemanticEvidence { + match completion { + DerivedSummaryCompletion::Exact if exact_value => SemanticEvidence::exact(), + DerivedSummaryCompletion::Exact => SemanticEvidence::exact(), + DerivedSummaryCompletion::OverApprox => { + SemanticEvidence::likely(SemanticEvidenceReason::PartialPathCoverage) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized) + } + DerivedSummaryCompletion::BudgetExhausted => { + SemanticEvidence::heuristic(SemanticEvidenceReason::SummaryBudget) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized) + .with_budget_limited(true) + } + DerivedSummaryCompletion::Unknown => { + SemanticEvidence::heuristic(SemanticEvidenceReason::ValueOpaque) + .with_coverage(SemanticEvidenceCoverage::Bounded) + } + } +} + +fn summary_memory_location_expr(arg_index: usize, offset: i64) -> String { + if offset == 0 { + format!("*arg{arg_index}") + } else if offset > 0 { + format!("*(arg{arg_index} + 0x{:x})", offset as u64) + } else { + format!("*(arg{arg_index} - 0x{:x})", offset.unsigned_abs()) + } +} + +fn derive_summary_memory_terms_by_anchor<'ctx>( + func: &SsaArtifact, + branch_blocks: &[(u64, u64, u64)], + derived: &DerivedSummarySet<'ctx>, +) -> BTreeMap> { + let hot_blocks = branch_blocks + .iter() + .flat_map(|(block, true_target, false_target)| [*block, *true_target, *false_target]) + .collect::>(); + let mut by_anchor = BTreeMap::>::new(); + let max_islands = branch_blocks.len().max(1) * 3; + + let mut call_blocks = func + .call_sites() + .by_id + .values() + .filter_map(|call| { + let target = call.direct_target?; + let summary = derived.summaries.get(&InterprocFunctionId(target))?; + if summary + .cases + .iter() + .all(|case| case.memory_writes.is_empty()) + { + return None; + } + let (block_addr, _) = func.inst_op_site(call.at)?; + Some((!hot_blocks.contains(&block_addr), block_addr, summary)) + }) + .collect::>(); + call_blocks.sort_by_key(|(cold_block, block_addr, _)| (*cold_block, *block_addr)); + + for (_, block_addr, summary) in call_blocks.into_iter().take(max_islands) { + let terms = by_anchor.entry(block_addr).or_default(); + for case in &summary.cases { + for write in &case.memory_writes { + let exact_value = write.value.is_concrete(); + let evidence = + derived_summary_memory_term_evidence(summary.completion, exact_value); + let term = BackwardMemoryCondition { + region: crate::BackwardMemoryRegion::Argument { + index: write.arg_index, + }, + offset_lo: write.offset, + offset_hi: write.offset, + size: write.size, + exact_offset: matches!(summary.completion, DerivedSummaryCompletion::Exact), + evidence, + binding: None, + expr: summary_memory_location_expr(write.arg_index, write.offset), + value_expr: Some(write.value.to_string()), + exact_value, + }; + if !terms.contains(&term) { + terms.push(term); + } + } + } + } + + by_anchor +} + +fn symbolic_condition_hint(summary: Option<&BackwardConditionSummary>) -> Option { + summary + .map(|compiled| compiled.simplified.trim().to_string()) + .filter(|text| !text.is_empty() && text != "true") +} + +fn symbolic_fact_explorer<'ctx>(ctx: &'ctx Context) -> PathExplorer<'ctx> { + let mut explorer = PathExplorer::with_config( + ctx, + ExploreConfig { + subsumption_states: true, + max_states: 256, + max_depth: 96, + max_completed_paths: Some(8), + merge_states: false, + ..ExploreConfig::default() + }, + ); + explorer.set_target_guided_queries(true); + explorer +} + +fn collect_branch_blocks(func: &SsaArtifact) -> Vec<(u64, u64, u64)> { + func.cfg() + .block_addrs() + .filter_map(|block_addr| { + let block = func.cfg().get_block(block_addr)?; + match block.terminator { + r2ssa::BlockTerminator::ConditionalBranch { + true_target, + false_target, + } => Some((block_addr, true_target, false_target)), + _ => None, + } + }) + .collect() +} + +fn large_cfg_branch_limit(func: &SsaArtifact) -> usize { + let summary = func.function().cfg_risk_summary(); + match summary.switch_block_count { + 0 => 8, + 1..=2 => 10, + _ => 12, + } +} + +fn limited_branch_blocks(func: &SsaArtifact, limit: usize) -> Vec<(u64, u64, u64)> { + if limit == 0 { + return Vec::new(); + } + + let mut queue = VecDeque::from([func.entry]); + let mut visited = BTreeSet::new(); + let mut selected = Vec::new(); + + while let Some(block_addr) = queue.pop_front() { + if !visited.insert(block_addr) { + continue; + } + + if let Some(block) = func.cfg().get_block(block_addr) + && let r2ssa::BlockTerminator::ConditionalBranch { + true_target, + false_target, + } = block.terminator + { + selected.push((block_addr, true_target, false_target)); + if selected.len() >= limit { + break; + } + } + + for successor in func.successors(block_addr) { + if !visited.contains(&successor) { + queue.push_back(successor); + } + } + } + + if selected.is_empty() { + collect_branch_blocks(func) + .into_iter() + .take(limit) + .collect() + } else { + selected + } +} + +fn symbolic_reachability_status( + feasible_paths: usize, + budget_exhausted: bool, +) -> SymbolicReachabilityStatus { + if feasible_paths > 0 { + SymbolicReachabilityStatus::Reachable + } else if budget_exhausted { + SymbolicReachabilityStatus::Unknown + } else { + SymbolicReachabilityStatus::Unreachable + } +} + +fn collect_branch_observations_for_branch_blocks<'ctx, F>( + ctx: &'ctx Context, + func: &SsaArtifact, + arch: &ArchSpec, + branch_blocks: &[(u64, u64, u64)], + install_hooks: F, +) -> (Vec, SymbolicFunctionFactDiagnostics) +where + F: Fn(&mut PathExplorer<'ctx>), +{ + let mut branch_facts = Vec::new(); + let mut diagnostics = SymbolicFunctionFactDiagnostics::default(); + for &(block_addr, true_target, false_target) in branch_blocks { + diagnostics.branches_evaluated += 1; + let predicate_uses_call_result = predicate_depends_on_call_result(func, block_addr); + + let make_state = || { + let mut state = SymState::new(ctx, func.entry); + seed_default_state_for_arch(&mut state, func, Some(arch)); + state + }; + + let mut true_explorer = symbolic_fact_explorer(ctx); + install_hooks(&mut true_explorer); + let true_initial_state = make_state(); + let (compiled_true_status, true_compiled) = compiled_branch_reachability_status( + &true_explorer, + func, + &true_initial_state, + block_addr, + true, + ); + let true_condition = symbolic_condition_hint(true_compiled.as_ref()); + let true_status = if let Some(status) = compiled_true_status { + status + } else if predicate_uses_call_result || func_contains_calls(func) { + SymbolicReachabilityStatus::Unknown + } else { + let paths = true_explorer.find_paths_to(func, make_state(), true_target); + symbolic_reachability_status(paths.len(), true_explorer.budget_exhausted()) + }; + + let mut false_explorer = symbolic_fact_explorer(ctx); + install_hooks(&mut false_explorer); + let false_initial_state = make_state(); + let (compiled_false_status, false_compiled) = compiled_branch_reachability_status( + &false_explorer, + func, + &false_initial_state, + block_addr, + false, + ); + let false_condition = symbolic_condition_hint(false_compiled.as_ref()); + let false_status = if let Some(status) = compiled_false_status { + status + } else if predicate_uses_call_result || func_contains_calls(func) { + SymbolicReachabilityStatus::Unknown + } else { + let paths = false_explorer.find_paths_to(func, make_state(), false_target); + symbolic_reachability_status(paths.len(), false_explorer.budget_exhausted()) + }; + + if matches!(true_status, SymbolicReachabilityStatus::Unknown) + || matches!(false_status, SymbolicReachabilityStatus::Unknown) + { + diagnostics.branches_unknown += 1; + } + if matches!( + (true_status, false_status), + ( + SymbolicReachabilityStatus::Reachable, + SymbolicReachabilityStatus::Unreachable + ) | ( + SymbolicReachabilityStatus::Unreachable, + SymbolicReachabilityStatus::Reachable + ) + ) { + diagnostics.branches_pruned += 1; + } + + branch_facts.push(BranchObservation { + block_addr, + true_target, + false_target, + true_status, + false_status, + true_condition, + false_condition, + true_compiled, + false_compiled, + }); + } + + (branch_facts, diagnostics) +} + +fn install_derived_summary_set<'ctx>( + explorer: &mut PathExplorer<'ctx>, + registry: &SummaryRegistry<'ctx>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + derived: &DerivedSummarySet<'ctx>, + symbol_map: &HashMap, +) { + let prepared = scope + .and_then(|scope| scope.root()) + .map(|root| &root.prepared) + .unwrap_or(func); + let _ = registry.install_interproc_summaries_for_function( + explorer, + prepared, + &derived.interproc, + symbol_map, + ); + let _ = registry.install_derived_summaries_for_function( + explorer, + prepared, + &derived.summaries, + symbol_map, + ); + let _ = registry.install_known_symbols_for_function(explorer, prepared, symbol_map); +} + +fn install_symbolic_fact_hooks<'ctx>( + ctx: &'ctx Context, + explorer: &mut PathExplorer<'ctx>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: &ArchSpec, + summary_profile: SummaryProfile, + symbol_map: &HashMap, +) { + let Some(registry) = + SummaryRegistry::with_profile_for_arch_and_symbols(arch, symbol_map, summary_profile) + else { + return; + }; + if let Some(scope) = scope { + let derived = registry.derive_symbolic_summaries(ctx, scope, Some(arch), symbol_map); + install_derived_summary_set(explorer, ®istry, func, Some(scope), &derived, symbol_map); + return; + } + let _ = registry.install_known_symbols_for_function(explorer, func, symbol_map); +} + +fn compiled_branch_reachability_status<'ctx>( + explorer: &PathExplorer<'ctx>, + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + block_addr: u64, + truth: bool, +) -> ( + Option, + Option, +) { + let derived_summaries = explorer.derived_call_summary_views(); + if func_contains_calls(func) && derived_summaries.is_empty() { + return (None, None); + } + let Some(compiled) = compile_branch_precondition_with_summaries( + func, + initial_state, + block_addr, + truth, + &derived_summaries, + ) else { + return (None, None); + }; + let summary = compiled.summary; + if !matches!(summary.precision, BackwardConditionPrecision::Exact) { + return (None, Some(summary)); + } + let status = match explorer + .solver() + .sat_with_constraint(initial_state, &compiled.predicate) + { + SatResult::Sat => Some(SymbolicReachabilityStatus::Reachable), + SatResult::Unsat => Some(SymbolicReachabilityStatus::Unreachable), + SatResult::Unknown => None, + }; + (status, Some(summary)) +} + +fn func_contains_calls(func: &SsaArtifact) -> bool { + func.blocks().any(|block| { + block + .ops + .iter() + .any(|op| matches!(op, r2ssa::SSAOp::Call { .. } | r2ssa::SSAOp::CallInd { .. })) + }) +} + +fn local_memory_store_value_ids( + func: &SsaArtifact, + inst_id: r2ssa::graph::InstId, + size: u32, +) -> Vec { + let Some(uses) = func.memory().uses_by_inst.get(&inst_id) else { + return Vec::new(); + }; + let mut values = Vec::new(); + for use_fact in uses { + if use_fact.location.size != size { + continue; + } + for (def_inst, defs) in &func.memory().defs_by_inst { + for def in defs { + if def.next_version != use_fact.version || def.location != use_fact.location { + continue; + } + let Some(inst) = func.graph().inst(*def_inst) else { + continue; + }; + let r2ssa::graph::InstPayload::Op(r2ssa::SSAOp::Store { val, .. }) = &inst.payload + else { + continue; + }; + if let Some(value_id) = func.graph().value_id_for_var(val) { + values.push(value_id); + } + } + } + } + values +} + +fn value_depends_on_call_result( + func: &SsaArtifact, + value_id: r2ssa::graph::ValueId, + visited: &mut BTreeSet, +) -> bool { + if !visited.insert(value_id) { + return false; + } + + let Some(inst_id) = func.graph().def_inst(value_id) else { + return false; + }; + let Some(inst) = func.graph().inst(inst_id) else { + return false; + }; + + match &inst.payload { + r2ssa::graph::InstPayload::Phi { .. } => inst + .inputs + .iter() + .copied() + .any(|input| value_depends_on_call_result(func, input, visited)), + r2ssa::graph::InstPayload::Op(op) => match op { + r2ssa::SSAOp::CallDefine { .. } => true, + r2ssa::SSAOp::Load { dst, .. } => local_memory_store_value_ids(func, inst_id, dst.size) + .into_iter() + .any(|stored| value_depends_on_call_result(func, stored, visited)), + _ => inst + .inputs + .iter() + .copied() + .any(|input| value_depends_on_call_result(func, input, visited)), + }, + } +} + +fn predicate_depends_on_call_result(func: &SsaArtifact, block_addr: u64) -> bool { + let Some(predicate) = func + .predicates() + .predicates + .values() + .find(|fact| fact.block_addr == block_addr) + else { + return false; + }; + let mut visited = BTreeSet::new(); + value_depends_on_call_result(func, predicate.condition, &mut visited) +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn collect_canonical_semantic_regions_with_derived<'ctx>( + ctx: &'ctx Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: &ArchSpec, + symbol_map: &HashMap, + summary_profile: SummaryProfile, + registry: &SummaryRegistry<'ctx>, + derived: &DerivedSummarySet<'ctx>, +) -> CollectedNativeSemanticRegions { + collect_canonical_semantic_regions_with_derived_for_branch_blocks( + ctx, + func, + scope, + arch, + &collect_branch_blocks(func), + summary_profile, + registry, + derived, + symbol_map, + ) +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn collect_canonical_semantic_regions_with_derived_for_branch_blocks<'ctx>( + ctx: &'ctx Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: &ArchSpec, + branch_blocks: &[(u64, u64, u64)], + summary_profile: SummaryProfile, + registry: &SummaryRegistry<'ctx>, + derived: &DerivedSummarySet<'ctx>, + symbol_map: &HashMap, +) -> CollectedNativeSemanticRegions { + let (branch_facts, diagnostics) = + collect_branch_observations_for_branch_blocks(ctx, func, arch, branch_blocks, |explorer| { + install_derived_summary_set(explorer, registry, func, scope, derived, symbol_map); + }); + let _ = summary_profile; + CollectedNativeSemanticRegions { + regions: build_canonical_regions( + &branch_facts, + &derive_summary_memory_terms_by_anchor(func, branch_blocks, derived), + &diagnostics, + ), + diagnostics, + } +} + +pub(super) fn collect_large_cfg_canonical_semantic_regions_with_limit( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: &ArchSpec, + symbol_map: &HashMap, + summary_profile: SummaryProfile, + branch_limit: usize, +) -> CollectedNativeSemanticRegions { + let branch_blocks = limited_branch_blocks(func, branch_limit.max(1)); + let mut collected = if let Some(scope) = scope { + if let Some(registry) = + SummaryRegistry::with_profile_for_arch_and_symbols(arch, symbol_map, summary_profile) + { + let derived = registry.derive_symbolic_summaries(ctx, scope, Some(arch), symbol_map); + collect_canonical_semantic_regions_with_derived_for_branch_blocks( + ctx, + func, + Some(scope), + arch, + &branch_blocks, + summary_profile, + ®istry, + &derived, + symbol_map, + ) + } else { + let (branch_facts, diagnostics) = collect_branch_observations_for_branch_blocks( + ctx, + func, + arch, + &branch_blocks, + |explorer| { + install_symbolic_fact_hooks( + ctx, + explorer, + func, + None, + arch, + summary_profile, + symbol_map, + ); + }, + ); + CollectedNativeSemanticRegions { + regions: build_canonical_regions(&branch_facts, &BTreeMap::new(), &diagnostics), + diagnostics, + } + } + } else { + let (branch_facts, diagnostics) = collect_branch_observations_for_branch_blocks( + ctx, + func, + arch, + &branch_blocks, + |explorer| { + install_symbolic_fact_hooks( + ctx, + explorer, + func, + None, + arch, + summary_profile, + symbol_map, + ); + }, + ); + CollectedNativeSemanticRegions { + regions: build_canonical_regions(&branch_facts, &BTreeMap::new(), &diagnostics), + diagnostics, + } + }; + collected.diagnostics.skipped_large_cfg = true; + collected +} + +pub(super) fn collect_canonical_semantic_regions_with_scope_and_profile( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, +) -> CollectedNativeSemanticRegions { + let Some(arch) = arch else { + return CollectedNativeSemanticRegions { + regions: BTreeMap::new(), + diagnostics: SymbolicFunctionFactDiagnostics { + skipped_missing_arch: true, + ..SymbolicFunctionFactDiagnostics::default() + }, + }; + }; + + let cfg_summary = func.function().cfg_risk_summary(); + if cfg_summary.block_count > 96 || cfg_summary.switch_block_count > 8 { + return collect_large_cfg_canonical_semantic_regions_with_limit( + ctx, + func, + scope, + arch, + symbol_map, + summary_profile, + large_cfg_branch_limit(func), + ); + } + + if let Some(scope) = scope { + let Some(registry) = + SummaryRegistry::with_profile_for_arch_and_symbols(arch, symbol_map, summary_profile) + else { + return CollectedNativeSemanticRegions::default(); + }; + let derived = registry.derive_symbolic_summaries(ctx, scope, Some(arch), symbol_map); + return collect_canonical_semantic_regions_with_derived( + ctx, + func, + Some(scope), + arch, + symbol_map, + summary_profile, + ®istry, + &derived, + ); + } + + let branch_blocks = collect_branch_blocks(func); + let (branch_facts, diagnostics) = collect_branch_observations_for_branch_blocks( + ctx, + func, + arch, + &branch_blocks, + |explorer| { + install_symbolic_fact_hooks( + ctx, + explorer, + func, + None, + arch, + summary_profile, + symbol_map, + ); + }, + ); + CollectedNativeSemanticRegions { + regions: build_canonical_regions(&branch_facts, &BTreeMap::new(), &diagnostics), + diagnostics, + } +} diff --git a/crates/r2sym/src/semantics/mod.rs b/crates/r2sym/src/semantics/mod.rs new file mode 100644 index 0000000..4d860af --- /dev/null +++ b/crates/r2sym/src/semantics/mod.rs @@ -0,0 +1,42 @@ +mod artifact; +mod cache; +mod classify; +mod compiler; +pub(crate) mod facts; +mod plan; +mod region; +mod vm; + +pub(crate) use artifact::TargetQueryRouteInput; +pub use artifact::{ + ResidualReason, SemanticArtifact, SemanticArtifactBody, SemanticConfidence, SemanticEvidence, + SemanticEvidenceAmbiguity, SemanticEvidenceCoverage, SemanticEvidenceProvenance, + SemanticEvidenceReason, SemanticEvidenceSoundness, SliceClass, +}; +pub use cache::{ + SEMANTIC_ARTIFACT_SCHEMA_VERSION, SemanticCompilationResult, SemanticSeedMode, + stable_scope_hash, +}; +pub use compiler::compile_semantic_artifact_default_with_scope; +pub use compiler::{ + compile_function_semantics_with_scope, compile_function_semantics_with_scope_and_replay_seed, + compile_query_semantic_artifact_with_scope, + compile_query_semantic_artifact_with_scope_and_replay_seed, + compile_semantic_artifact_with_scope, compile_semantic_artifact_with_scope_and_replay_seed, +}; +pub use facts::SymbolicReachabilityStatus; +pub use plan::{ + ArtifactBuildPlan, DecompilePlan, QueryGuidanceMode, QueryPlan, TargetQueryExecutionRoute, + TargetQueryPlan, TargetQueryRoutePlan, TypePlan, +}; +pub use region::{ + ArtifactGranularity, ControlFact, ExecutionModel, Judged, MemoryFact, NativeArtifactBody, + NativeFunctionSummary, RefinementStage, RegionKey, SemanticArtifactDiagnostics, + SemanticPredicate, SemanticRegion, SemanticTargetConditionSource, TargetFact, VmArtifactBody, +}; +pub use vm::{ + InterpreterDispatchSummary, InterpreterKind, VmBinaryOp, VmGuardCondition, VmGuardedExit, + VmMemoryCondition, VmMemoryRegionRef, VmStateUpdate, VmStepSummary, VmTransferArm, VmUnaryOp, + VmValueExpr, +}; +pub(crate) use vm::{build_vm_step_summary, classify_interpreter_like}; diff --git a/crates/r2sym/src/semantics/plan.rs b/crates/r2sym/src/semantics/plan.rs new file mode 100644 index 0000000..912fb05 --- /dev/null +++ b/crates/r2sym/src/semantics/plan.rs @@ -0,0 +1,542 @@ +use serde::{Deserialize, Serialize}; + +use super::artifact::ResidualReason; +use super::region::{ExecutionModel, RefinementStage, SemanticArtifactDiagnostics}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum ArtifactBuildPlan { + Ready, + Fallback { reason: String }, + Residual { reasons: Vec }, + Refuse { reason: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum QueryPlan { + Ready, + Fallback { reason: String }, + Residual { reasons: Vec }, + Refuse { reason: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum TypePlan { + NativeAugmentation, + VmSummaryOnly { reason: String }, + Fallback { reason: String }, + Residual { reasons: Vec }, + Refuse { reason: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum DecompilePlan { + NativeStructured, + NativeLinear { reason: String }, + VmSummaryOnly { reason: String }, + Fallback { reason: String }, + Residual { reasons: Vec }, + Refuse { reason: String }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum QueryGuidanceMode { + Necessary, + NarrowOnly, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum TargetQueryPlan { + Ready { mode: QueryGuidanceMode }, + Fallback { reason: String }, + Residual { reasons: Vec }, + Refuse { reason: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum TargetQueryExecutionRoute { + ContinuationSeeded { + bridge_target: u64, + route: Box, + }, + ArtifactCondition { + mode: QueryGuidanceMode, + }, + ArtifactMemoryOnly, + DynamicTargetCompile { + reason: String, + mode: QueryGuidanceMode, + }, + VmTargetCompile { + reason: String, + }, + ResidualOnly { + reasons: Vec, + }, + Refuse { + reason: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TargetQueryRoutePlan { + pub target_plan: TargetQueryPlan, + pub execution: TargetQueryExecutionRoute, +} + +impl TargetQueryRoutePlan { + pub fn dynamic_fallback() -> Self { + Self { + target_plan: TargetQueryPlan::Fallback { + reason: "semantic artifact unavailable".to_string(), + }, + execution: TargetQueryExecutionRoute::DynamicTargetCompile { + reason: "semantic artifact unavailable".to_string(), + mode: QueryGuidanceMode::NarrowOnly, + }, + } + } +} + +impl TypePlan { + pub fn allows_native_augmentation(&self) -> bool { + matches!(self, Self::NativeAugmentation) + } + + pub fn is_vm_summary_only(&self) -> bool { + matches!(self, Self::VmSummaryOnly { .. }) + } +} + +impl DecompilePlan { + pub fn allows_native_linearization(&self) -> bool { + matches!(self, Self::NativeStructured | Self::NativeLinear { .. }) + } + + pub fn allows_native_structuring(&self) -> bool { + matches!(self, Self::NativeStructured) + } + + pub fn is_vm_summary_only(&self) -> bool { + matches!(self, Self::VmSummaryOnly { .. }) + } +} + +fn residual_reason_strings(diagnostics: &SemanticArtifactDiagnostics) -> Vec { + diagnostics + .residual_reasons + .iter() + .map(|reason| format!("{reason:?}")) + .collect() +} + +fn joined_residual_reason(reasons: &[String]) -> String { + if reasons.is_empty() { + "residual semantic guidance required".to_string() + } else { + reasons.join(", ") + } +} + +fn has_large_cfg_only_residual(diagnostics: &SemanticArtifactDiagnostics) -> bool { + !diagnostics.residual_reasons.is_empty() + && diagnostics + .residual_reasons + .iter() + .all(|reason| matches!(reason, ResidualReason::LargeCfg)) +} + +pub fn derive_artifact_build_plan( + stage: RefinementStage, + diagnostics: &SemanticArtifactDiagnostics, +) -> ArtifactBuildPlan { + if diagnostics.skipped_missing_arch { + return ArtifactBuildPlan::Refuse { + reason: "missing architecture".to_string(), + }; + } + match stage { + RefinementStage::Residual => ArtifactBuildPlan::Residual { + reasons: residual_reason_strings(diagnostics), + }, + RefinementStage::Raw | RefinementStage::Compiled => ArtifactBuildPlan::Ready, + } +} + +pub fn derive_query_plan( + stage: RefinementStage, + execution: ExecutionModel, + diagnostics: &SemanticArtifactDiagnostics, + has_query_support: bool, +) -> QueryPlan { + if matches!(execution, ExecutionModel::Vm) || has_query_support { + QueryPlan::Ready + } else if matches!(stage, RefinementStage::Residual) { + QueryPlan::Residual { + reasons: residual_reason_strings(diagnostics), + } + } else { + QueryPlan::Refuse { + reason: "query capability unavailable".to_string(), + } + } +} + +pub fn derive_type_plan( + stage: RefinementStage, + execution: ExecutionModel, + diagnostics: &SemanticArtifactDiagnostics, + has_native_semantics: bool, +) -> TypePlan { + if matches!(execution, ExecutionModel::Vm) { + TypePlan::VmSummaryOnly { + reason: "vm artifacts expose summary-only type hints".to_string(), + } + } else if !diagnostics.skipped_large_cfg || has_native_semantics { + TypePlan::NativeAugmentation + } else if matches!(stage, RefinementStage::Residual) { + TypePlan::Residual { + reasons: residual_reason_strings(diagnostics), + } + } else { + TypePlan::Fallback { + reason: "type capability unavailable".to_string(), + } + } +} + +pub fn derive_decompile_plan( + stage: RefinementStage, + execution: ExecutionModel, + diagnostics: &SemanticArtifactDiagnostics, + has_native_semantics: bool, + supports_guarded_structuring: bool, +) -> DecompilePlan { + if matches!(execution, ExecutionModel::Vm) { + DecompilePlan::VmSummaryOnly { + reason: "vm artifacts currently support summary rendering only".to_string(), + } + } else if matches!(execution, ExecutionModel::Native) + && has_native_semantics + && supports_guarded_structuring + { + DecompilePlan::NativeStructured + } else if matches!(execution, ExecutionModel::Native) && has_native_semantics { + DecompilePlan::NativeLinear { + reason: "guarded structuring unavailable".to_string(), + } + } else if matches!(stage, RefinementStage::Residual) + && !has_large_cfg_only_residual(diagnostics) + { + DecompilePlan::Residual { + reasons: residual_reason_strings(diagnostics), + } + } else { + DecompilePlan::Fallback { + reason: "decompile capability unavailable".to_string(), + } + } +} + +pub fn derive_target_query_plan( + query_plan: &QueryPlan, + has_guidance: bool, + has_source_conflict: bool, + necessary_for_target: bool, +) -> TargetQueryPlan { + match query_plan { + QueryPlan::Refuse { reason } => TargetQueryPlan::Refuse { + reason: reason.clone(), + }, + QueryPlan::Residual { reasons } => TargetQueryPlan::Residual { + reasons: reasons.clone(), + }, + QueryPlan::Fallback { reason } => TargetQueryPlan::Fallback { + reason: reason.clone(), + }, + QueryPlan::Ready if has_source_conflict => TargetQueryPlan::Residual { + reasons: vec!["conflicting target guidance sources".to_string()], + }, + QueryPlan::Ready if has_guidance => TargetQueryPlan::Ready { + mode: if necessary_for_target { + QueryGuidanceMode::Necessary + } else { + QueryGuidanceMode::NarrowOnly + }, + }, + QueryPlan::Ready => TargetQueryPlan::Fallback { + reason: "target guidance unavailable".to_string(), + }, + } +} + +pub fn derive_target_query_route_plan( + query_plan: &QueryPlan, + target_plan: &TargetQueryPlan, + execution: ExecutionModel, + has_authoritative_source: bool, + has_memory_guidance: bool, +) -> TargetQueryRoutePlan { + let execution = match query_plan { + QueryPlan::Ready => match target_plan { + TargetQueryPlan::Ready { mode } => match execution { + ExecutionModel::Vm => TargetQueryExecutionRoute::VmTargetCompile { + reason: "vm target compilation selected".to_string(), + }, + ExecutionModel::Native if has_authoritative_source => { + TargetQueryExecutionRoute::ArtifactCondition { mode: *mode } + } + ExecutionModel::Native if has_memory_guidance => { + TargetQueryExecutionRoute::ArtifactMemoryOnly + } + ExecutionModel::Native => TargetQueryExecutionRoute::DynamicTargetCompile { + reason: "target guidance unavailable".to_string(), + mode: *mode, + }, + }, + TargetQueryPlan::Fallback { reason } => match execution { + ExecutionModel::Vm => TargetQueryExecutionRoute::VmTargetCompile { + reason: reason.clone(), + }, + ExecutionModel::Native => TargetQueryExecutionRoute::DynamicTargetCompile { + reason: reason.clone(), + mode: QueryGuidanceMode::NarrowOnly, + }, + }, + TargetQueryPlan::Residual { reasons } => TargetQueryExecutionRoute::ResidualOnly { + reasons: reasons.clone(), + }, + TargetQueryPlan::Refuse { reason } => TargetQueryExecutionRoute::Refuse { + reason: reason.clone(), + }, + }, + QueryPlan::Fallback { reason } => match execution { + ExecutionModel::Vm => TargetQueryExecutionRoute::VmTargetCompile { + reason: reason.clone(), + }, + ExecutionModel::Native => TargetQueryExecutionRoute::DynamicTargetCompile { + reason: reason.clone(), + mode: QueryGuidanceMode::NarrowOnly, + }, + }, + QueryPlan::Residual { reasons } => match execution { + ExecutionModel::Vm => TargetQueryExecutionRoute::ResidualOnly { + reasons: reasons.clone(), + }, + ExecutionModel::Native => TargetQueryExecutionRoute::DynamicTargetCompile { + reason: joined_residual_reason(reasons), + mode: QueryGuidanceMode::NarrowOnly, + }, + }, + QueryPlan::Refuse { reason } => TargetQueryExecutionRoute::Refuse { + reason: reason.clone(), + }, + }; + TargetQueryRoutePlan { + target_plan: target_plan.clone(), + execution, + } +} + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + + use super::{ + DecompilePlan, QueryPlan, TargetQueryExecutionRoute, TargetQueryPlan, TargetQueryRoutePlan, + TypePlan, derive_decompile_plan, derive_query_plan, derive_target_query_plan, + derive_target_query_route_plan, derive_type_plan, + }; + use crate::{ExecutionModel, RefinementStage, SemanticArtifactDiagnostics}; + + fn diagnostics(skipped_large_cfg: bool) -> SemanticArtifactDiagnostics { + SemanticArtifactDiagnostics { + branches_evaluated: 0, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg, + residual_reasons: Vec::new(), + ambiguous_targets: Vec::new(), + cache_hit: false, + } + } + + proptest! { + #[test] + fn query_plan_derivation_is_total( + is_vm in any::(), + is_residual in any::(), + skipped_large_cfg in any::(), + has_query_support in any::(), + ) { + let plan = derive_query_plan( + if is_residual { RefinementStage::Residual } else { RefinementStage::Compiled }, + if is_vm { ExecutionModel::Vm } else { ExecutionModel::Native }, + &diagnostics(skipped_large_cfg), + has_query_support, + ); + let is_valid = match plan { + QueryPlan::Ready + | QueryPlan::Fallback { .. } + | QueryPlan::Residual { .. } + | QueryPlan::Refuse { .. } => true, + }; + prop_assert!(is_valid); + } + + #[test] + fn type_plan_derivation_is_total( + is_vm in any::(), + is_residual in any::(), + skipped_large_cfg in any::(), + has_native_semantics in any::(), + ) { + let plan = derive_type_plan( + if is_residual { RefinementStage::Residual } else { RefinementStage::Compiled }, + if is_vm { ExecutionModel::Vm } else { ExecutionModel::Native }, + &diagnostics(skipped_large_cfg), + has_native_semantics, + ); + let is_valid = match plan { + TypePlan::NativeAugmentation + | TypePlan::VmSummaryOnly { .. } + | TypePlan::Fallback { .. } + | TypePlan::Residual { .. } + | TypePlan::Refuse { .. } => true, + }; + prop_assert!(is_valid); + } + + #[test] + fn decompile_plan_derivation_is_total( + is_vm in any::(), + is_residual in any::(), + skipped_large_cfg in any::(), + has_native_semantics in any::(), + supports_guarded_structuring in any::(), + ) { + let plan = derive_decompile_plan( + if is_residual { RefinementStage::Residual } else { RefinementStage::Compiled }, + if is_vm { ExecutionModel::Vm } else { ExecutionModel::Native }, + &diagnostics(skipped_large_cfg), + has_native_semantics, + supports_guarded_structuring, + ); + let is_valid = match plan { + DecompilePlan::NativeStructured + | DecompilePlan::NativeLinear { .. } + | DecompilePlan::VmSummaryOnly { .. } + | DecompilePlan::Fallback { .. } + | DecompilePlan::Residual { .. } + | DecompilePlan::Refuse { .. } => true, + }; + prop_assert!(is_valid); + } + } + + #[test] + fn target_query_plan_refuses_conflicting_guidance() { + let plan = derive_target_query_plan(&QueryPlan::Ready, true, true, true); + assert!(matches!(plan, TargetQueryPlan::Residual { .. })); + } + + #[test] + fn target_query_route_plan_blocks_conflicting_guidance() { + let target_plan = derive_target_query_plan(&QueryPlan::Ready, true, true, true); + let route = derive_target_query_route_plan( + &QueryPlan::Ready, + &target_plan, + ExecutionModel::Native, + true, + true, + ); + assert!(matches!( + route.target_plan, + TargetQueryPlan::Residual { .. } + )); + assert!(matches!( + route.execution, + TargetQueryExecutionRoute::ResidualOnly { .. } + )); + } + + #[test] + fn target_query_route_plan_keeps_ready_paths_artifact_authoritative() { + let target_plan = derive_target_query_plan(&QueryPlan::Ready, false, false, false); + let route = derive_target_query_route_plan( + &QueryPlan::Ready, + &target_plan, + ExecutionModel::Native, + false, + false, + ); + assert!(matches!( + route.target_plan, + TargetQueryPlan::Fallback { .. } + )); + assert!(matches!( + route.execution, + TargetQueryExecutionRoute::DynamicTargetCompile { .. } + )); + } + + #[test] + fn target_query_route_plan_allows_dynamic_fallback_on_residual_routes() { + let query_plan = QueryPlan::Residual { + reasons: vec!["budget".to_string()], + }; + let target_plan = derive_target_query_plan(&query_plan, false, false, false); + let route = derive_target_query_route_plan( + &query_plan, + &target_plan, + ExecutionModel::Native, + false, + false, + ); + assert!(matches!( + route.execution, + TargetQueryExecutionRoute::DynamicTargetCompile { .. } + )); + } + + proptest! { + #[test] + fn target_query_route_plan_is_total( + is_vm in any::(), + ready in any::(), + conflict in any::(), + has_guidance in any::(), + necessary in any::(), + has_authoritative_source in any::(), + has_memory_guidance in any::(), + ) { + let query_plan = if ready { + QueryPlan::Ready + } else { + QueryPlan::Residual { reasons: vec!["budget".to_string()] } + }; + let target_plan = derive_target_query_plan(&query_plan, has_guidance, conflict, necessary); + let route = derive_target_query_route_plan( + &query_plan, + &target_plan, + if is_vm { + ExecutionModel::Vm + } else { + ExecutionModel::Native + }, + has_authoritative_source, + has_memory_guidance, + ); + let TargetQueryRoutePlan { .. } = route; + match route.execution { + TargetQueryExecutionRoute::ContinuationSeeded { .. } + | TargetQueryExecutionRoute::ArtifactCondition { .. } + | TargetQueryExecutionRoute::ArtifactMemoryOnly + | TargetQueryExecutionRoute::DynamicTargetCompile { .. } + | TargetQueryExecutionRoute::VmTargetCompile { .. } + | TargetQueryExecutionRoute::ResidualOnly { .. } + | TargetQueryExecutionRoute::Refuse { .. } => {} + } + } + } +} diff --git a/crates/r2sym/src/semantics/region.rs b/crates/r2sym/src/semantics/region.rs new file mode 100644 index 0000000..dc0a6c2 --- /dev/null +++ b/crates/r2sym/src/semantics/region.rs @@ -0,0 +1,807 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::backward::{BackwardConditionSummary, BackwardMemoryCondition}; +use crate::sim::DerivedSummaryDiagnostics; + +use super::artifact::{ResidualReason, SemanticEvidence, SliceClass}; +use super::facts::SymbolicReachabilityStatus; +use super::vm::{InterpreterDispatchSummary, VmStepSummary}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum RefinementStage { + Raw, + Compiled, + Residual, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ArtifactGranularity { + WholeFunction, + Regioned, + SummaryOnly, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ExecutionModel { + Native, + Vm, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct RegionKey { + pub anchor_block: u64, + pub frontier: BTreeSet, +} + +impl RegionKey { + pub fn new(anchor_block: u64, frontier: BTreeSet) -> Self { + Self { + anchor_block, + frontier, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Judged { + pub value: T, + #[serde(default, skip_serializing_if = "SemanticEvidence::is_default_exact")] + pub evidence: SemanticEvidence, +} + +impl Judged { + pub fn new(value: T, evidence: SemanticEvidence) -> Self { + Self { value, evidence } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ControlFact { + pub target: u64, + pub status: SymbolicReachabilityStatus, + #[serde(skip_serializing_if = "Option::is_none")] + pub branch_truth: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub condition: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub compiled: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MemoryFact { + pub term: BackwardMemoryCondition, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SemanticPredicate { + pub expr: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub compiled: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TargetFact { + pub target: u64, + pub status: SymbolicReachabilityStatus, + #[serde(skip_serializing_if = "Option::is_none")] + pub branch_truth: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SemanticRegion { + pub anchor: u64, + pub frontier: BTreeSet, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub control: Vec>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub memory: Vec>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub pre: Vec>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub post: Vec>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub targets: Vec>, +} + +impl SemanticRegion { + pub fn key(&self) -> RegionKey { + RegionKey::new(self.anchor, self.frontier.clone()) + } + + fn compiled_condition_for_target( + &self, + target: u64, + proof_only: bool, + ) -> Option<&BackwardConditionSummary> { + self.control + .iter() + .find(|fact| fact.value.target == target) + .and_then(|fact| { + let allowed = if proof_only { + fact.evidence.allows_hard_proof() + } else { + fact.evidence.allows_narrowing() + }; + allowed.then_some(fact.value.compiled.as_ref()).flatten() + }) + } + + fn unique_compiled_condition(&self, proof_only: bool) -> Option<&BackwardConditionSummary> { + let mut candidates = self.control.iter().filter_map(|fact| { + let allowed = if proof_only { + fact.evidence.allows_hard_proof() + } else { + fact.evidence.allows_narrowing() + }; + allowed.then_some(fact.value.compiled.as_ref()).flatten() + }); + let first = candidates.next()?; + candidates + .all(|candidate| candidate == first) + .then_some(first) + } + + fn unique_reachable_target(&self, proof_only: bool) -> Option { + let mut candidates = self.control.iter().filter_map(|fact| { + let allowed = if proof_only { + fact.evidence.allows_hard_proof() + } else { + fact.evidence.allows_narrowing() + }; + allowed + .then_some(matches!( + fact.value.status, + SymbolicReachabilityStatus::Reachable + )) + .and_then(|reachable| reachable.then_some(fact.value.target)) + }); + let first = candidates.next()?; + candidates + .all(|candidate| candidate == first) + .then_some(first) + } + + pub fn exact_reachable_target(&self) -> Option { + self.unique_reachable_target(true) + } + + pub fn actionable_reachable_target(&self) -> Option { + self.unique_reachable_target(false) + } + + pub fn exact_compiled_condition(&self) -> Option<&BackwardConditionSummary> { + self.unique_compiled_condition(true) + } + + pub fn actionable_compiled_condition(&self) -> Option<&BackwardConditionSummary> { + self.unique_compiled_condition(false) + } + + pub fn exact_compiled_condition_for_target( + &self, + target: u64, + ) -> Option<&BackwardConditionSummary> { + self.compiled_condition_for_target(target, true) + } + + pub fn actionable_compiled_condition_for_target( + &self, + target: u64, + ) -> Option<&BackwardConditionSummary> { + self.compiled_condition_for_target(target, false) + } + + pub fn exact_memory_terms(&self) -> Vec<&BackwardMemoryCondition> { + self.memory + .iter() + .filter(|term| term.evidence.allows_hard_proof()) + .map(|term| &term.value.term) + .collect() + } + + pub fn actionable_memory_terms(&self) -> Vec<&BackwardMemoryCondition> { + self.memory + .iter() + .filter(|term| term.evidence.allows_narrowing()) + .map(|term| &term.value.term) + .collect() + } + + pub fn exact_memory_terms_for_target(&self, target: u64) -> Vec<&BackwardMemoryCondition> { + self.exact_compiled_condition_for_target(target) + .map(|compiled| { + compiled + .memory_terms + .iter() + .filter(|term| term.evidence().allows_hard_proof()) + .collect() + }) + .unwrap_or_default() + } + + pub fn actionable_memory_terms_for_target(&self, target: u64) -> Vec<&BackwardMemoryCondition> { + match self.actionable_compiled_condition_for_target(target) { + Some(compiled) if !compiled.memory_terms.is_empty() => compiled + .memory_terms + .iter() + .filter(|term| term.evidence().allows_narrowing()) + .collect(), + Some(_) => Vec::new(), + None => { + if self.actionable_reachable_target() == Some(target) { + self.actionable_memory_terms() + } else { + Vec::new() + } + } + } + } + + pub fn branch_truth_for_target(&self, target: u64) -> Option { + self.control + .iter() + .find(|fact| fact.value.target == target) + .and_then(|fact| fact.value.branch_truth) + } + + pub fn supports_guarded_structuring(&self) -> bool { + let reachable_target = self + .exact_reachable_target() + .or_else(|| self.actionable_reachable_target()); + let has_condition = reachable_target + .and_then(|target| self.actionable_compiled_condition_for_target(target)) + .is_some(); + let has_memory_support = reachable_target + .map(|target| !self.actionable_memory_terms_for_target(target).is_empty()) + .unwrap_or(false); + self.control + .iter() + .any(|fact| fact.evidence.allows_guarded_structuring()) + && has_condition + && has_memory_support + } + + pub fn supports_query_guidance(&self) -> bool { + self.control + .iter() + .any(|fact| fact.evidence.allows_narrowing() && fact.value.compiled.is_some()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct NativeFunctionSummary { + pub slice_class: SliceClass, + pub closure_functions: usize, + pub helper_functions: usize, + pub derived_summaries: usize, + pub derived_diagnostics: DerivedSummaryDiagnostics, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct NativeArtifactBody { + pub summary: NativeFunctionSummary, + #[serde( + default, + skip_serializing_if = "BTreeMap::is_empty", + serialize_with = "serialize_region_map", + deserialize_with = "deserialize_region_map" + )] + pub regions: BTreeMap, +} + +fn serialize_region_map( + regions: &BTreeMap, + serializer: S, +) -> Result +where + S: Serializer, +{ + regions + .values() + .cloned() + .collect::>() + .serialize(serializer) +} + +fn deserialize_region_map<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let regions = Vec::::deserialize(deserializer)?; + Ok(regions + .into_iter() + .map(|region| (region.key(), region)) + .collect()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SemanticTargetConditionSource<'a> { + pub block_addr: u64, + pub branch_truth: bool, + pub summary: &'a BackwardConditionSummary, + pub necessary_for_target: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct SemanticTargetRegionSource<'a> { + pub region: &'a SemanticRegion, + pub branch_truth: Option, + pub summary: &'a BackwardConditionSummary, + pub necessary_for_target: bool, +} + +fn summary_strength_rank( + summary: &BackwardConditionSummary, +) -> (u8, u8, usize, usize, usize, usize) { + let strength = summary.evidence().strength_rank(); + ( + strength.0, + strength.1, + summary.memory_terms.len(), + summary.supported_paths, + usize::MAX.saturating_sub(summary.total_paths), + usize::MAX.saturating_sub(summary.simplified.len()), + ) +} + +impl NativeArtifactBody { + fn target_source_region(&self, target_addr: u64, proof_only: bool) -> Option<&SemanticRegion> { + self.authoritative_target_region_source(target_addr, proof_only) + .map(|source| source.region) + } + + fn target_memory_region_candidates(&self, target_addr: u64) -> Vec<&SemanticRegion> { + self.regions + .values() + .filter(|region| { + !region + .actionable_memory_terms_for_target(target_addr) + .is_empty() + }) + .collect() + } + + fn target_memory_sources_are_equivalent( + candidates: &[&SemanticRegion], + target_addr: u64, + ) -> bool { + let Some(first) = candidates.first() else { + return true; + }; + let first_terms = first.actionable_memory_terms_for_target(target_addr); + candidates.iter().all(|candidate| { + candidate.actionable_memory_terms_for_target(target_addr) == first_terms + }) + } + + fn authoritative_memory_region(&self, target_addr: u64) -> Option<&SemanticRegion> { + let candidates = self.target_memory_region_candidates(target_addr); + if candidates.is_empty() { + return None; + } + if !Self::target_memory_sources_are_equivalent(&candidates, target_addr) { + return None; + } + candidates.into_iter().next() + } + + fn target_region_candidates( + &self, + target_addr: u64, + proof_only: bool, + ) -> Vec> { + self.regions + .values() + .filter_map(|region| { + let necessary_for_target = if proof_only { + region.exact_reachable_target() == Some(target_addr) + } else { + region.actionable_reachable_target() == Some(target_addr) + }; + let summary = if proof_only { + region.exact_compiled_condition_for_target(target_addr) + } else { + region.actionable_compiled_condition_for_target(target_addr) + }?; + Some(SemanticTargetRegionSource { + region, + branch_truth: region.branch_truth_for_target(target_addr), + summary, + necessary_for_target, + }) + }) + .collect() + } + + fn target_sources_are_equivalent(candidates: &[SemanticTargetRegionSource<'_>]) -> bool { + let Some(first) = candidates.first() else { + return true; + }; + candidates.iter().all(|candidate| { + candidate.summary == first.summary + && !matches!( + (candidate.branch_truth, first.branch_truth), + (Some(left), Some(right)) if left != right + ) + }) + } + + fn authoritative_target_region_source( + &self, + target_addr: u64, + proof_only: bool, + ) -> Option> { + let candidates = self.target_region_candidates(target_addr, proof_only); + if candidates.is_empty() { + return None; + } + if proof_only + && candidates + .iter() + .any(|candidate| !candidate.necessary_for_target) + { + return None; + } + if !Self::target_sources_are_equivalent(&candidates) { + return None; + } + let representative = candidates.iter().copied().max_by(|left, right| { + ( + usize::from(left.necessary_for_target), + summary_strength_rank(left.summary), + usize::MAX.saturating_sub(left.region.anchor as usize), + usize::from(left.branch_truth.unwrap_or(false)), + ) + .cmp(&( + usize::from(right.necessary_for_target), + summary_strength_rank(right.summary), + usize::MAX.saturating_sub(right.region.anchor as usize), + usize::from(right.branch_truth.unwrap_or(false)), + )) + })?; + Some(SemanticTargetRegionSource { + necessary_for_target: candidates + .iter() + .all(|candidate| candidate.necessary_for_target), + ..representative + }) + } + + pub fn target_source_conflict(&self, target_addr: u64, proof_only: bool) -> bool { + let candidates = self.target_region_candidates(target_addr, proof_only); + candidates.len() > 1 && !Self::target_sources_are_equivalent(&candidates) + } + + pub fn conflicting_targets(&self, proof_only: bool) -> BTreeSet { + self.regions + .values() + .flat_map(|region| region.control.iter().map(|fact| fact.value.target)) + .filter(|target| self.target_source_conflict(*target, proof_only)) + .collect() + } + + pub fn region_for_anchor(&self, anchor: u64) -> Option<&SemanticRegion> { + self.regions.values().find(|region| region.anchor == anchor) + } + + pub fn exact_control_count(&self) -> usize { + self.regions + .values() + .flat_map(|region| region.control.iter()) + .filter(|fact| fact.evidence.allows_hard_proof()) + .count() + } + + pub fn actionable_control_count(&self) -> usize { + self.regions + .values() + .flat_map(|region| region.control.iter()) + .filter(|fact| fact.evidence.allows_narrowing()) + .count() + } + + pub fn supports_guarded_structuring(&self) -> bool { + self.regions.values().any(|region| { + region.supports_guarded_structuring() + && region + .actionable_reachable_target() + .is_some_and(|target| !self.target_source_conflict(target, false)) + }) + } + + pub fn supports_query_guidance(&self) -> bool { + self.regions + .values() + .any(SemanticRegion::supports_query_guidance) + } + + pub fn has_target_guidance(&self, target_addr: u64, proof_only: bool) -> bool { + self.target_source_region(target_addr, proof_only).is_some() + } + + pub fn target_guidance_is_necessary(&self, target_addr: u64, proof_only: bool) -> bool { + self.target_source_region(target_addr, proof_only) + .is_some_and(|region| { + if proof_only { + region.exact_reachable_target() == Some(target_addr) + } else { + region.actionable_reachable_target() == Some(target_addr) + } + }) + } + + pub fn exact_reachable_target_for_block(&self, block_addr: u64) -> Option { + self.region_for_anchor(block_addr) + .and_then(SemanticRegion::exact_reachable_target) + } + + pub fn exact_branch_truth_for_block(&self, block_addr: u64) -> Option { + let region = self.region_for_anchor(block_addr)?; + let target = region.exact_reachable_target()?; + region.branch_truth_for_target(target) + } + + pub fn actionable_reachable_target_for_block(&self, block_addr: u64) -> Option { + self.region_for_anchor(block_addr) + .and_then(SemanticRegion::actionable_reachable_target) + } + + pub fn exact_compiled_condition_for_block( + &self, + block_addr: u64, + ) -> Option<&BackwardConditionSummary> { + self.region_for_anchor(block_addr) + .and_then(SemanticRegion::exact_compiled_condition) + } + + pub fn actionable_compiled_condition_for_block( + &self, + block_addr: u64, + ) -> Option<&BackwardConditionSummary> { + self.region_for_anchor(block_addr) + .and_then(SemanticRegion::actionable_compiled_condition) + } + + pub fn actionable_memory_terms_for_block( + &self, + block_addr: u64, + ) -> Vec<&BackwardMemoryCondition> { + self.region_for_anchor(block_addr) + .map(SemanticRegion::actionable_memory_terms) + .unwrap_or_default() + } + + pub fn actionable_regions(&self) -> impl Iterator { + self.regions.values().filter(|region| { + region.supports_guarded_structuring() + && region + .actionable_reachable_target() + .is_some_and(|target| !self.target_source_conflict(target, false)) + }) + } + + pub fn target_condition_source( + &self, + target_addr: u64, + proof_only: bool, + ) -> Option> { + let representative = self.authoritative_target_region_source(target_addr, proof_only)?; + let branch_truth = representative.branch_truth?; + Some(SemanticTargetConditionSource { + block_addr: representative.region.anchor, + branch_truth, + summary: representative.summary, + necessary_for_target: representative.necessary_for_target, + }) + } + + pub fn actionable_compiled_condition_for_target( + &self, + target_addr: u64, + ) -> Option<&BackwardConditionSummary> { + self.target_source_region(target_addr, false)? + .actionable_compiled_condition_for_target(target_addr) + } + + pub fn exact_compiled_condition_for_target( + &self, + target_addr: u64, + ) -> Option<&BackwardConditionSummary> { + self.target_source_region(target_addr, true)? + .exact_compiled_condition_for_target(target_addr) + } + + pub fn actionable_memory_terms_for_target( + &self, + target_addr: u64, + ) -> Vec<&BackwardMemoryCondition> { + self.authoritative_memory_region(target_addr) + .map(|region| region.actionable_memory_terms_for_target(target_addr)) + .unwrap_or_default() + } + + pub fn authoritative_region_for_target( + &self, + target_addr: u64, + proof_only: bool, + ) -> Option<&SemanticRegion> { + self.target_source_region(target_addr, proof_only) + } + + pub fn authoritative_memory_region_for_target( + &self, + target_addr: u64, + ) -> Option<&SemanticRegion> { + self.authoritative_memory_region(target_addr) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VmArtifactBody { + #[serde(skip_serializing_if = "Option::is_none")] + pub interpreter: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub step_summary: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub transfer_summary: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SemanticArtifactDiagnostics { + pub branches_evaluated: usize, + pub branches_pruned: usize, + pub branches_unknown: usize, + pub skipped_missing_arch: bool, + pub skipped_large_cfg: bool, + pub residual_reasons: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub ambiguous_targets: Vec, + pub cache_hit: bool, +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use proptest::prelude::*; + + use super::{ + ControlFact, Judged, MemoryFact, NativeArtifactBody, NativeFunctionSummary, RegionKey, + SemanticRegion, TargetFact, + }; + use crate::sim::DerivedSummaryDiagnostics; + use crate::{ + BackwardConditionPrecision, BackwardConditionSummary, BackwardMemoryCondition, + BackwardMemoryRegion, SemanticEvidence, SliceClass, SymbolicReachabilityStatus, + }; + + fn compiled_summary(tag: u64) -> BackwardConditionSummary { + BackwardConditionSummary { + simplified: format!("cond_{tag}"), + terms: vec![format!("term_{tag}")], + memory_terms: vec![BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: tag as i64, + offset_hi: tag as i64, + size: 8, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: Some(format!("arg0_{tag}")), + expr: format!("*(arg0 + 0x{tag:x})"), + value_expr: None, + exact_value: false, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + } + } + + fn worker_body(regions: impl IntoIterator) -> NativeArtifactBody { + NativeArtifactBody { + summary: NativeFunctionSummary { + slice_class: SliceClass::Worker, + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: DerivedSummaryDiagnostics::default(), + }, + regions: regions + .into_iter() + .map(|region| { + ( + RegionKey::new(region.anchor, region.frontier.clone()), + region, + ) + }) + .collect(), + } + } + + fn guided_region(anchor: u64, target: u64, branch_truth: bool) -> SemanticRegion { + let compiled = compiled_summary(anchor ^ target); + SemanticRegion { + anchor, + frontier: BTreeSet::from([target]), + control: vec![Judged::new( + ControlFact { + target, + status: SymbolicReachabilityStatus::Reachable, + branch_truth: Some(branch_truth), + condition: Some(compiled.simplified.clone()), + compiled: Some(compiled.clone()), + }, + SemanticEvidence::exact(), + )], + memory: compiled + .memory_terms + .iter() + .cloned() + .map(|term| Judged::new(MemoryFact { term }, SemanticEvidence::exact())) + .collect(), + pre: Vec::new(), + post: Vec::new(), + targets: vec![Judged::new( + TargetFact { + target, + status: SymbolicReachabilityStatus::Reachable, + branch_truth: Some(branch_truth), + }, + SemanticEvidence::exact(), + )], + } + } + + proptest! { + #[test] + fn conflicting_targets_are_reported_deterministically( + targets in proptest::collection::vec(0x401000u64..0x401100, 1..6), + ) { + let regions = targets + .iter() + .enumerate() + .flat_map(|(idx, target)| { + [ + guided_region(0x5000 + (idx as u64) * 2, *target, true), + guided_region(0x5001 + (idx as u64) * 2, *target, false), + ] + }) + .collect::>(); + let reversed = regions.iter().cloned().rev().collect::>(); + let expected = targets + .iter() + .copied() + .collect::>() + .into_iter() + .collect::>(); + + let forward = worker_body(regions); + let backward = worker_body(reversed); + + prop_assert_eq!( + forward.conflicting_targets(false).into_iter().collect::>(), + expected + ); + prop_assert_eq!(forward.conflicting_targets(false), backward.conflicting_targets(false)); + } + } + + #[test] + fn conflicting_target_sources_disable_guidance_and_structuring() { + let body = worker_body([ + guided_region(0x401000, 0x401100, true), + guided_region(0x401010, 0x401100, false), + ]); + + assert!(body.target_source_conflict(0x401100, false)); + assert!(!body.has_target_guidance(0x401100, false)); + assert!(!body.supports_guarded_structuring()); + } +} diff --git a/crates/r2sym/src/semantics/vm.rs b/crates/r2sym/src/semantics/vm.rs new file mode 100644 index 0000000..d01f76d --- /dev/null +++ b/crates/r2sym/src/semantics/vm.rs @@ -0,0 +1,1592 @@ +use std::collections::{BTreeMap, BTreeSet, VecDeque}; + +use r2ssa::cfg::BlockTerminator; +use r2ssa::{CFGEdge, ObjectKind, SSAOp, SSAVar, SsaArtifact, StackAddressBase}; +use serde::{Deserialize, Serialize}; + +use super::{ + SemanticConfidence, SemanticEvidence, SemanticEvidenceAmbiguity, SemanticEvidenceCoverage, + SemanticEvidenceProvenance, SemanticEvidenceReason, +}; +use crate::MemoryRegionKind; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum InterpreterKind { + SwitchDispatch, + IndirectDispatch, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct InterpreterDispatchSummary { + pub kind: InterpreterKind, + pub dispatch_header: u64, + pub dispatch_targets: usize, + pub selector: Option, + pub back_edges: usize, + pub score: i32, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum VmUnaryOp { + Neg, + BitNot, + BoolNot, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum VmBinaryOp { + Add, + Sub, + Mul, + Div, + Rem, + And, + Or, + Xor, + Shl, + LShr, + AShr, + Eq, + Ne, + Lt, + SLt, + Le, + SLe, + BoolAnd, + BoolOr, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum VmValueExpr { + Const(u64), + Var(String), + Unary { + op: VmUnaryOp, + arg: Box, + }, + Binary { + op: VmBinaryOp, + lhs: Box, + rhs: Box, + }, + Expr(String), +} + +impl VmValueExpr { + fn render_unary(op: &VmUnaryOp) -> &'static str { + match op { + VmUnaryOp::Neg => "-", + VmUnaryOp::BitNot => "~", + VmUnaryOp::BoolNot => "!", + } + } + + fn render_binary(op: &VmBinaryOp) -> &'static str { + match op { + VmBinaryOp::Add => "+", + VmBinaryOp::Sub => "-", + VmBinaryOp::Mul => "*", + VmBinaryOp::Div => "/", + VmBinaryOp::Rem => "%", + VmBinaryOp::And => "&", + VmBinaryOp::Or => "|", + VmBinaryOp::Xor => "^", + VmBinaryOp::Shl => "<<", + VmBinaryOp::LShr | VmBinaryOp::AShr => ">>", + VmBinaryOp::Eq => "==", + VmBinaryOp::Ne => "!=", + VmBinaryOp::Lt | VmBinaryOp::SLt => "<", + VmBinaryOp::Le | VmBinaryOp::SLe => "<=", + VmBinaryOp::BoolAnd => "&&", + VmBinaryOp::BoolOr => "||", + } + } + + fn render(&self) -> String { + match self { + Self::Const(value) => format!("0x{value:x}"), + Self::Var(name) | Self::Expr(name) => name.clone(), + Self::Unary { op, arg } => { + format!("({}{})", Self::render_unary(op), arg.render()) + } + Self::Binary { op, lhs, rhs } => format!( + "({} {} {})", + lhs.render(), + Self::render_binary(op), + rhs.render() + ), + } + } + + fn is_exact(&self) -> bool { + match self { + Self::Const(_) | Self::Var(_) => true, + Self::Unary { arg, .. } => arg.is_exact(), + Self::Binary { lhs, rhs, .. } => lhs.is_exact() && rhs.is_exact(), + Self::Expr(_) => false, + } + } + + fn lookup_binding(bindings: &BTreeMap, name: &str) -> Option { + bindings.get(name).copied().or_else(|| { + bindings + .iter() + .find_map(|(candidate, value)| same_logical_name(candidate, name).then_some(*value)) + }) + } + + pub(crate) fn evaluate_u64(&self, bindings: &BTreeMap) -> Option { + match self { + Self::Const(value) => Some(*value), + Self::Var(name) => Self::lookup_binding(bindings, name), + Self::Unary { op, arg } => { + let value = arg.evaluate_u64(bindings)?; + Some(match op { + VmUnaryOp::Neg => value.wrapping_neg(), + VmUnaryOp::BitNot => !value, + VmUnaryOp::BoolNot => u64::from(value == 0), + }) + } + Self::Binary { op, lhs, rhs } => { + let lhs = lhs.evaluate_u64(bindings)?; + let rhs = rhs.evaluate_u64(bindings)?; + Some(match op { + VmBinaryOp::Add => lhs.wrapping_add(rhs), + VmBinaryOp::Sub => lhs.wrapping_sub(rhs), + VmBinaryOp::Mul => lhs.wrapping_mul(rhs), + VmBinaryOp::Div => lhs.checked_div(rhs)?, + VmBinaryOp::Rem => lhs.checked_rem(rhs)?, + VmBinaryOp::And => lhs & rhs, + VmBinaryOp::Or => lhs | rhs, + VmBinaryOp::Xor => lhs ^ rhs, + VmBinaryOp::Shl => lhs.wrapping_shl((rhs & 63) as u32), + VmBinaryOp::LShr => lhs.wrapping_shr((rhs & 63) as u32), + VmBinaryOp::AShr => ((lhs as i64) >> ((rhs & 63) as u32)) as u64, + VmBinaryOp::Eq => u64::from(lhs == rhs), + VmBinaryOp::Ne => u64::from(lhs != rhs), + VmBinaryOp::Lt => u64::from(lhs < rhs), + VmBinaryOp::SLt => u64::from((lhs as i64) < (rhs as i64)), + VmBinaryOp::Le => u64::from(lhs <= rhs), + VmBinaryOp::SLe => u64::from((lhs as i64) <= (rhs as i64)), + VmBinaryOp::BoolAnd => u64::from(lhs != 0 && rhs != 0), + VmBinaryOp::BoolOr => u64::from(lhs != 0 || rhs != 0), + }) + } + Self::Expr(_) => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VmGuardCondition { + pub expr: String, + pub value: VmValueExpr, + pub expect_nonzero: bool, + pub exact: bool, +} + +impl VmGuardCondition { + pub(crate) fn evaluate(&self, bindings: &BTreeMap) -> Option { + let value = self.value.evaluate_u64(bindings)?; + Some((value != 0) == self.expect_nonzero) + } + + pub fn evidence(&self) -> SemanticEvidence { + if self.exact { + SemanticEvidence::exact() + } else if !matches!(self.value, VmValueExpr::Expr(_)) { + SemanticEvidence::likely(SemanticEvidenceReason::GuardOpaque) + .with_provenance(SemanticEvidenceProvenance::Normalized) + } else { + SemanticEvidence::heuristic(SemanticEvidenceReason::GuardOpaque) + .with_provenance(SemanticEvidenceProvenance::Ranked) + } + } + + pub fn confidence(&self) -> SemanticConfidence { + self.evidence().tier + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VmGuardedExit { + pub target: u64, + pub guard: VmGuardCondition, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VmMemoryRegionRef { + pub id: u32, + pub kind: MemoryRegionKind, + pub name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VmMemoryCondition { + pub region: VmMemoryRegionRef, + pub offset_lo: i64, + pub offset_hi: i64, + pub size: u32, + pub exact_offset: bool, + pub binding: Option, + pub expr: String, + pub value_expr: Option, + pub value: Option, + pub exact_value: bool, +} + +impl VmMemoryCondition { + pub fn evidence(&self) -> SemanticEvidence { + if self.exact_offset && (self.value.is_none() || self.exact_value) { + SemanticEvidence::exact() + } else if self.binding.is_some() || self.exact_offset { + let reason = if self.exact_offset { + SemanticEvidenceReason::ValueOpaque + } else { + SemanticEvidenceReason::AliasAmbiguity + }; + SemanticEvidence::likely(reason) + .with_provenance(SemanticEvidenceProvenance::Normalized) + .with_ambiguity(if self.exact_offset { + SemanticEvidenceAmbiguity::Single + } else { + SemanticEvidenceAmbiguity::Bounded + }) + } else { + SemanticEvidence::heuristic(SemanticEvidenceReason::AliasAmbiguity) + .with_coverage(SemanticEvidenceCoverage::Bounded) + } + } + + pub fn confidence(&self) -> SemanticConfidence { + self.evidence().tier + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VmStateUpdate { + pub output: String, + pub expr: String, + pub value: VmValueExpr, + pub exact: bool, +} + +impl VmStateUpdate { + pub fn evidence(&self) -> SemanticEvidence { + if self.exact { + SemanticEvidence::exact() + } else if !matches!(self.value, VmValueExpr::Expr(_)) { + SemanticEvidence::likely(SemanticEvidenceReason::ValueOpaque) + .with_provenance(SemanticEvidenceProvenance::Normalized) + } else { + SemanticEvidence::heuristic(SemanticEvidenceReason::ValueOpaque) + .with_provenance(SemanticEvidenceProvenance::Ranked) + } + } + + pub fn confidence(&self) -> SemanticConfidence { + self.evidence().tier + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VmTransferArm { + pub handler_target: u64, + pub case_values: Vec, + pub region_blocks: Vec, + pub exit_targets: Vec, + pub exit_guards: Vec, + pub state_updates: Vec, + pub selector_update: Option, + pub memory_reads: Vec, + pub memory_writes: Vec, + pub residual_guards: bool, + pub residual_memory_effects: bool, + pub exact: bool, + pub redispatch: bool, + pub may_return: bool, + pub truncated: bool, +} + +impl VmTransferArm { + pub fn evidence(&self) -> SemanticEvidence { + if self.exact { + return SemanticEvidence::exact(); + } + if self.truncated { + return if self.case_values.is_empty() + && self.exit_targets.is_empty() + && self.state_updates.is_empty() + && self.memory_reads.is_empty() + && self.memory_writes.is_empty() + { + SemanticEvidence::residual(SemanticEvidenceReason::TruncatedTransfer) + .with_coverage(SemanticEvidenceCoverage::Partial) + .with_budget_limited(true) + } else { + SemanticEvidence::heuristic(SemanticEvidenceReason::TruncatedTransfer) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_budget_limited(true) + }; + } + if !self.residual_guards + && !self.residual_memory_effects + && self + .state_updates + .iter() + .all(|update| update.evidence().is_reliable()) + && self + .selector_update + .as_ref() + .is_none_or(|update| update.evidence().is_reliable()) + && self + .exit_guards + .iter() + .all(|guard| guard.guard.evidence().is_reliable()) + && self + .memory_reads + .iter() + .all(|effect| effect.evidence().is_reliable()) + && self + .memory_writes + .iter() + .all(|effect| effect.evidence().is_reliable()) + { + let mut evidence = + SemanticEvidence::likely(SemanticEvidenceReason::PartialPathCoverage) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized); + if self.redispatch || self.may_return { + evidence = evidence.with_coverage(SemanticEvidenceCoverage::Partial); + } + return evidence; + } + if self.case_values.is_empty() + && self.exit_targets.is_empty() + && self.state_updates.is_empty() + && self.memory_reads.is_empty() + && self.memory_writes.is_empty() + { + SemanticEvidence::residual(SemanticEvidenceReason::PartialPathCoverage) + } else { + let mut evidence = + SemanticEvidence::heuristic(SemanticEvidenceReason::DerivedFromRanking) + .with_coverage(SemanticEvidenceCoverage::Bounded); + if self.residual_guards { + evidence = evidence.with_reason(SemanticEvidenceReason::GuardOpaque); + } + if self.residual_memory_effects { + evidence = evidence.with_reason(SemanticEvidenceReason::AliasAmbiguity); + } + evidence + } + } + + pub fn confidence(&self) -> SemanticConfidence { + self.evidence().tier + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VmStepSummary { + pub kind: InterpreterKind, + pub loop_header: u64, + pub dispatch_header: u64, + pub selector: Option, + pub dispatch_targets: Vec, + pub default_target: Option, + pub case_values_by_target: BTreeMap>, + pub loop_latches: Vec, + pub state_inputs: Vec, + pub state_outputs: Vec, + pub step_blocks: Vec, + pub handler_regions: BTreeMap>, + pub handler_state_inputs: BTreeMap>, + pub handler_state_outputs: BTreeMap>, + pub handler_state_updates: BTreeMap>, + pub handler_exit_guards: BTreeMap>, + pub handler_memory_read_effects: BTreeMap>, + pub handler_memory_write_effects: BTreeMap>, + pub handler_memory_reads: BTreeMap, + pub handler_memory_writes: BTreeMap, + pub handler_calls: BTreeMap, + pub handler_conditional_branches: BTreeMap, + pub handler_exit_targets: BTreeMap>, + pub redispatch_handlers: Vec, + pub returning_handlers: Vec, + pub truncated_handlers: Vec, + pub transfers: Vec, +} + +const MAX_HANDLER_REGION_BLOCKS: usize = 16; +const MAX_HANDLER_REGION_DEPTH: usize = 8; + +#[derive(Debug, Default)] +struct HandlerRegionSummary { + blocks: Vec, + state_inputs: Vec, + state_outputs: Vec, + state_updates: Vec, + exit_guards: Vec, + memory_read_effects: Vec, + memory_write_effects: Vec, + memory_reads: usize, + memory_writes: usize, + calls: usize, + conditional_branches: usize, + exit_targets: Vec, + reenters_dispatch: bool, + may_return: bool, + truncated: bool, + residual_guards: bool, + residual_memory_effects: bool, +} + +fn record_block_state( + block: &r2ssa::function::SSABlock, + state_inputs: &mut BTreeSet, + state_outputs: &mut BTreeSet, +) { + block.for_each_source(|src| { + if (!src.var.is_register() && !src.var.name.starts_with("ram:")) || src.var.version != 0 { + return; + } + state_inputs.insert(src.var.display_name()); + }); + for phi in &block.phis { + if !phi.dst.is_const() && !phi.dst.is_temp() && !phi.dst.name.starts_with("ram:") { + state_outputs.insert(phi.dst.display_name()); + } + } + for op in &block.ops { + let Some(dst) = op.dst() else { + continue; + }; + if dst.is_const() || dst.is_temp() || dst.name.starts_with("ram:") { + continue; + } + state_outputs.insert(dst.display_name()); + } +} + +fn case_values_by_target( + func: &SsaArtifact, + dispatch_header: u64, +) -> (BTreeMap>, Option) { + let Some((cases, default_target)) = func.function().switch_info(dispatch_header) else { + return (BTreeMap::new(), None); + }; + let mut case_values_by_target = BTreeMap::>::new(); + for (value, target) in cases { + case_values_by_target.entry(target).or_default().push(value); + } + for values in case_values_by_target.values_mut() { + values.sort_unstable(); + values.dedup(); + } + (case_values_by_target, default_target) +} + +fn display_const(name: &str) -> Option { + let hex = name.strip_prefix("const:")?; + let digits = hex.strip_prefix("0x").unwrap_or(hex); + let value = u64::from_str_radix(digits, 16).ok()?; + Some(format!("0x{value:x}")) +} + +fn split_version(name: &str) -> (&str, Option<&str>) { + name.rsplit_once('_') + .filter(|(_, version)| version.chars().all(|ch| ch.is_ascii_digit())) + .map_or((name, None), |(base, version)| (base, Some(version))) +} + +fn same_logical_name(left: &str, right: &str) -> bool { + split_version(left) + .0 + .eq_ignore_ascii_case(split_version(right).0) +} + +fn render_vm_var_expr(func: &SsaArtifact, var: &SSAVar, depth: u32) -> String { + if depth > 4 { + return var.display_name(); + } + if var.is_const() { + return display_const(&var.name).unwrap_or_else(|| var.display_name()); + } + let Some(value_id) = func.graph().value_id_for_var(var) else { + return var.display_name(); + }; + let Some(inst_id) = func.graph().def_inst(value_id) else { + return var.display_name(); + }; + let Some(inst) = func.graph().inst(inst_id) else { + return var.display_name(); + }; + let r2ssa::graph::InstPayload::Op(op) = &inst.payload else { + return var.display_name(); + }; + render_vm_op_expr(func, op, depth + 1).unwrap_or_else(|| var.display_name()) +} + +fn classify_vm_var_value(func: &SsaArtifact, var: &SSAVar, depth: u32) -> VmValueExpr { + if depth > 4 { + return VmValueExpr::Var(var.display_name()); + } + if var.is_const() { + return display_const(&var.name) + .and_then(|_| { + let hex = var.name.strip_prefix("const:")?; + let digits = hex.strip_prefix("0x").unwrap_or(hex); + u64::from_str_radix(digits, 16).ok() + }) + .map(VmValueExpr::Const) + .unwrap_or_else(|| VmValueExpr::Expr(var.display_name())); + } + let Some(value_id) = func.graph().value_id_for_var(var) else { + return VmValueExpr::Var(var.display_name()); + }; + let Some(inst_id) = func.graph().def_inst(value_id) else { + return VmValueExpr::Var(var.display_name()); + }; + let Some(inst) = func.graph().inst(inst_id) else { + return VmValueExpr::Var(var.display_name()); + }; + let r2ssa::graph::InstPayload::Op(op) = &inst.payload else { + return VmValueExpr::Var(var.display_name()); + }; + if let Some((block_addr, op_idx)) = func.inst_op_site(inst_id) + && let Some(value) = classify_vm_op_value_at_site(func, block_addr, op_idx, op, depth + 1) + { + return value; + } + classify_vm_op_value(func, op, depth + 1) + .unwrap_or_else(|| VmValueExpr::Var(var.display_name())) +} + +fn render_vm_binary_expr( + func: &SsaArtifact, + a: &SSAVar, + op: &str, + b: &SSAVar, + depth: u32, +) -> String { + format!( + "({} {} {})", + render_vm_var_expr(func, a, depth + 1), + op, + render_vm_var_expr(func, b, depth + 1) + ) +} + +fn render_vm_op_expr(func: &SsaArtifact, op: &SSAOp, depth: u32) -> Option { + use SSAOp::*; + + Some(match op { + Copy { src, .. } | IntZExt { src, .. } | IntSExt { src, .. } | Cast { src, .. } => { + render_vm_var_expr(func, src, depth + 1) + } + Load { addr, .. } => format!("*{}", render_vm_var_expr(func, addr, depth + 1)), + IntAdd { a, b, .. } | FloatAdd { a, b, .. } => { + render_vm_binary_expr(func, a, "+", b, depth + 1) + } + IntSub { a, b, .. } | FloatSub { a, b, .. } => { + render_vm_binary_expr(func, a, "-", b, depth + 1) + } + IntMult { a, b, .. } | FloatMult { a, b, .. } => { + render_vm_binary_expr(func, a, "*", b, depth + 1) + } + IntDiv { a, b, .. } | IntSDiv { a, b, .. } | FloatDiv { a, b, .. } => { + render_vm_binary_expr(func, a, "/", b, depth + 1) + } + IntRem { a, b, .. } | IntSRem { a, b, .. } => { + render_vm_binary_expr(func, a, "%", b, depth + 1) + } + IntAnd { a, b, .. } => render_vm_binary_expr(func, a, "&", b, depth + 1), + IntOr { a, b, .. } => render_vm_binary_expr(func, a, "|", b, depth + 1), + IntXor { a, b, .. } | BoolXor { a, b, .. } => { + render_vm_binary_expr(func, a, "^", b, depth + 1) + } + IntLeft { a, b, .. } => render_vm_binary_expr(func, a, "<<", b, depth + 1), + IntRight { a, b, .. } | IntSRight { a, b, .. } => { + render_vm_binary_expr(func, a, ">>", b, depth + 1) + } + IntEqual { a, b, .. } | FloatEqual { a, b, .. } => { + render_vm_binary_expr(func, a, "==", b, depth + 1) + } + IntNotEqual { a, b, .. } | FloatNotEqual { a, b, .. } => { + render_vm_binary_expr(func, a, "!=", b, depth + 1) + } + IntLess { a, b, .. } | IntSLess { a, b, .. } | FloatLess { a, b, .. } => { + render_vm_binary_expr(func, a, "<", b, depth + 1) + } + IntLessEqual { a, b, .. } | IntSLessEqual { a, b, .. } | FloatLessEqual { a, b, .. } => { + render_vm_binary_expr(func, a, "<=", b, depth + 1) + } + BoolAnd { a, b, .. } => render_vm_binary_expr(func, a, "&&", b, depth + 1), + BoolOr { a, b, .. } => render_vm_binary_expr(func, a, "||", b, depth + 1), + IntNegate { src, .. } | FloatNeg { src, .. } => { + format!("(-{})", render_vm_var_expr(func, src, depth + 1)) + } + IntNot { src, .. } => format!("(~{})", render_vm_var_expr(func, src, depth + 1)), + BoolNot { src, .. } => format!("(!{})", render_vm_var_expr(func, src, depth + 1)), + PtrAdd { + base, + index, + element_size, + .. + } => format!( + "({} + ({} * {}))", + render_vm_var_expr(func, base, depth + 1), + render_vm_var_expr(func, index, depth + 1), + element_size + ), + PtrSub { + base, + index, + element_size, + .. + } => format!( + "({} - ({} * {}))", + render_vm_var_expr(func, base, depth + 1), + render_vm_var_expr(func, index, depth + 1), + element_size + ), + Piece { hi, lo, .. } => format!( + "piece({}, {})", + render_vm_var_expr(func, hi, depth + 1), + render_vm_var_expr(func, lo, depth + 1) + ), + Subpiece { src, offset, .. } => format!( + "subpiece({}, {})", + render_vm_var_expr(func, src, depth + 1), + offset + ), + _ => return None, + }) +} + +fn classify_vm_op_value(func: &SsaArtifact, op: &SSAOp, depth: u32) -> Option { + use SSAOp::*; + + Some(match op { + Copy { src, .. } | IntZExt { src, .. } | IntSExt { src, .. } | Cast { src, .. } => { + classify_vm_var_value(func, src, depth + 1) + } + IntAdd { a, b, .. } | FloatAdd { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Add, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntSub { a, b, .. } | FloatSub { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Sub, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntMult { a, b, .. } | FloatMult { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Mul, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntDiv { a, b, .. } | IntSDiv { a, b, .. } | FloatDiv { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Div, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntRem { a, b, .. } | IntSRem { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Rem, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntAnd { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::And, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntOr { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Or, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntXor { a, b, .. } | BoolXor { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Xor, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntLeft { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Shl, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntRight { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::LShr, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntSRight { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::AShr, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntEqual { a, b, .. } | FloatEqual { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Eq, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntNotEqual { a, b, .. } | FloatNotEqual { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Ne, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntLess { a, b, .. } | FloatLess { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Lt, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntSLess { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::SLt, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntLessEqual { a, b, .. } | FloatLessEqual { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Le, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntSLessEqual { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::SLe, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + BoolAnd { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::BoolAnd, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + BoolOr { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::BoolOr, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntNegate { src, .. } | FloatNeg { src, .. } => VmValueExpr::Unary { + op: VmUnaryOp::Neg, + arg: Box::new(classify_vm_var_value(func, src, depth + 1)), + }, + IntNot { src, .. } => VmValueExpr::Unary { + op: VmUnaryOp::BitNot, + arg: Box::new(classify_vm_var_value(func, src, depth + 1)), + }, + BoolNot { src, .. } => VmValueExpr::Unary { + op: VmUnaryOp::BoolNot, + arg: Box::new(classify_vm_var_value(func, src, depth + 1)), + }, + _ => VmValueExpr::Expr(render_vm_op_expr(func, op, depth + 1)?), + }) +} + +fn classify_vm_value_id(func: &SsaArtifact, value_id: r2ssa::ValueId, depth: u32) -> VmValueExpr { + if let Some(var) = func.value_var(value_id) { + return classify_vm_var_value(func, var, depth); + } + VmValueExpr::Expr(format!("value:{value_id:?}")) +} + +fn stack_base_name(base: StackAddressBase) -> &'static str { + match base { + StackAddressBase::FramePointer => "fp", + StackAddressBase::StackPointer => "sp", + } +} + +fn vm_memory_binding_name(region: &VmMemoryRegionRef, offset: i64, size: u32) -> String { + format!("mem:r{}:{offset}:{size}", region.id) +} + +fn vm_memory_region_ref_from_object( + func: &SsaArtifact, + object_id: r2ssa::ObjectId, +) -> Option { + let object = func.objects().object(object_id)?; + let (kind, name) = match &object.kind { + ObjectKind::StackSlot { base, offset } | ObjectKind::FrameObject { base, offset } => ( + MemoryRegionKind::Stack, + format!("stack:{}{:+#x}", stack_base_name(*base), offset), + ), + ObjectKind::Global { space, address } => { + (MemoryRegionKind::Global, format!("{space}:0x{address:x}")) + } + ObjectKind::HeapAlloc { call_site } => ( + MemoryRegionKind::Heap, + format!("heap_alloc@{}", call_site.0), + ), + ObjectKind::EscapedUnknown => return None, + }; + Some(VmMemoryRegionRef { + id: object_id.0, + kind, + name, + }) +} + +fn render_memory_access_expr(op: &SSAOp, func: &SsaArtifact) -> String { + match op { + SSAOp::Load { addr, .. } + | SSAOp::Store { addr, .. } + | SSAOp::LoadLinked { addr, .. } + | SSAOp::StoreConditional { addr, .. } + | SSAOp::AtomicCAS { addr, .. } + | SSAOp::LoadGuarded { addr, .. } + | SSAOp::StoreGuarded { addr, .. } => render_vm_var_expr(func, addr, 0), + _ => "".to_string(), + } +} + +fn vm_memory_write_value(op: &SSAOp, func: &SsaArtifact) -> (Option, bool) { + match op { + SSAOp::Store { val, .. } => { + let value = classify_vm_var_value(func, val, 0); + let exact = value.is_exact(); + (Some(value), exact) + } + _ => (None, false), + } +} + +fn classify_vm_op_value_at_site( + func: &SsaArtifact, + block_addr: u64, + op_idx: usize, + op: &SSAOp, + depth: u32, +) -> Option { + match op { + SSAOp::Load { .. } | SSAOp::LoadLinked { .. } => { + let (reads, _writes, residual) = + vm_memory_conditions_for_op(func, block_addr, op_idx, op); + if !residual + && let [read] = reads.as_slice() + && let Some(binding) = read.binding.as_ref() + { + return Some(VmValueExpr::Var(binding.clone())); + } + classify_vm_op_value(func, op, depth) + } + _ => classify_vm_op_value(func, op, depth), + } +} + +fn vm_memory_conditions_for_op( + func: &SsaArtifact, + block_addr: u64, + op_idx: usize, + op: &SSAOp, +) -> (Vec, Vec, bool) { + let expr = render_memory_access_expr(op, func); + let mut reads = Vec::new(); + let mut writes = Vec::new(); + let mut residual = false; + + if op.is_memory_read() { + let Some(uses) = func.memory_uses_for_op_site(block_addr, op_idx) else { + residual = true; + return (reads, writes, residual); + }; + for use_fact in uses { + let Some(region) = vm_memory_region_ref_from_object(func, use_fact.location.object) + else { + residual = true; + continue; + }; + let binding = Some(vm_memory_binding_name( + ®ion, + use_fact.location.offset, + use_fact.location.size, + )); + reads.push(VmMemoryCondition { + region, + offset_lo: use_fact.location.offset, + offset_hi: use_fact.location.offset, + size: use_fact.location.size, + exact_offset: true, + binding, + expr: expr.clone(), + value_expr: None, + value: None, + exact_value: false, + }); + } + } + + if op.is_memory_write() { + let Some(defs) = func.memory_defs_for_op_site(block_addr, op_idx) else { + residual = true; + return (reads, writes, residual); + }; + let (write_value, exact_value) = vm_memory_write_value(op, func); + if matches!( + op, + SSAOp::StoreConditional { .. } | SSAOp::StoreGuarded { .. } | SSAOp::AtomicCAS { .. } + ) { + residual = true; + } + for def in defs { + let Some(region) = vm_memory_region_ref_from_object(func, def.location.object) else { + residual = true; + continue; + }; + let binding = Some(vm_memory_binding_name( + ®ion, + def.location.offset, + def.location.size, + )); + writes.push(VmMemoryCondition { + region, + offset_lo: def.location.offset, + offset_hi: def.location.offset, + size: def.location.size, + exact_offset: true, + binding, + expr: expr.clone(), + value_expr: write_value.as_ref().map(VmValueExpr::render), + value: write_value.clone(), + exact_value, + }); + } + } + + reads.sort_by(|lhs, rhs| { + ( + lhs.region.id, + &lhs.region.name, + lhs.offset_lo, + lhs.size, + lhs.binding.as_deref(), + &lhs.expr, + ) + .cmp(&( + rhs.region.id, + &rhs.region.name, + rhs.offset_lo, + rhs.size, + rhs.binding.as_deref(), + &rhs.expr, + )) + }); + reads.dedup(); + writes.sort_by(|lhs, rhs| { + ( + lhs.region.id, + &lhs.region.name, + lhs.offset_lo, + lhs.size, + lhs.binding.as_deref(), + &lhs.expr, + lhs.value_expr.as_deref(), + lhs.exact_value, + ) + .cmp(&( + rhs.region.id, + &rhs.region.name, + rhs.offset_lo, + rhs.size, + rhs.binding.as_deref(), + &rhs.expr, + rhs.value_expr.as_deref(), + rhs.exact_value, + )) + }); + writes.dedup(); + + (reads, writes, residual) +} + +fn vm_exit_guards_for_block( + func: &SsaArtifact, + block_addr: u64, + seen_targets: &mut BTreeSet<(u64, bool, String)>, +) -> (Vec, bool) { + let Some(predicate) = func + .predicates() + .predicates + .values() + .find(|fact| fact.block_addr == block_addr) + else { + return (Vec::new(), true); + }; + let value = classify_vm_value_id(func, predicate.condition, 0); + let base_expr = value.render(); + let exact = value.is_exact(); + let true_expr = base_expr.clone(); + let false_expr = format!("!({base_expr})"); + let mut guards = Vec::new(); + for (target, expect_nonzero, expr) in [ + (predicate.true_target, true, true_expr), + (predicate.false_target, false, false_expr), + ] { + if seen_targets.insert((target, expect_nonzero, expr.clone())) { + guards.push(VmGuardedExit { + target, + guard: VmGuardCondition { + expr, + value: value.clone(), + expect_nonzero, + exact, + }, + }); + } + } + (guards, false) +} + +fn summarize_handler_region( + func: &SsaArtifact, + entry: u64, + dispatch_header: u64, + loop_header: u64, + dispatch_targets: &BTreeSet, +) -> HandlerRegionSummary { + let mut visited = BTreeSet::new(); + let mut queue = VecDeque::from([(entry, 0usize)]); + let mut exit_targets = BTreeSet::new(); + let mut state_inputs = BTreeSet::new(); + let mut state_outputs = BTreeSet::new(); + let mut state_updates = BTreeMap::::new(); + let mut exit_guards = Vec::new(); + let mut seen_exit_guards = BTreeSet::new(); + let mut memory_read_effects = Vec::new(); + let mut memory_write_effects = Vec::new(); + let mut memory_reads = 0usize; + let mut memory_writes = 0usize; + let mut calls = 0usize; + let mut conditional_branches = 0usize; + let mut reenters_dispatch = false; + let mut may_return = false; + let mut truncated = false; + let mut residual_guards = false; + let mut residual_memory_effects = false; + + while let Some((block_addr, depth)) = queue.pop_front() { + if !visited.insert(block_addr) { + continue; + } + if visited.len() > MAX_HANDLER_REGION_BLOCKS { + truncated = true; + visited.remove(&block_addr); + continue; + } + + let Some(block) = func.get_block(block_addr) else { + continue; + }; + let cfg_block = func.cfg().get_block(block_addr); + if matches!( + cfg_block.map(|block| &block.terminator), + Some(BlockTerminator::ConditionalBranch { .. }) + ) { + conditional_branches += 1; + let (block_guards, residual) = + vm_exit_guards_for_block(func, block_addr, &mut seen_exit_guards); + if block_guards.is_empty() { + residual_guards |= residual; + } else { + exit_guards.extend(block_guards); + } + } + if matches!( + cfg_block.map(|block| &block.terminator), + Some(BlockTerminator::Return) + ) { + may_return = true; + } + + record_block_state(block, &mut state_inputs, &mut state_outputs); + for (op_idx, op) in block.ops.iter().enumerate() { + if op.is_memory_read() { + memory_reads += 1; + } + if op.is_memory_write() { + memory_writes += 1; + } + let (reads, writes, residual) = + vm_memory_conditions_for_op(func, block_addr, op_idx, op); + if residual { + residual_memory_effects = true; + } + memory_read_effects.extend(reads); + memory_write_effects.extend(writes); + if matches!( + op, + SSAOp::Call { .. } | SSAOp::CallInd { .. } | SSAOp::CallOther { .. } + ) { + calls += 1; + } + if let Some(dst) = op.dst() + && !dst.is_const() + && !dst.is_temp() + && !dst.name.starts_with("ram:") + { + let value = classify_vm_op_value_at_site(func, block_addr, op_idx, op, 0) + .unwrap_or_else(|| VmValueExpr::Var(dst.display_name())); + state_updates.insert(dst.display_name(), value); + } + } + + let succs = func.successors(block_addr); + if succs.is_empty() { + continue; + } + for succ in succs { + if succ == dispatch_header || succ == loop_header { + reenters_dispatch = true; + exit_targets.insert(succ); + continue; + } + if dispatch_targets.contains(&succ) && succ != entry { + exit_targets.insert(succ); + continue; + } + if depth >= MAX_HANDLER_REGION_DEPTH { + truncated = true; + exit_targets.insert(succ); + continue; + } + queue.push_back((succ, depth + 1)); + } + } + + exit_guards.sort_by(|lhs, rhs| { + (lhs.target, &lhs.guard.expr, lhs.guard.expect_nonzero).cmp(&( + rhs.target, + &rhs.guard.expr, + rhs.guard.expect_nonzero, + )) + }); + memory_read_effects.sort_by(|lhs, rhs| { + ( + lhs.region.id, + &lhs.region.name, + lhs.offset_lo, + lhs.size, + &lhs.expr, + ) + .cmp(&( + rhs.region.id, + &rhs.region.name, + rhs.offset_lo, + rhs.size, + &rhs.expr, + )) + }); + memory_read_effects.dedup(); + memory_write_effects.sort_by(|lhs, rhs| { + ( + lhs.region.id, + &lhs.region.name, + lhs.offset_lo, + lhs.size, + &lhs.expr, + ) + .cmp(&( + rhs.region.id, + &rhs.region.name, + rhs.offset_lo, + rhs.size, + &rhs.expr, + )) + }); + memory_write_effects.dedup(); + + HandlerRegionSummary { + blocks: visited.into_iter().collect(), + state_inputs: state_inputs.into_iter().collect(), + state_outputs: state_outputs.into_iter().collect(), + state_updates: state_updates + .into_iter() + .map(|(output, value)| VmStateUpdate { + output, + expr: value.render(), + exact: value.is_exact(), + value, + }) + .collect(), + exit_guards, + memory_read_effects, + memory_write_effects, + memory_reads, + memory_writes, + calls, + conditional_branches, + exit_targets: exit_targets.into_iter().collect(), + reenters_dispatch, + may_return, + truncated, + residual_guards, + residual_memory_effects, + } +} + +fn direct_loop_latches(func: &SsaArtifact, header: u64) -> Vec { + func.predecessors(header) + .into_iter() + .filter(|pred| matches!(func.edge_type(*pred, header), Some(CFGEdge::Back))) + .collect() +} + +fn can_reach_within( + func: &SsaArtifact, + from: u64, + target: u64, + max_depth: usize, + visited: &mut BTreeSet, +) -> bool { + if from == target { + return true; + } + if max_depth == 0 || !visited.insert(from) { + return false; + } + func.successors(from) + .into_iter() + .any(|succ| can_reach_within(func, succ, target, max_depth - 1, visited)) +} + +fn enclosing_loop_header(func: &SsaArtifact, dispatch_header: u64) -> Option<(u64, Vec)> { + let mut visited = BTreeSet::new(); + let mut frontier = func.predecessors(dispatch_header); + let mut depth = 0usize; + let dispatch_targets = func.successors(dispatch_header); + + while !frontier.is_empty() && depth < 8 { + let mut next = Vec::new(); + for candidate in frontier { + if !visited.insert(candidate) { + continue; + } + let latches = direct_loop_latches(func, candidate); + if !latches.is_empty() && func.dominates(candidate, dispatch_header) { + return Some((candidate, latches)); + } + if func.dominates(candidate, dispatch_header) { + let returning_targets = dispatch_targets + .iter() + .copied() + .filter(|target| { + can_reach_within(func, *target, candidate, 4, &mut BTreeSet::new()) + }) + .collect::>(); + if !returning_targets.is_empty() { + return Some((candidate, returning_targets)); + } + } + next.extend(func.predecessors(candidate)); + } + frontier = next; + depth += 1; + } + + None +} + +pub(crate) fn classify_interpreter_like(func: &SsaArtifact) -> Option { + let summary = func.function().cfg_risk_summary(); + if summary.block_count < 6 || summary.loop_count == 0 { + return None; + } + + let has_indirect = func + .cfg() + .block_addrs() + .filter_map(|addr| func.cfg().get_block(addr)) + .any(|block| matches!(block.terminator, BlockTerminator::IndirectBranch)); + if summary.switch_block_count == 0 && !has_indirect { + return None; + } + + let direct_call_diversity = func + .call_sites() + .by_id + .values() + .filter_map(|call| call.direct_target) + .collect::>() + .len(); + + let mut best: Option = None; + let mut best_score = i32::MIN; + for block_addr in func.cfg().block_addrs() { + let Some(block) = func.cfg().get_block(block_addr) else { + continue; + }; + let selector = func + .function() + .infer_switch_selector_var(block_addr) + .map(|var| var.name); + let dispatch_targets = func.successors(block_addr); + let kind = match block.terminator { + BlockTerminator::Switch { .. } => InterpreterKind::SwitchDispatch, + BlockTerminator::IndirectBranch => InterpreterKind::IndirectDispatch, + _ if dispatch_targets.len() >= 4 && selector.is_some() => { + InterpreterKind::SwitchDispatch + } + _ => continue, + }; + + let preds = func.predecessors(block_addr); + let back_edges = preds + .iter() + .filter(|pred| matches!(func.edge_type(**pred, block_addr), Some(CFGEdge::Back))) + .count(); + let dispatch_fanout = dispatch_targets.len(); + + let mut score = 0i32; + if back_edges > 0 { + score += 2; + } + if selector.is_some() { + score += 2; + } + if matches!(kind, InterpreterKind::SwitchDispatch) { + score += 2; + } + if dispatch_fanout >= 4 { + score += 1; + } + let dominated_targets = dispatch_targets + .iter() + .filter(|target| func.dominates(block_addr, **target)) + .count(); + if dispatch_fanout > 0 && dominated_targets * 2 >= dispatch_fanout { + score += 1; + } + if direct_call_diversity <= 2 { + score += 1; + } + if direct_call_diversity > dispatch_fanout.max(4) { + score -= 2; + } + + let threshold = match kind { + InterpreterKind::SwitchDispatch => 6, + InterpreterKind::IndirectDispatch => 5, + }; + if score < threshold || score < best_score { + continue; + } + + best_score = score; + best = Some(InterpreterDispatchSummary { + kind, + dispatch_header: block_addr, + dispatch_targets: dispatch_fanout, + selector, + back_edges, + score, + }); + } + + if best.is_some() { + return best; + } + + let mut fallback: Option = None; + let mut fallback_score = i32::MIN; + for block_addr in func.cfg().block_addrs() { + let dispatch_targets = func.successors(block_addr); + if dispatch_targets.len() < 4 { + continue; + } + let selector = func + .function() + .infer_switch_selector_var(block_addr) + .map(|var| var.name); + let back_edges = func + .predecessors(block_addr) + .iter() + .filter(|pred| matches!(func.edge_type(**pred, block_addr), Some(CFGEdge::Back))) + .count(); + let score = (dispatch_targets.len() as i32) + if selector.is_some() { 2 } else { 0 }; + if score < fallback_score { + continue; + } + fallback_score = score; + fallback = Some(InterpreterDispatchSummary { + kind: InterpreterKind::SwitchDispatch, + dispatch_header: block_addr, + dispatch_targets: dispatch_targets.len(), + selector, + back_edges, + score, + }); + } + + fallback +} + +pub(crate) fn build_vm_step_summary( + func: &SsaArtifact, + interpreter: &InterpreterDispatchSummary, +) -> Option { + let dispatch_targets = func.successors(interpreter.dispatch_header); + if dispatch_targets.len() < 2 { + return None; + } + let (loop_header, loop_latches) = { + let direct = direct_loop_latches(func, interpreter.dispatch_header); + if direct.is_empty() { + enclosing_loop_header(func, interpreter.dispatch_header) + .unwrap_or((interpreter.dispatch_header, Vec::new())) + } else { + (interpreter.dispatch_header, direct) + } + }; + if loop_latches.is_empty() { + return None; + } + + let dispatch_target_set = dispatch_targets.iter().copied().collect::>(); + let (case_values_by_target, default_target) = + case_values_by_target(func, interpreter.dispatch_header); + let mut handler_regions = BTreeMap::new(); + let mut handler_state_inputs = BTreeMap::new(); + let mut handler_state_outputs = BTreeMap::new(); + let mut handler_state_updates = BTreeMap::new(); + let mut handler_exit_guards = BTreeMap::new(); + let mut handler_memory_read_effects = BTreeMap::new(); + let mut handler_memory_write_effects = BTreeMap::new(); + let mut handler_memory_reads = BTreeMap::new(); + let mut handler_memory_writes = BTreeMap::new(); + let mut handler_calls = BTreeMap::new(); + let mut handler_conditional_branches = BTreeMap::new(); + let mut handler_exit_targets = BTreeMap::new(); + let mut redispatch_handlers = Vec::new(); + let mut returning_handlers = Vec::new(); + let mut truncated_handlers = Vec::new(); + let mut transfers = Vec::new(); + let mut step_block_set = BTreeSet::from([loop_header, interpreter.dispatch_header]); + + for target in dispatch_targets.iter().copied() { + let summary = summarize_handler_region( + func, + target, + interpreter.dispatch_header, + loop_header, + &dispatch_target_set, + ); + step_block_set.extend(summary.blocks.iter().copied()); + if summary.reenters_dispatch { + redispatch_handlers.push(target); + } + if summary.may_return { + returning_handlers.push(target); + } + if summary.truncated { + truncated_handlers.push(target); + } + let case_values = case_values_by_target + .get(&target) + .cloned() + .unwrap_or_default(); + let selector_update = interpreter.selector.as_ref().and_then(|selector| { + summary + .state_updates + .iter() + .find(|update| same_logical_name(&update.output, selector)) + .cloned() + }); + let exact_memory_effects = summary + .memory_read_effects + .iter() + .all(|effect| effect.exact_offset) + && summary + .memory_write_effects + .iter() + .all(|effect| effect.exact_offset && effect.exact_value); + let exact = !summary.truncated + && summary.calls == 0 + && summary.state_updates.iter().all(|update| update.exact) + && selector_update.as_ref().is_none_or(|update| update.exact) + && summary.exit_guards.iter().all(|guard| guard.guard.exact) + && !summary.residual_guards + && exact_memory_effects + && !summary.residual_memory_effects; + transfers.push(VmTransferArm { + handler_target: target, + case_values, + region_blocks: summary.blocks.clone(), + exit_targets: summary.exit_targets.clone(), + exit_guards: summary.exit_guards.clone(), + state_updates: summary.state_updates.clone(), + selector_update, + memory_reads: summary.memory_read_effects.clone(), + memory_writes: summary.memory_write_effects.clone(), + residual_guards: summary.residual_guards, + residual_memory_effects: summary.residual_memory_effects, + exact, + redispatch: summary.reenters_dispatch, + may_return: summary.may_return, + truncated: summary.truncated, + }); + handler_regions.insert(target, summary.blocks); + handler_state_inputs.insert(target, summary.state_inputs); + handler_state_outputs.insert(target, summary.state_outputs); + handler_state_updates.insert(target, summary.state_updates); + handler_exit_guards.insert(target, summary.exit_guards); + handler_memory_read_effects.insert(target, summary.memory_read_effects); + handler_memory_write_effects.insert(target, summary.memory_write_effects); + handler_memory_reads.insert(target, summary.memory_reads); + handler_memory_writes.insert(target, summary.memory_writes); + handler_calls.insert(target, summary.calls); + handler_conditional_branches.insert(target, summary.conditional_branches); + handler_exit_targets.insert(target, summary.exit_targets); + } + + let step_blocks = step_block_set.into_iter().collect::>(); + let mut state_inputs = BTreeSet::new(); + let mut state_outputs = BTreeSet::new(); + for block_addr in step_blocks.iter().copied() { + let Some(block) = func.get_block(block_addr) else { + continue; + }; + record_block_state(block, &mut state_inputs, &mut state_outputs); + } + + if state_inputs.is_empty() || state_outputs.is_empty() { + return None; + } + + redispatch_handlers.sort_unstable(); + redispatch_handlers.dedup(); + returning_handlers.sort_unstable(); + returning_handlers.dedup(); + truncated_handlers.sort_unstable(); + truncated_handlers.dedup(); + transfers.sort_by_key(|transfer| transfer.handler_target); + + Some(VmStepSummary { + kind: interpreter.kind, + loop_header, + dispatch_header: interpreter.dispatch_header, + selector: interpreter.selector.clone(), + dispatch_targets, + default_target, + case_values_by_target, + loop_latches, + state_inputs: state_inputs.into_iter().collect(), + state_outputs: state_outputs.into_iter().collect(), + step_blocks, + handler_regions, + handler_state_inputs, + handler_state_outputs, + handler_state_updates, + handler_exit_guards, + handler_memory_read_effects, + handler_memory_write_effects, + handler_memory_reads, + handler_memory_writes, + handler_calls, + handler_conditional_branches, + handler_exit_targets, + redispatch_handlers, + returning_handlers, + truncated_handlers, + transfers, + }) +} diff --git a/crates/r2sym/src/sim.rs b/crates/r2sym/src/sim.rs index 6d2b5d2..df08150 100644 --- a/crates/r2sym/src/sim.rs +++ b/crates/r2sym/src/sim.rs @@ -3,19 +3,24 @@ //! These summaries short-circuit into lightweight models to avoid //! path explosion from libc implementations. -use std::collections::{BTreeSet, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::rc::Rc; use std::sync::Arc; use r2il::ArchSpec; use r2ssa::{ - FunctionSemanticSummary, InterprocFunctionId, InterprocSummarySet, SsaArtifact, - SummaryMemoryEffectKind, SummaryMemoryRegion, SummaryReturnRelation, + FunctionSemanticSummary, InterprocFunctionId, InterprocFunctionInput, InterprocSolveConfig, + InterprocSummarySet, SsaArtifact, SummaryMemoryEffectKind, SummaryMemoryRegion, + SummaryReturnRelation, solve_interproc_summary_set, }; -use z3::ast::BV; +use serde::{Deserialize, Serialize}; +use z3::ast::{Ast, BV, Bool}; use crate::executor::{CallHookResult, SymExecutor}; use crate::path::PathExplorer; -use crate::state::{ExitStatus, SymState}; +use crate::solver::{SatResult, SymSolver}; +use crate::state::{ExitStatus, RuntimeValueProvenance, SymState}; use crate::value::SymValue; /// Default upper bound for string operations. @@ -30,6 +35,100 @@ pub const DEFAULT_MAX_MEMCMP: u64 = 0x1000; pub const DEFAULT_MAX_PRINTF_SCAN: u64 = 0x400; /// Default upper bound for generic interproc memory havoc windows. pub const DEFAULT_MAX_INTERPROC_HAVOC: u64 = 0x40; +/// Path-listing upper bound for memory copy operations. +pub const PATH_LIST_MAX_MEMCPY: u64 = 0x40; +/// Path-listing upper bound for memory set operations. +pub const PATH_LIST_MAX_MEMSET: u64 = 0x40; +/// Path-listing upper bound for string operations. +pub const PATH_LIST_MAX_STRLEN: u64 = 0x80; +/// Path-listing upper bound for memcmp operations. +pub const PATH_LIST_MAX_MEMCMP: u64 = 0x40; +/// Path-listing upper bound for printf/puts modeled return values. +pub const PATH_LIST_MAX_PRINTF_SCAN: u64 = 0x40; +/// Path-listing precise-byte threshold before summaries switch to coarse modeling. +pub const PATH_LIST_PRECISE_BYTE_LIMIT: u64 = 0x10; +/// Default state budget for derived symbolic helper summaries. +pub const DEFAULT_DERIVED_SUMMARY_MAX_STATES: usize = 64; +/// Default path cap for derived helper summarization. +pub const DEFAULT_DERIVED_SUMMARY_MAX_PATHS: usize = 8; +/// Default depth budget for derived helper summarization. +pub const DEFAULT_DERIVED_SUMMARY_MAX_DEPTH: usize = 128; +/// Default bounded fixed-point iteration count for derived helper SCCs. +pub const DEFAULT_DERIVED_SUMMARY_MAX_ITERATIONS: usize = 8; + +/// Summary profile used to install function summaries for different workflows. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SummaryProfile { + /// Default symbolic execution behavior. + Default, + /// Fast bounded modeling for path-listing workflows. + PathListing, +} + +#[derive(Debug, Clone, Copy)] +struct SummaryBudgets { + max_strlen: u64, + max_memcpy: u64, + max_memset: u64, + max_memcmp: u64, + max_printf_scan: u64, + byte_policy: ByteSummaryPolicy, +} + +#[derive(Debug, Clone, Copy)] +struct ByteSummaryPolicy { + summarize_symbolic: bool, + summarize_large: bool, + precise_limit: u64, +} + +impl ByteSummaryPolicy { + const fn precise(max_bytes: u64) -> Self { + Self { + summarize_symbolic: false, + summarize_large: false, + precise_limit: max_bytes, + } + } + + const fn summarized(precise_limit: u64) -> Self { + Self { + summarize_symbolic: true, + summarize_large: true, + precise_limit, + } + } + + fn use_precise_model(&self, n_concrete: Option) -> bool { + match n_concrete { + Some(len) => !self.summarize_large || len <= self.precise_limit, + None => !self.summarize_symbolic, + } + } +} + +impl SummaryProfile { + fn budgets(self) -> SummaryBudgets { + match self { + SummaryProfile::Default => SummaryBudgets { + max_strlen: DEFAULT_MAX_STRLEN, + max_memcpy: DEFAULT_MAX_MEMCPY, + max_memset: DEFAULT_MAX_MEMSET, + max_memcmp: DEFAULT_MAX_MEMCMP, + max_printf_scan: DEFAULT_MAX_PRINTF_SCAN, + byte_policy: ByteSummaryPolicy::precise(DEFAULT_MAX_MEMCPY), + }, + SummaryProfile::PathListing => SummaryBudgets { + max_strlen: PATH_LIST_MAX_STRLEN, + max_memcpy: PATH_LIST_MAX_MEMCPY, + max_memset: PATH_LIST_MAX_MEMSET, + max_memcmp: PATH_LIST_MAX_MEMCMP, + max_printf_scan: PATH_LIST_MAX_PRINTF_SCAN, + byte_policy: ByteSummaryPolicy::summarized(PATH_LIST_PRECISE_BYTE_LIMIT), + }, + } + } +} /// Summary execution outcome. pub enum SummaryEffect<'ctx> { @@ -89,10 +188,17 @@ impl CallConv { Self::new(vec!["RDI", "RSI", "RDX", "RCX", "R8", "R9"], "RAX", 64, 64) } + /// x86-64 Windows ABI (RCX, RDX, R8, R9; return in RAX). + pub fn x86_64_windows() -> Self { + Self::new(vec!["RCX", "RDX", "R8", "R9"], "RAX", 64, 64) + } + /// Architecture-derived calling convention used by the symbolic runtime. pub fn for_arch_spec(arch: &ArchSpec) -> Option { let arch_name = arch.name.to_ascii_lowercase(); - if arch.addr_size == 8 && arch_name.contains("x86") { + let looks_x86 = arch_name.contains("x86") || arch_name == "x64" || arch_name == "amd64"; + let looks_64 = arch.addr_size == 8 || arch.addr_size == 64 || arch_name.contains("64"); + if looks_x86 && looks_64 { return Some(Self::x86_64_sysv()); } @@ -112,7 +218,25 @@ impl CallConv { None } - fn collect_call_info<'ctx>(&self, state: &SymState<'ctx>, arity: usize) -> CallInfo<'ctx> { + /// Architecture-derived calling convention with binary-environment hints. + pub fn for_arch_spec_and_symbols( + arch: &ArchSpec, + symbol_map: &HashMap, + ) -> Option { + let arch_name = arch.name.to_ascii_lowercase(); + let looks_x86 = arch_name.contains("x86") || arch_name == "x64" || arch_name == "amd64"; + let looks_64 = arch.addr_size == 8 || arch.addr_size == 64 || arch_name.contains("64"); + if looks_x86 && looks_64 && symbol_map_looks_windows(symbol_map) { + return Some(Self::x86_64_windows()); + } + Self::for_arch_spec(arch) + } + + pub(crate) fn collect_call_info<'ctx>( + &self, + state: &SymState<'ctx>, + arity: usize, + ) -> CallInfo<'ctx> { let mut args = Vec::with_capacity(arity); for i in 0..arity { if let Some(reg) = self.arg_registers.get(i) { @@ -129,19 +253,64 @@ impl CallConv { } fn read_register<'ctx>(&self, state: &SymState<'ctx>, base: &str) -> SymValue<'ctx> { - if let Some(key) = find_register_key(state, base) { - state.get_register_sized(&key, self.arg_bits) - } else { - SymValue::unknown(self.arg_bits) + for alias in register_aliases(base) { + if let Some(key) = find_register_key(state, alias) { + return state.get_register_sized(&key, self.arg_bits); + } } + SymValue::unknown(self.arg_bits) } - fn write_return<'ctx>(&self, state: &mut SymState<'ctx>, value: SymValue<'ctx>) { - let key = find_register_key(state, self.ret_register) + pub(crate) fn write_return<'ctx>(&self, state: &mut SymState<'ctx>, value: SymValue<'ctx>) { + let key = register_aliases(self.ret_register) + .into_iter() + .find_map(|alias| find_register_key(state, alias)) .unwrap_or_else(|| format!("{}_0", self.ret_register)); - let adjusted = adjust_bits(state.context(), value, self.ret_bits); + let key_bits = state + .registers() + .get(&key) + .map(|existing| existing.bits()) + .unwrap_or(self.ret_bits); + let adjusted = adjust_bits(state.context(), value, key_bits); state.set_register(&key, adjusted); } + + pub(crate) fn arg_register_name(&self, index: usize) -> Option<&'static str> { + self.arg_registers.get(index).copied() + } + + pub(crate) fn arg_capacity(&self) -> usize { + self.arg_registers.len() + } + + pub(crate) fn arg_bits(&self) -> u32 { + self.arg_bits + } + + pub(crate) fn ret_bits(&self) -> u32 { + self.ret_bits + } + + pub(crate) fn ret_register_name(&self) -> &'static str { + self.ret_register + } + + pub(crate) fn return_value<'ctx>(&self, state: &SymState<'ctx>) -> SymValue<'ctx> { + self.read_register(state, self.ret_register) + } +} + +fn symbol_map_looks_windows(symbol_map: &HashMap) -> bool { + symbol_map.values().any(|name| { + let lower = name.to_ascii_lowercase(); + lower.contains(".dll_") + || lower.contains("kernel32") + || lower.contains("ntdll") + || lower.contains("msvcrt") + || lower.ends_with("addvectoredexceptionhandler") + || lower.ends_with("raiseexception") + || lower.ends_with("virtualalloc") + }) } #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] @@ -152,6 +321,156 @@ pub struct SummaryInstallStats { pub duplicates: usize, } +#[derive(Debug, Clone)] +pub struct ScopedPreparedFunction { + pub id: InterprocFunctionId, + pub name: Option, + pub prepared: SsaArtifact, +} + +#[derive(Debug, Clone)] +pub struct PreparedFunctionScope { + root: InterprocFunctionId, + functions: BTreeMap, +} + +impl PreparedFunctionScope { + pub fn new(root_addr: u64, functions: Vec) -> Option { + let root = InterprocFunctionId(root_addr); + let mut by_id = BTreeMap::new(); + for function in functions { + by_id.insert(function.id, function); + } + by_id.contains_key(&root).then_some(Self { + root, + functions: by_id, + }) + } + + pub fn root_id(&self) -> InterprocFunctionId { + self.root + } + + pub fn root(&self) -> Option<&ScopedPreparedFunction> { + self.functions.get(&self.root) + } + + pub fn functions(&self) -> &BTreeMap { + &self.functions + } + + pub fn function_containing_block(&self, pc: u64) -> Option<&ScopedPreparedFunction> { + self.functions + .values() + .find(|function| function.prepared.get_block(pc).is_some()) + } + + pub fn contains_block(&self, pc: u64) -> bool { + self.function_containing_block(pc).is_some() + } + + pub fn helper_functions(&self) -> impl Iterator { + self.functions + .values() + .filter(move |function| function.id != self.root) + } + + pub fn with_prepared_root(&self, prepared: &SsaArtifact) -> Option { + let mut functions = self.functions.values().cloned().collect::>(); + for function in &mut functions { + if function.id == self.root { + function.prepared = prepared.clone(); + if function.name.is_none() { + function.name = prepared.function().name.clone(); + } + } + } + Self::new(self.root.0, functions) + } +} + +fn is_runtime_materialized_scope_function(function: &ScopedPreparedFunction) -> bool { + function + .name + .as_deref() + .is_some_and(|name| name.starts_with("runtime.materialized.")) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DerivedSummaryCompletion { + Exact, + OverApprox, + BudgetExhausted, + Unknown, +} + +#[derive(Clone)] +pub struct DerivedSummaryInput<'ctx> { + pub arg_index: usize, + pub symbol: SymValue<'ctx>, + pub size: u32, +} + +#[derive(Clone)] +pub struct DerivedMemoryWrite<'ctx> { + pub arg_index: usize, + pub offset: i64, + pub size: u32, + pub value: SymValue<'ctx>, +} + +#[derive(Clone)] +pub struct DerivedSummaryCase<'ctx> { + pub guard: Bool, + pub return_value: Option>, + pub memory_writes: Vec>, +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub(crate) struct DerivedSummaryGuidance { + pub summary_known: bool, + pub exact: bool, + pub feasible_cases: usize, + pub contradictory: bool, +} + +#[derive(Debug, Clone, Copy, Default)] +struct PointerInputWindow { + headroom: u32, + forward_size: u32, +} + +#[derive(Clone)] +pub struct DerivedFunctionSummary<'ctx> { + pub id: InterprocFunctionId, + pub name: Option, + pub arg_count_hint: usize, + pub arg_symbols: Vec<(usize, SymValue<'ctx>)>, + pub memory_inputs: Vec>, + pub cases: Vec>, + pub completion: DerivedSummaryCompletion, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct DerivedSummaryDiagnostics { + pub attempted: usize, + pub derived: usize, + pub budget_exhausted: usize, + pub skipped_core: usize, + pub skipped_missing: usize, + pub scc_count: usize, + pub max_scc_size: usize, + pub scc_converged: usize, + pub scc_budget_exhausted: usize, +} + +#[derive(Clone)] +pub struct DerivedSummarySet<'ctx> { + pub interproc: InterprocSummarySet, + pub summaries: BTreeMap>>, + pub diagnostics: DerivedSummaryDiagnostics, +} + /// Summary registry that can install summaries as call hooks. pub struct SummaryRegistry<'ctx> { summaries: HashMap + 'ctx>>, @@ -169,23 +488,60 @@ impl<'ctx> SummaryRegistry<'ctx> { /// Create a registry pre-populated with core summaries. pub fn with_core(callconv: CallConv) -> Self { + Self::with_profile(callconv, SummaryProfile::Default) + } + + /// Create a registry pre-populated with summaries for a typed workflow profile. + pub fn with_profile(callconv: CallConv, profile: SummaryProfile) -> Self { + let budgets = profile.budgets(); let mut registry = Self::new(callconv); - registry.register_summary(MemcpySummary::new(DEFAULT_MAX_MEMCPY)); - registry.register_summary(MemsetSummary::new(DEFAULT_MAX_MEMSET)); - registry.register_summary(StrlenSummary::new(DEFAULT_MAX_STRLEN)); + registry.register_summary(MemcpySummary::with_policy( + budgets.max_memcpy, + budgets.byte_policy, + )); + registry.register_summary(MemsetSummary::with_policy( + budgets.max_memset, + budgets.byte_policy, + )); + registry.register_summary(StrlenSummary::new(budgets.max_strlen)); registry.register_summary(StrcmpSummary::new()); - registry.register_summary(MemcmpSummary::new(DEFAULT_MAX_MEMCMP)); + registry.register_summary(MemcmpSummary::new(budgets.max_memcmp)); registry.register_summary(MallocSummary::new()); registry.register_summary(FreeSummary::new()); - registry.register_summary(PutsSummary::new(DEFAULT_MAX_PRINTF_SCAN)); - registry.register_summary(PrintfSummaryBasic::new(DEFAULT_MAX_PRINTF_SCAN)); + registry.register_summary(PutsSummary::new(budgets.max_printf_scan)); + registry.register_summary(PrintfSummaryBasic::new(budgets.max_printf_scan)); + registry.register_summary(ReadSummary::new()); + registry.register_summary(IsattySummary::new()); + registry.register_summary(SleepSummary::sleep()); + registry.register_summary(SleepSummary::usleep()); + registry.register_summary(SleepSummary::nanosleep()); registry.register_summary(ExitSummary::new()); registry } /// Create a registry pre-populated with core summaries for a known architecture. pub fn with_core_for_arch(arch: &ArchSpec) -> Option { - Some(Self::with_core(CallConv::for_arch_spec(arch)?)) + Some(Self::with_profile( + CallConv::for_arch_spec(arch)?, + SummaryProfile::Default, + )) + } + + /// Create a registry pre-populated with summaries for a typed workflow profile. + pub fn with_profile_for_arch(arch: &ArchSpec, profile: SummaryProfile) -> Option { + Some(Self::with_profile(CallConv::for_arch_spec(arch)?, profile)) + } + + /// Create a registry using symbol-map environment hints when the architecture is ambiguous. + pub fn with_profile_for_arch_and_symbols( + arch: &ArchSpec, + symbol_map: &HashMap, + profile: SummaryProfile, + ) -> Option { + Some(Self::with_profile( + CallConv::for_arch_spec_and_symbols(arch, symbol_map)?, + profile, + )) } /// Register a function summary. @@ -327,6 +683,909 @@ impl<'ctx> SummaryRegistry<'ctx> { stats } + + pub fn has_core_summary_name(&self, name: &str) -> bool { + normalize_core_summary_name(name) + .is_some_and(|summary_name| self.summaries.contains_key(summary_name)) + } + + pub fn derive_symbolic_summaries( + &self, + ctx: &'ctx z3::Context, + scope: &PreparedFunctionScope, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + ) -> DerivedSummarySet<'ctx> { + let interproc = build_interproc_summary_set(scope, arch, symbol_map); + let mut summaries: BTreeMap>> = + BTreeMap::new(); + let mut diagnostics = DerivedSummaryDiagnostics::default(); + + let helper_scope = scope + .helper_functions() + .filter(|helper| !is_runtime_materialized_scope_function(helper)) + .map(|helper| (helper.id, helper)) + .collect::>(); + let sccs = compute_derived_summary_sccs(&helper_scope); + diagnostics.scc_count = sccs.len(); + + for scc in sccs { + diagnostics.max_scc_size = diagnostics.max_scc_size.max(scc.len()); + let mut converged = false; + for _ in 0..DEFAULT_DERIVED_SUMMARY_MAX_ITERATIONS { + let mut changed = false; + for function_id in &scc { + let Some(helper) = helper_scope.get(function_id).copied() else { + continue; + }; + diagnostics.attempted += 1; + if helper + .name + .as_deref() + .is_some_and(|name| self.has_core_summary_name(name)) + { + diagnostics.skipped_core += 1; + continue; + } + + let Some(static_summary) = interproc.summaries.get(function_id).cloned() else { + diagnostics.skipped_missing += 1; + continue; + }; + + let derived = derive_symbolic_summary_for_function(DerivedSummaryBuildInputs { + ctx, + registry: self, + arch, + function: helper, + static_summary: &static_summary, + interproc: &interproc, + derived_summaries: &summaries, + symbol_map, + }); + let next = Rc::new(derived); + let previous = summaries.get(function_id); + if previous.map(|current| derived_summary_fingerprint(current)) + != Some(derived_summary_fingerprint(&next)) + { + summaries.insert(*function_id, next); + changed = true; + } + } + if !changed { + converged = true; + diagnostics.scc_converged += 1; + break; + } + } + + if !converged { + diagnostics.scc_budget_exhausted += 1; + for function_id in &scc { + if let Some(summary) = summaries.get_mut(function_id) { + let next = with_budget_exhausted_completion(summary.as_ref()); + *summary = Rc::new(next); + } + } + } + } + + for summary in summaries.values() { + match summary.completion { + DerivedSummaryCompletion::BudgetExhausted => diagnostics.budget_exhausted += 1, + DerivedSummaryCompletion::Unknown => diagnostics.skipped_missing += 1, + _ => diagnostics.derived += 1, + } + } + + DerivedSummarySet { + interproc, + summaries, + diagnostics, + } + } + + pub fn install_scope_summaries_for_explorer( + &self, + explorer: &mut PathExplorer<'ctx>, + ctx: &'ctx z3::Context, + scope: &PreparedFunctionScope, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + ) -> DerivedSummaryDiagnostics { + let derived = self.derive_symbolic_summaries(ctx, scope, arch, symbol_map); + if let Some(root) = scope.root() { + let _ = self.install_interproc_summaries_for_function( + explorer, + &root.prepared, + &derived.interproc, + symbol_map, + ); + let _ = self.install_derived_summaries_for_function( + explorer, + &root.prepared, + &derived.summaries, + symbol_map, + ); + let _ = self.install_known_symbols_for_function(explorer, &root.prepared, symbol_map); + } + derived.diagnostics + } + + pub fn install_derived_summaries_for_function( + &self, + explorer: &mut PathExplorer<'ctx>, + prepared: &SsaArtifact, + summaries: &BTreeMap>>, + symbol_map: &HashMap, + ) -> SummaryInstallStats { + let mut stats = SummaryInstallStats::default(); + let mut targets = BTreeSet::new(); + for call in prepared.call_sites().by_id.values() { + if let Some(target) = call.direct_target { + targets.insert(target); + } + } + for target in targets { + stats.attempted += 1; + if let Some(raw_name) = symbol_map.get(&target) + && self.has_core_summary_name(raw_name) + { + stats.skipped_unknown += 1; + continue; + } + let Some(summary) = summaries.get(&InterprocFunctionId(target)).cloned() else { + stats.skipped_unknown += 1; + continue; + }; + if summary.cases.is_empty() + || matches!( + summary.completion, + DerivedSummaryCompletion::Unknown | DerivedSummaryCompletion::BudgetExhausted + ) + { + stats.skipped_unknown += 1; + continue; + } + let callconv = self.callconv.clone(); + explorer.register_derived_call_hook( + target, + summary.clone(), + callconv.clone(), + move |state| apply_derived_summary(state, &summary, &callconv), + ); + stats.installed += 1; + } + stats + } +} + +fn compute_derived_summary_sccs( + helpers: &BTreeMap, +) -> Vec> { + let node_ids: Vec = helpers.keys().copied().collect(); + let node_set = node_ids.iter().copied().collect::>(); + let mut succs = BTreeMap::>::new(); + let mut rev = BTreeMap::>::new(); + + for node in &node_ids { + succs.entry(*node).or_default(); + rev.entry(*node).or_default(); + } + + for (id, helper) in helpers { + let mut out = helper + .prepared + .call_sites() + .by_id + .values() + .filter_map(|call| call.direct_target.map(InterprocFunctionId)) + .filter(|target| node_set.contains(target)) + .collect::>(); + out.sort_unstable(); + out.dedup(); + succs.insert(*id, out.clone()); + for succ in out { + rev.entry(succ).or_default().push(*id); + } + } + + for preds in rev.values_mut() { + preds.sort_unstable(); + preds.dedup(); + } + + let mut visited = BTreeSet::new(); + let mut order = Vec::new(); + for node in &node_ids { + dfs_summary_postorder(*node, &succs, &mut visited, &mut order); + } + + visited.clear(); + let mut sccs = Vec::new(); + while let Some(node) = order.pop() { + if visited.contains(&node) { + continue; + } + let mut component = Vec::new(); + dfs_summary_component(node, &rev, &mut visited, &mut component); + component.sort_unstable(); + sccs.push(component); + } + + sccs.reverse(); + sccs +} + +fn dfs_summary_postorder( + node: InterprocFunctionId, + succs: &BTreeMap>, + visited: &mut BTreeSet, + order: &mut Vec, +) { + if !visited.insert(node) { + return; + } + if let Some(nexts) = succs.get(&node) { + for next in nexts { + dfs_summary_postorder(*next, succs, visited, order); + } + } + order.push(node); +} + +fn dfs_summary_component( + node: InterprocFunctionId, + rev: &BTreeMap>, + visited: &mut BTreeSet, + component: &mut Vec, +) { + if !visited.insert(node) { + return; + } + component.push(node); + if let Some(preds) = rev.get(&node) { + for pred in preds { + dfs_summary_component(*pred, rev, visited, component); + } + } +} + +fn derived_summary_fingerprint(summary: &DerivedFunctionSummary<'_>) -> Vec { + let mut out = vec![ + format!("{:?}", summary.completion), + summary.arg_count_hint.to_string(), + summary.arg_symbols.len().to_string(), + summary.memory_inputs.len().to_string(), + summary.cases.len().to_string(), + ]; + out.extend( + summary + .arg_symbols + .iter() + .map(|(index, symbol)| format!("arg:{index}:{}", symbol)), + ); + out.extend( + summary + .memory_inputs + .iter() + .map(|input| format!("mem:{}:{}:{}", input.arg_index, input.size, input.symbol)), + ); + out.extend(summary.cases.iter().map(|case| { + let writes = case + .memory_writes + .iter() + .map(|write| { + format!( + "{}:{}:{}:{}", + write.arg_index, write.offset, write.size, write.value + ) + }) + .collect::>() + .join("|"); + format!( + "case:{}:{}:{}", + case.guard.simplify(), + case.return_value + .as_ref() + .map(|value| value.to_string()) + .unwrap_or_else(|| "".to_string()), + writes + ) + })); + out +} + +fn with_budget_exhausted_completion<'ctx>( + summary: &DerivedFunctionSummary<'ctx>, +) -> DerivedFunctionSummary<'ctx> { + let mut next = summary.clone(); + next.completion = DerivedSummaryCompletion::BudgetExhausted; + next +} + +fn build_interproc_summary_set( + scope: &PreparedFunctionScope, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, +) -> InterprocSummarySet { + let mut inputs = Vec::new(); + let mut seeds = BTreeMap::new(); + for function in scope.functions().values() { + if is_runtime_materialized_scope_function(function) { + continue; + } + inputs.push(InterprocFunctionInput { + id: function.id, + name: function.name.clone(), + prepared: &function.prepared, + }); + if let Some(name) = symbol_map.get(&function.id.0) + && let Some(summary) = FunctionSemanticSummary::seed_for_name(function.id, name) + { + seeds.insert(function.id, summary); + } else if let Some(name) = function.name.as_deref() + && let Some(summary) = FunctionSemanticSummary::seed_for_name(function.id, name) + { + seeds.insert(function.id, summary); + } + } + + solve_interproc_summary_set( + &inputs, + arch, + Some(scope.root_id()), + &seeds, + InterprocSolveConfig::default(), + ) +} + +struct DerivedSummaryBuildInputs<'a, 'ctx> { + ctx: &'ctx z3::Context, + registry: &'a SummaryRegistry<'ctx>, + arch: Option<&'a ArchSpec>, + function: &'a ScopedPreparedFunction, + static_summary: &'a FunctionSemanticSummary, + interproc: &'a InterprocSummarySet, + derived_summaries: &'a BTreeMap>>, + symbol_map: &'a HashMap, +} + +fn derive_symbolic_summary_for_function<'ctx>( + inputs: DerivedSummaryBuildInputs<'_, 'ctx>, +) -> DerivedFunctionSummary<'ctx> { + let DerivedSummaryBuildInputs { + ctx, + registry, + arch, + function, + static_summary, + interproc, + derived_summaries, + symbol_map, + } = inputs; + let mut state = SymState::new(ctx, function.prepared.entry); + crate::runtime::seed_default_state_for_arch(&mut state, &function.prepared, arch); + let defines_return_value = function_defines_return_value(function, ®istry.callconv); + let opaque_return = matches!( + static_summary.return_relation, + SummaryReturnRelation::Unknown + ) && !defines_return_value; + + let mut arg_symbols = Vec::new(); + let helper_name = function + .name + .clone() + .unwrap_or_else(|| format!("sub_{:x}", function.id.0)); + let mut memory_inputs = Vec::new(); + let pointer_inputs = collect_pointer_memory_windows(static_summary); + + for index in 0..registry.callconv.arg_capacity() { + if let Some(reg) = registry.callconv.arg_register_name(index) { + let key = find_register_key(&state, reg) + .unwrap_or_else(|| format!("{}_0", reg.to_ascii_uppercase())); + let arg_bits = static_summary_arg_bits(®istry.callconv); + if let Some(window) = pointer_inputs.get(&index).copied() { + let ptr_base = helper_arg_region_base(function.id, index); + let region_start = ptr_base.saturating_sub(window.headroom as u64); + let region_size = window.headroom.saturating_add(window.forward_size.max(1)); + let _ = state.make_symbolic_memory( + region_start, + region_size.max(1), + &format!("{}_arg{}_mem", helper_name, index), + ); + let symbol = state.mem_read( + &SymValue::concrete(ptr_base, arg_bits), + window.forward_size.max(1), + ); + state.set_register(&key, SymValue::concrete(ptr_base, arg_bits)); + memory_inputs.push(DerivedSummaryInput { + arg_index: index, + symbol, + size: window.forward_size.max(1), + }); + } else { + if !state.registers().contains_key(&key) { + state.make_symbolic_named( + &key, + &format!("{}_arg{}", helper_name, index), + arg_bits, + ); + } + arg_symbols.push((index, state.get_register_sized(&key, arg_bits))); + } + } + } + + let config = crate::path::ExploreConfig { + max_states: DEFAULT_DERIVED_SUMMARY_MAX_STATES, + max_completed_paths: Some(DEFAULT_DERIVED_SUMMARY_MAX_PATHS), + max_depth: DEFAULT_DERIVED_SUMMARY_MAX_DEPTH, + timeout: None, + prune_infeasible: true, + merge_states: true, + subsumption_states: true, + ..crate::path::ExploreConfig::default() + }; + let mut explorer = PathExplorer::with_config(ctx, config); + let _ = registry.install_interproc_summaries_for_function( + &mut explorer, + &function.prepared, + interproc, + symbol_map, + ); + let _ = registry.install_derived_summaries_for_function( + &mut explorer, + &function.prepared, + derived_summaries, + symbol_map, + ); + let _ = + registry.install_known_symbols_for_function(&mut explorer, &function.prepared, symbol_map); + let summary = explorer.summarize_function(&function.prepared, state); + + let completion = if opaque_return { + DerivedSummaryCompletion::Unknown + } else if summary.stats.timed_out || summary.stats.max_states_exhausted { + DerivedSummaryCompletion::BudgetExhausted + } else if summary.paths.is_empty() { + DerivedSummaryCompletion::Unknown + } else if summary.paths.iter().all(|path| path.feasible) { + DerivedSummaryCompletion::Exact + } else { + DerivedSummaryCompletion::OverApprox + }; + + let mut cases = Vec::new(); + let mut write_locations = collect_tracked_memory_writes(static_summary); + write_locations.sort_unstable(); + write_locations.dedup(); + for path in summary.paths.iter().filter(|path| path.feasible) { + let mut memory_writes = Vec::new(); + for (arg_index, offset, size) in &write_locations { + let base_addr = helper_arg_region_base(function.id, *arg_index); + let addr = SymValue::concrete( + concrete_with_signed_offset(base_addr, *offset), + static_summary_arg_bits(®istry.callconv), + ); + let value = path.state.mem_read(&addr, *size); + memory_writes.push(DerivedMemoryWrite { + arg_index: *arg_index, + offset: *offset, + size: *size, + value, + }); + } + memory_writes = coalesce_adjacent_memory_writes(ctx, memory_writes); + + let return_value = match static_summary.return_relation { + SummaryReturnRelation::Void => None, + SummaryReturnRelation::Unknown if opaque_return => None, + _ => Some(registry.callconv.return_value(&path.state)), + }; + cases.push(DerivedSummaryCase { + guard: path.state.path_condition(), + return_value, + memory_writes, + }); + } + + DerivedFunctionSummary { + id: function.id, + name: function.name.clone(), + arg_count_hint: summary_arity(static_summary), + arg_symbols, + memory_inputs, + cases, + completion, + } +} + +fn function_defines_return_value(function: &ScopedPreparedFunction, callconv: &CallConv) -> bool { + let aliases = register_aliases(callconv.ret_register_name()); + function.prepared.blocks().any(|block| { + block.ops.iter().any(|op| { + op.dst().is_some_and(|dst| { + aliases + .iter() + .any(|alias| dst.name.eq_ignore_ascii_case(alias)) + }) + }) + }) +} + +fn coalesce_adjacent_memory_writes<'ctx>( + ctx: &'ctx z3::Context, + mut writes: Vec>, +) -> Vec> { + writes.sort_by_key(|write| (write.arg_index, write.offset, write.size)); + + let mut merged: Vec> = Vec::with_capacity(writes.len()); + for write in writes { + if let Some(last) = merged.last_mut() + && last.arg_index == write.arg_index + && last.offset.checked_add(last.size as i64) == Some(write.offset) + && let Some(new_size) = last.size.checked_add(write.size) + { + last.value = write.value.concat(ctx, &last.value); + last.size = new_size; + continue; + } + merged.push(write); + } + merged +} + +fn helper_arg_region_base(function: InterprocFunctionId, arg_index: usize) -> u64 { + 0x5000_0000u64 + .wrapping_add((function.0 & 0xffff) << 12) + .wrapping_add((arg_index as u64) << 8) + .wrapping_add(0x80) +} + +fn static_summary_arg_bits(callconv: &CallConv) -> u32 { + callconv.arg_bits +} + +fn collect_pointer_memory_windows( + summary: &FunctionSemanticSummary, +) -> BTreeMap { + let mut inputs = BTreeMap::new(); + for (index, effect) in &summary.arg_effects { + if !(effect.read || effect.write || effect.escape || effect.free) { + continue; + } + let mut window = PointerInputWindow { + headroom: 0, + forward_size: DEFAULT_MAX_INTERPROC_HAVOC as u32, + }; + let mut saw_precise_range = false; + for effect in &summary.memory_effects { + let SummaryMemoryRegion::Arg { + index: effect_index, + } = effect.location.region + else { + continue; + }; + if effect_index != *index { + continue; + } + let Some(range) = effect.location.range else { + continue; + }; + let width = range.width.unwrap_or(1).max(1); + let start = range.offset_lo.min(range.offset_hi); + let end = range.offset_hi.max(range.offset_lo); + let forward = if end >= 0 { + (end as u32).saturating_add(width) + } else { + width + }; + let headroom = if start < 0 { + start.checked_abs().unwrap_or(i64::MAX).min(u32::MAX as i64) as u32 + } else { + 0 + }; + window.forward_size = window.forward_size.max(forward.max(1)); + window.headroom = window.headroom.max(headroom); + saw_precise_range = true; + } + if !saw_precise_range { + window.forward_size = DEFAULT_MAX_INTERPROC_HAVOC as u32; + } + inputs.insert(*index, window); + } + inputs +} + +fn collect_tracked_memory_writes(summary: &FunctionSemanticSummary) -> Vec<(usize, i64, u32)> { + let mut writes = Vec::new(); + for effect in &summary.memory_effects { + if !matches!( + effect.kind, + SummaryMemoryEffectKind::Write + | SummaryMemoryEffectKind::Escape + | SummaryMemoryEffectKind::Free + ) { + continue; + } + let SummaryMemoryRegion::Arg { index } = effect.location.region else { + continue; + }; + let range = effect.location.range.unwrap_or(r2ssa::SummaryMemoryRange { + offset_lo: 0, + offset_hi: 0, + width: Some(1), + }); + let width = range.width.unwrap_or(1).max(1); + let start = range.offset_lo.min(range.offset_hi); + let end = range.offset_hi.max(range.offset_lo); + let mut offset = start; + while offset <= end { + writes.push((index, offset, width)); + let Some(next) = offset.checked_add(width as i64) else { + break; + }; + offset = next; + } + } + writes +} + +fn apply_derived_summary<'ctx>( + state: &mut SymState<'ctx>, + summary: &DerivedFunctionSummary<'ctx>, + callconv: &CallConv, +) -> CallHookResult { + let call = + callconv.collect_call_info(state, summary.arg_count_hint.max(callconv.arg_capacity())); + if summary.cases.is_empty() { + return CallHookResult::Fallthrough; + } + if apply_derived_runtime_materialization_copy(state, summary, &call, callconv) { + return CallHookResult::Fallthrough; + } + + let substitutions = build_summary_substitutions(state, summary, &call); + + if summary.cases.iter().any(|case| case.return_value.is_some()) { + let mut merged = SymValue::unknown(call.ret_bits); + for case in summary.cases.iter().rev() { + let Some(return_value) = &case.return_value else { + continue; + }; + let guard = substitute_bool(&case.guard, &substitutions); + let value = substitute_value(state.context(), return_value, &substitutions); + merged = ite_value(state.context(), &guard, &value, &merged); + } + callconv.write_return(state, merged); + } + + let mut writes = BTreeMap::<(usize, i64, u32), Vec<(Bool, SymValue<'ctx>)>>::new(); + for case in &summary.cases { + let guard = substitute_bool(&case.guard, &substitutions); + for write in &case.memory_writes { + let value = substitute_value(state.context(), &write.value, &substitutions); + writes + .entry((write.arg_index, write.offset, write.size)) + .or_default() + .push((guard.clone(), value)); + } + } + + for ((arg_index, offset, size), cases) in writes { + let Some(base) = call.args.get(arg_index) else { + continue; + }; + let addr = add_signed_offset(state.context(), base, offset, call.arg_bits); + let mut merged = state.mem_read(&addr, size); + for (guard, value) in cases.into_iter().rev() { + merged = ite_value(state.context(), &guard, &value, &merged); + } + state.mem_write(&addr, &merged, size); + } + + CallHookResult::Fallthrough +} + +const DERIVED_RUNTIME_COPY_MIN_SIZE: u32 = 0x100; + +fn apply_derived_runtime_materialization_copy<'ctx>( + state: &mut SymState<'ctx>, + summary: &DerivedFunctionSummary<'ctx>, + call: &CallInfo<'ctx>, + callconv: &CallConv, +) -> bool { + let [case] = summary.cases.as_slice() else { + return false; + }; + if case.guard.simplify().as_bool() != Some(true) { + return false; + } + let Some(write) = case + .memory_writes + .iter() + .find(|write| write.arg_index == 0 && write.offset == 0) + else { + return false; + }; + if write.size < DERIVED_RUNTIME_COPY_MIN_SIZE { + return false; + } + if !summary + .memory_inputs + .iter() + .any(|input| input.arg_index == 1 && input.size >= write.size) + { + return false; + } + let Some(dst) = call.args.first().and_then(SymValue::as_concrete) else { + return false; + }; + let Some(src) = call.args.get(1).and_then(SymValue::as_concrete) else { + return false; + }; + let len = call + .args + .get(2) + .and_then(SymValue::as_concrete) + .unwrap_or(write.size as u64); + if len == 0 || len > write.size as u64 || len > u32::MAX as u64 { + return false; + } + if state.runtime_region_for_pc(dst).is_none() { + return false; + } + + let provenance = RuntimeValueProvenance { + source_addr: src, + size: len as u32, + }; + state.note_runtime_store_copy(dst, len as u32, Some(&provenance)); + if case.return_value.is_some() { + callconv.write_return(state, SymValue::concrete(dst, call.ret_bits)); + } + true +} + +pub(crate) fn evaluate_derived_summary_guidance<'ctx>( + state: &SymState<'ctx>, + summary: &DerivedFunctionSummary<'ctx>, + callconv: &CallConv, + solver: &SymSolver<'ctx>, +) -> DerivedSummaryGuidance { + let exact = matches!(summary.completion, DerivedSummaryCompletion::Exact); + if summary.cases.is_empty() { + return DerivedSummaryGuidance { + summary_known: true, + exact, + feasible_cases: 0, + contradictory: false, + }; + } + + let call = + callconv.collect_call_info(state, summary.arg_count_hint.max(callconv.arg_capacity())); + let substitutions = build_summary_substitutions(state, summary, &call); + let mut feasible_cases = 0; + let mut saw_unknown = false; + + for case in &summary.cases { + let Some(guard) = try_substitute_bool(&case.guard, &substitutions) else { + saw_unknown = true; + continue; + }; + match solver.sat_with_constraint(state, &guard) { + SatResult::Sat => feasible_cases += 1, + SatResult::Unknown => saw_unknown = true, + SatResult::Unsat => {} + } + } + + DerivedSummaryGuidance { + summary_known: true, + exact, + feasible_cases, + contradictory: exact && feasible_cases == 0 && !saw_unknown, + } +} + +fn concrete_with_signed_offset(base: u64, offset: i64) -> u64 { + if offset >= 0 { + base.wrapping_add(offset as u64) + } else { + base.wrapping_sub(offset.unsigned_abs()) + } +} + +fn add_signed_offset<'ctx>( + ctx: &'ctx z3::Context, + base: &SymValue<'ctx>, + offset: i64, + bits: u32, +) -> SymValue<'ctx> { + if offset >= 0 { + base.add(ctx, &SymValue::concrete(offset as u64, bits)) + } else { + base.sub(ctx, &SymValue::concrete(offset.unsigned_abs(), bits)) + } +} + +fn build_summary_substitutions<'ctx>( + state: &SymState<'ctx>, + summary: &DerivedFunctionSummary<'ctx>, + call: &CallInfo<'ctx>, +) -> Vec<(BV, BV)> { + let mut substitutions = Vec::new(); + for (index, symbol) in &summary.arg_symbols { + let Some(actual) = call.args.get(*index) else { + continue; + }; + let adjusted = adjust_bits(state.context(), actual.clone(), symbol.bits()); + substitutions.push(( + symbol.to_bv(state.context()), + adjusted.to_bv(state.context()), + )); + } + for input in &summary.memory_inputs { + let Some(base) = call.args.get(input.arg_index) else { + continue; + }; + let actual = adjust_bits( + state.context(), + state.mem_read(base, input.size), + input.symbol.bits(), + ); + substitutions.push(( + input.symbol.to_bv(state.context()), + actual.to_bv(state.context()), + )); + } + substitutions +} + +fn substitute_bool(ast: &Bool, substitutions: &[(BV, BV)]) -> Bool { + let pairs = substitutions + .iter() + .map(|(from, to)| (from, to)) + .collect::>(); + ast.substitute(&pairs) +} + +fn try_substitute_bool(ast: &Bool, substitutions: &[(BV, BV)]) -> Option { + catch_unwind(AssertUnwindSafe(|| substitute_bool(ast, substitutions))).ok() +} + +fn substitute_value<'ctx>( + _ctx: &'ctx z3::Context, + value: &SymValue<'ctx>, + substitutions: &[(BV, BV)], +) -> SymValue<'ctx> { + match value { + SymValue::Concrete { .. } | SymValue::Unknown { .. } => value.clone(), + SymValue::Symbolic { + ast, bits, taint, .. + } => { + let pairs = substitutions + .iter() + .map(|(from, to)| (from, to)) + .collect::>(); + SymValue::symbolic_tainted(ast.substitute(&pairs), *bits, *taint) + } + } +} + +fn ite_value<'ctx>( + ctx: &'ctx z3::Context, + guard: &Bool, + when_true: &SymValue<'ctx>, + when_false: &SymValue<'ctx>, +) -> SymValue<'ctx> { + let bits = when_true.bits().max(when_false.bits()); + let taint = when_true.get_taint() | when_false.get_taint(); + let true_bv = adjust_bits(ctx, when_true.clone(), bits).to_bv(ctx); + let false_bv = adjust_bits(ctx, when_false.clone(), bits).to_bv(ctx); + SymValue::symbolic_tainted(guard.ite(&true_bv, &false_bv), bits, taint) } fn apply_summary<'ctx>( @@ -391,10 +1650,14 @@ fn interproc_return_value<'ctx>( SummaryReturnRelation::Const(value) => Some(SymValue::concrete(value, call.ret_bits)), SummaryReturnRelation::Global(address) => Some(SymValue::concrete(address, call.ret_bits)), SummaryReturnRelation::HeapAlloc => { - let ret_ast = BV::fresh_const("interproc_heap_ptr", call.ret_bits); - let ret = SymValue::symbolic(ret_ast, call.ret_bits); - state.constrain_ne(&ret, 0); - Some(ret) + let size = call + .args + .first() + .and_then(SymValue::as_concrete) + .filter(|size| *size > 0) + .unwrap_or(0x100); + let (_region, base_addr) = state.allocate_heap_region("interproc_heap", size); + Some(SymValue::concrete(base_addr, call.ret_bits)) } SummaryReturnRelation::Unknown => Some(SymValue::unknown(call.ret_bits)), } @@ -467,6 +1730,21 @@ fn arch_has_register(arch: &ArchSpec, name: &str) -> bool { .any(|reg| reg.name.eq_ignore_ascii_case(name)) } +fn register_aliases(base: &str) -> Vec<&str> { + match base { + "RAX" => vec!["RAX", "EAX"], + "RDI" => vec!["RDI", "EDI"], + "RSI" => vec!["RSI", "ESI"], + "RDX" => vec!["RDX", "EDX"], + "RCX" => vec!["RCX", "ECX"], + "R8" => vec!["R8", "R8D"], + "R9" => vec!["R9", "R9D"], + "RSP" => vec!["RSP", "ESP"], + "RBP" => vec!["RBP", "EBP"], + _ => vec![base], + } +} + fn normalize_core_summary_name(name: &str) -> Option<&'static str> { let normalized_owned = name.trim().to_ascii_lowercase(); let mut normalized = normalized_owned.as_str(); @@ -504,6 +1782,11 @@ fn normalize_core_summary_name(name: &str) -> Option<&'static str> { "free" => Some("free"), "puts" => Some("puts"), "printf" | "__printf_chk" => Some("printf"), + "read" | "__read_chk" => Some("read"), + "isatty" => Some("isatty"), + "sleep" => Some("sleep"), + "usleep" => Some("usleep"), + "nanosleep" => Some("nanosleep"), "exit" | "_exit" => Some("exit"), _ => { if normalized.starts_with("strlen") { @@ -518,6 +1801,16 @@ fn normalize_core_summary_name(name: &str) -> Option<&'static str> { Some("memset") } else if normalized.starts_with("printf") || normalized == "__printf_chk" { Some("printf") + } else if normalized.starts_with("read") { + Some("read") + } else if normalized.starts_with("isatty") { + Some("isatty") + } else if normalized == "sleep" || normalized.ends_with("sleep") { + Some("sleep") + } else if normalized.starts_with("usleep") { + Some("usleep") + } else if normalized.starts_with("nanosleep") { + Some("nanosleep") } else if normalized.starts_with("puts") { Some("puts") } else if normalized == "malloc" || normalized.ends_with("malloc") { @@ -574,12 +1867,20 @@ fn adjust_bits<'ctx>(ctx: &'ctx z3::Context, value: SymValue<'ctx>, bits: u32) - /// memcpy(dst, src, n) summary. pub struct MemcpySummary { max_copy: u64, + byte_policy: ByteSummaryPolicy, } impl MemcpySummary { /// Create a memcpy summary with an upper bound on copy size. pub fn new(max_copy: u64) -> Self { - Self { max_copy } + Self::with_policy(max_copy, ByteSummaryPolicy::precise(max_copy)) + } + + fn with_policy(max_copy: u64, byte_policy: ByteSummaryPolicy) -> Self { + Self { + max_copy, + byte_policy, + } } } @@ -608,7 +1909,19 @@ impl<'ctx> FunctionSummary<'ctx> for MemcpySummary { .get(2) .cloned() .unwrap_or_else(|| SymValue::unknown(call.arg_bits)); - copy_bytes(state, &dst, &src, &n, self.max_copy); + copy_bytes(state, &dst, &src, &n, self.max_copy, self.byte_policy); + if let (Some(dst_addr), Some(src_addr), Some(n)) = + (dst.as_concrete(), src.as_concrete(), n.as_concrete()) + && n > 0 + && n <= self.max_copy + && n <= u32::MAX as u64 + { + let provenance = RuntimeValueProvenance { + source_addr: src_addr, + size: n as u32, + }; + state.note_runtime_store_copy(dst_addr, n as u32, Some(&provenance)); + } SummaryEffect::Return(Some(dst)) } } @@ -776,12 +2089,20 @@ impl<'ctx> FunctionSummary<'ctx> for MemcmpSummary { /// memset(dst, c, n) summary. pub struct MemsetSummary { max_set: u64, + byte_policy: ByteSummaryPolicy, } impl MemsetSummary { /// Create a memset summary with an upper bound on set size. pub fn new(max_set: u64) -> Self { - Self { max_set } + Self::with_policy(max_set, ByteSummaryPolicy::precise(max_set)) + } + + fn with_policy(max_set: u64, byte_policy: ByteSummaryPolicy) -> Self { + Self { + max_set, + byte_policy, + } } } @@ -811,7 +2132,7 @@ impl<'ctx> FunctionSummary<'ctx> for MemsetSummary { .cloned() .unwrap_or_else(|| SymValue::unknown(call.arg_bits)); - set_bytes(state, &dst, &c, &n, self.max_set); + set_bytes(state, &dst, &c, &n, self.max_set, self.byte_policy); SummaryEffect::Return(Some(dst)) } } @@ -896,6 +2217,141 @@ impl<'ctx> FunctionSummary<'ctx> for PrintfSummaryBasic { } } +/// read(fd, buf, count) summary. +#[derive(Default)] +pub struct ReadSummary; + +impl ReadSummary { + pub fn new() -> Self { + Self + } +} + +impl<'ctx> FunctionSummary<'ctx> for ReadSummary { + fn name(&self) -> &'static str { + "read" + } + + fn arity(&self) -> usize { + 3 + } + + fn execute(&self, state: &mut SymState<'ctx>, call: &CallInfo<'ctx>) -> SummaryEffect<'ctx> { + let fd = call + .args + .first() + .and_then(SymValue::as_concrete) + .map(|value| value as i32); + let buf = call + .args + .get(1) + .cloned() + .unwrap_or_else(|| SymValue::unknown(call.arg_bits)); + let count = call + .args + .get(2) + .and_then(SymValue::as_concrete) + .unwrap_or(0) as usize; + + let Some(fd) = fd else { + let ret = SymValue::unknown(call.ret_bits); + return SummaryEffect::Return(Some(ret)); + }; + + let Some(bytes) = state.read_symbolic_fd_bytes(fd, count) else { + return SummaryEffect::Return(Some(SymValue::concrete(0, call.ret_bits))); + }; + + if let Some(base) = buf.as_concrete() { + for (idx, byte) in bytes.iter().enumerate() { + let addr = SymValue::concrete(base.saturating_add(idx as u64), call.arg_bits); + state.mem_write(&addr, byte, 1); + } + } + + SummaryEffect::Return(Some(SymValue::concrete(bytes.len() as u64, call.ret_bits))) + } +} + +/// isatty(fd) summary. +#[derive(Default)] +pub struct IsattySummary; + +impl IsattySummary { + pub fn new() -> Self { + Self + } +} + +impl<'ctx> FunctionSummary<'ctx> for IsattySummary { + fn name(&self) -> &'static str { + "isatty" + } + + fn arity(&self) -> usize { + 1 + } + + fn execute(&self, state: &mut SymState<'ctx>, call: &CallInfo<'ctx>) -> SummaryEffect<'ctx> { + let fd = call + .args + .first() + .and_then(SymValue::as_concrete) + .map(|value| value as i32) + .unwrap_or(-1); + let ret = if state.is_tty_fd(fd) { 1 } else { 0 }; + SummaryEffect::Return(Some(SymValue::concrete(ret, call.ret_bits))) + } +} + +/// sleep/usleep/nanosleep summary. +pub struct SleepSummary { + name: &'static str, + arity: usize, +} + +impl SleepSummary { + pub fn sleep() -> Self { + Self { + name: "sleep", + arity: 1, + } + } + + pub fn usleep() -> Self { + Self { + name: "usleep", + arity: 1, + } + } + + pub fn nanosleep() -> Self { + Self { + name: "nanosleep", + arity: 2, + } + } +} + +impl<'ctx> FunctionSummary<'ctx> for SleepSummary { + fn name(&self) -> &'static str { + self.name + } + + fn arity(&self) -> usize { + self.arity + } + + fn execute(&self, state: &mut SymState<'ctx>, call: &CallInfo<'ctx>) -> SummaryEffect<'ctx> { + let ret = if state.skip_sleep_calls() { + SymValue::concrete(0, call.ret_bits) + } else { + SymValue::unknown(call.ret_bits) + }; + SummaryEffect::Return(Some(ret)) + } +} + /// malloc(size) summary. #[derive(Default)] pub struct MallocSummary; @@ -991,15 +2447,34 @@ fn copy_bytes<'ctx>( src: &SymValue<'ctx>, n: &SymValue<'ctx>, max_copy: u64, + byte_policy: ByteSummaryPolicy, ) { let ctx = state.context(); let n_concrete = n.as_concrete(); + if n_concrete == Some(0) { + return; + } let copy_len = n_concrete.unwrap_or(max_copy).min(max_copy); if n_concrete.is_none() { state.constrain_range(n, 0, max_copy); } + if !byte_policy.use_precise_model(n_concrete) { + let src_byte = if src.as_concrete().is_some() { + let read = state.mem_read(src, 1); + read.with_taint(read.get_taint() | src.get_taint() | n.get_taint()) + } else { + SymValue::symbolic_tainted( + BV::fresh_const("memcpy_byte", 8), + 8, + src.get_taint() | n.get_taint(), + ) + }; + state.mem_write(dst, &src_byte, 1); + return; + } + for offset in 0..copy_len { let offset_val = SymValue::concrete(offset, dst.bits()); let dst_addr = dst.add(ctx, &offset_val); @@ -1029,9 +2504,13 @@ fn set_bytes<'ctx>( c: &SymValue<'ctx>, n: &SymValue<'ctx>, max_set: u64, + byte_policy: ByteSummaryPolicy, ) { let ctx = state.context(); let n_concrete = n.as_concrete(); + if n_concrete == Some(0) { + return; + } let set_len = n_concrete.unwrap_or(max_set).min(max_set); if n_concrete.is_none() { @@ -1044,6 +2523,11 @@ fn set_bytes<'ctx>( c.extract(ctx, 7, 0).with_taint(c.get_taint()) }; + if !byte_policy.use_precise_model(n_concrete) { + state.mem_write(dst, &c_byte, 1); + return; + } + for offset in 0..set_len { let offset_val = SymValue::concrete(offset, dst.bits()); let dst_addr = dst.add(ctx, &offset_val); @@ -1073,3 +2557,215 @@ fn constrain_ret_tristate<'ctx>(state: &mut SymState<'ctx>, ret: &SymValue<'ctx> let cond = ret_bv.eq(&neg_one) | ret_bv.eq(&zero) | ret_bv.eq(&one); state.add_constraint(cond); } + +#[cfg(test)] +mod tests { + use super::*; + use r2il::{R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; + + fn test_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("RAX", 0, 8)); + arch + } + + fn const_vn(value: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: value, + size, + meta: None, + } + } + + #[test] + fn callconv_accepts_x86_64_arch_specs_with_bit_sized_addr_width() { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 64; + assert!(CallConv::for_arch_spec(&arch).is_some()); + assert!(SummaryRegistry::with_core_for_arch(&arch).is_some()); + assert!( + SummaryRegistry::with_profile_for_arch(&arch, SummaryProfile::PathListing).is_some() + ); + } + + #[test] + fn memcpy_summary_path_listing_summarizes_symbolic_lengths() { + let ctx = z3::Context::thread_local(); + let src = SymValue::concrete(0x1000, 64); + let dst = SymValue::concrete(0x2000, 64); + let n = SymValue::symbolic(BV::fresh_const("n", 64), 64); + + let mut precise_state = SymState::new(&ctx, 0); + precise_state.mem_write( + &src, + &SymValue::symbolic_tainted(BV::fresh_const("src_base", 8), 8, 0x20), + 1, + ); + let src_far = src.add(&ctx, &SymValue::concrete(5, 64)); + precise_state.mem_write( + &src_far, + &SymValue::symbolic_tainted(BV::fresh_const("src_far", 8), 8, 0x40), + 1, + ); + let precise_summary = MemcpySummary::new(0x40); + let call = CallInfo { + args: vec![dst.clone(), src.clone(), n.clone()], + arg_bits: 64, + ret_bits: 64, + }; + let _ = precise_summary.execute(&mut precise_state, &call); + let precise_far = precise_state.mem_read(&dst.add(&ctx, &SymValue::concrete(5, 64)), 1); + assert_eq!(precise_far.get_taint(), 0x40); + + let mut path_listing_state = SymState::new(&ctx, 0); + path_listing_state.mem_write( + &src, + &SymValue::symbolic_tainted(BV::fresh_const("src_base_pl", 8), 8, 0x20), + 1, + ); + let src_far = src.add(&ctx, &SymValue::concrete(5, 64)); + path_listing_state.mem_write( + &src_far, + &SymValue::symbolic_tainted(BV::fresh_const("src_far_pl", 8), 8, 0x40), + 1, + ); + let path_listing_summary = MemcpySummary::with_policy( + 0x40, + ByteSummaryPolicy::summarized(PATH_LIST_PRECISE_BYTE_LIMIT), + ); + let _ = path_listing_summary.execute(&mut path_listing_state, &call); + let path_listing_far = + path_listing_state.mem_read(&dst.add(&ctx, &SymValue::concrete(5, 64)), 1); + assert_eq!(path_listing_far.get_taint(), 0); + let path_listing_base = path_listing_state.mem_read(&dst, 1); + assert_eq!(path_listing_base.get_taint(), 0x20); + } + + #[test] + fn memcpy_summary_records_runtime_materialization_provenance() { + let ctx = z3::Context::thread_local(); + let mut state = SymState::new(&ctx, 0); + let (_region, runtime_base) = state.allocate_heap_region("jit", 0x1000); + state.register_runtime_region_alias(runtime_base, 0x1000, true); + + let summary = MemcpySummary::new(0x1000); + let call = CallInfo { + args: vec![ + SymValue::concrete(runtime_base, 64), + SymValue::concrete(0x14009c000, 64), + SymValue::concrete(0x1000, 64), + ], + arg_bits: 64, + ret_bits: 64, + }; + let _ = summary.execute(&mut state, &call); + + let region = state + .runtime_region_for_pc(runtime_base) + .expect("runtime region should remain registered"); + assert_eq!(region.source_base, Some(0x14009c000)); + } + + #[test] + fn prepared_function_scope_with_prepared_root_rebinds_root_only() { + let blocks_a = vec![R2ILBlock { + addr: 0x1000, + size: 1, + ops: vec![R2ILOp::Return { + target: const_vn(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let blocks_b = vec![R2ILBlock { + addr: 0x1000, + size: 1, + ops: vec![R2ILOp::Return { + target: const_vn(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + ops: vec![R2ILOp::Return { + target: const_vn(2, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let root_a = SsaArtifact::for_symbolic(&blocks_a, Some(&test_arch())).expect("root a"); + let root_b = SsaArtifact::for_symbolic(&blocks_b, Some(&test_arch())).expect("root b"); + let helper = SsaArtifact::for_symbolic(&helper_blocks, Some(&test_arch())).expect("helper"); + + let scope = PreparedFunctionScope::new( + 0x1000, + vec![ + ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: None, + prepared: root_a, + }, + ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("helper".to_string()), + prepared: helper.clone(), + }, + ], + ) + .expect("scope"); + + let rebound = scope.with_prepared_root(&root_b).expect("rebound scope"); + let rebound_root = rebound.root().expect("rebound root"); + assert_eq!(rebound_root.prepared.entry, root_b.entry); + assert_eq!(rebound_root.prepared.function().blocks().count(), 1); + assert_eq!( + rebound + .functions() + .get(&InterprocFunctionId(0x2000)) + .expect("helper") + .prepared + .entry, + helper.entry + ); + } + + #[test] + fn derived_summary_guidance_adjusts_arg_widths_before_substitution() { + let ctx = z3::Context::thread_local(); + let mut state = SymState::new(&ctx, 0); + state.set_register( + "RDI_0", + SymValue::symbolic(BV::fresh_const("call_arg", 64), 64), + ); + + let helper_arg = SymValue::symbolic(BV::fresh_const("helper_arg", 32), 32); + let summary = DerivedFunctionSummary { + id: InterprocFunctionId(0x401000), + name: Some("sym.helper_symbolic_zero".to_string()), + arg_count_hint: 1, + arg_symbols: vec![(0, helper_arg.clone())], + memory_inputs: Vec::new(), + cases: vec![DerivedSummaryCase { + guard: helper_arg.to_bv(&ctx).eq(helper_arg.to_bv(&ctx)), + return_value: Some(SymValue::concrete(0, 32)), + memory_writes: Vec::new(), + }], + completion: DerivedSummaryCompletion::Exact, + }; + + let guidance = evaluate_derived_summary_guidance( + &state, + &summary, + &CallConv::x86_64_sysv(), + &SymSolver::new(&ctx), + ); + + assert!(guidance.summary_known); + assert_eq!(guidance.feasible_cases, 1); + assert!(!guidance.contradictory); + } +} diff --git a/crates/r2sym/src/solver.rs b/crates/r2sym/src/solver.rs index 13aa123..3e24f1e 100644 --- a/crates/r2sym/src/solver.rs +++ b/crates/r2sym/src/solver.rs @@ -3,13 +3,15 @@ //! This module provides a high-level interface to Z3 for checking //! path feasibility and extracting concrete values. -use std::collections::HashMap; +use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; +use std::rc::Rc; use std::time::Duration; -use z3::ast::{BV, Bool}; -use z3::{Context, Model, Params, Solver}; +use z3::ast::{Ast, BV, Bool, Dynamic}; +use z3::{AstKind, Context, DeclKind, Model, Params, Solver}; -use crate::state::SymState; +use crate::state::{ConstraintCursorKey, SymState}; use crate::value::SymValue; /// Result of a satisfiability check. @@ -33,41 +35,310 @@ impl From for SatResult { } } +/// Lightweight counters for solver-heavy symbolic execution workflows. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct SolverStats { + /// Number of state satisfiability queries. + pub sat_queries: usize, + /// Number of satisfiability queries answered from the cache. + pub sat_cache_hits: usize, + /// Number of satisfiability queries that required a solver check. + pub sat_cache_misses: usize, + /// Number of model-building solve requests. + pub solve_calls: usize, + /// Number of solve calls that returned early from a cached UNSAT result. + pub solve_unsat_shortcuts: usize, +} + /// A wrapper around Z3's solver with convenience methods. pub struct SymSolver<'ctx> { /// The Z3 context. ctx: &'ctx Context, - /// The underlying Z3 solver. + /// The user-visible manual solver. solver: Solver, + /// Incremental solver for exact state-cursor queries. + state_solver: Solver, + /// Incremental solver for query-local selected constraints. + selection_solver: Solver, + /// Scratch solver for sliced one-shot queries. + scratch_solver: Solver, /// Timeout in milliseconds (0 = no timeout). - _timeout_ms: u32, + timeout_ms: u32, + /// Memoized satisfiability results for state queries. + sat_cache: RefCell>, + /// Cached dependency and partition analysis for cursor-based states. + analysis_cache: RefCell, + /// Current assertion stack for the incremental state solver. + state_session: RefCell, + /// Current assertion set for the incremental selection solver. + selection_session: RefCell, + /// Internal counters used for perf/debug summaries. + stats: RefCell, +} + +#[derive(Debug, Clone)] +struct SolverCursorSession { + asserted_stack: Vec, +} + +#[derive(Debug, Clone, Default)] +struct SelectionSolverSession { + asserted_key: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SolverMode { + ExploreFast, + QueryDeep, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +struct DependencyAtom(Dynamic); + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct UnsignedInterval { + min: u64, + max: u64, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct AbstractValueFacts { + exact: Option, + interval: Option, + nonzero: bool, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct AbstractFacts { + unsat: bool, + values: HashMap, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +struct ConstraintSelectionKey { + state_key: ConstraintCursorKey, + full_state: bool, + partition_ids: Vec, +} + +#[derive(Clone)] +struct ConstraintSelection { + key: ConstraintSelectionKey, + constraints: Vec, + abstract_facts: AbstractFacts, + is_full_state: bool, +} + +#[derive(Clone, Default)] +struct ConstraintPartition { + id: usize, + constraints: Vec, + atoms: HashSet, + abstract_facts: AbstractFacts, +} + +#[derive(Clone, Default)] +struct StateConstraintAnalysis { + ground_constraints: Vec, + ground_abstract_facts: AbstractFacts, + partitions: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +struct PartitionSatCacheKey { + state_key: ConstraintCursorKey, + partition_id: usize, +} + +#[derive(Clone)] +struct CursorConstraintFacts { + parent: ConstraintCursorKey, + deps: HashSet, + abstract_delta: AbstractFacts, + is_ground: bool, +} + +impl Default for CursorConstraintFacts { + fn default() -> Self { + Self { + parent: ConstraintCursorKey::ROOT, + deps: HashSet::new(), + abstract_delta: AbstractFacts::default(), + is_ground: true, + } + } +} + +#[derive(Default)] +struct ConstraintAnalysisCache { + cursor_facts: HashMap, + state_cache: HashMap>, + ground_sat_cache: HashMap, + partition_sat_cache: HashMap, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum DeepQueryKind { + Predicate, + Model, +} + +impl Default for SolverCursorSession { + fn default() -> Self { + Self { + asserted_stack: vec![ConstraintCursorKey::ROOT], + } + } +} + +impl AbstractValueFacts { + fn update_exact(&mut self, value: u64) { + if let Some(existing) = self.exact { + if existing != value { + self.interval = Some(UnsignedInterval { min: 1, max: 0 }); + } + } else { + self.exact = Some(value); + } + self.tighten_interval(value, value); + if value != 0 { + self.nonzero = true; + } + } + + fn tighten_interval(&mut self, min: u64, max: u64) { + let interval = self.interval.get_or_insert(UnsignedInterval { + min: 0, + max: u64::MAX, + }); + interval.min = interval.min.max(min); + interval.max = interval.max.min(max); + } + + fn mark_nonzero(&mut self) { + self.nonzero = true; + if let Some(0) = self.exact { + self.interval = Some(UnsignedInterval { min: 1, max: 0 }); + } else if let Some(interval) = &mut self.interval + && interval.min == 0 + && interval.max > 0 + { + interval.min = 1; + } + } + + fn is_unsat(&self) -> bool { + matches!(self.interval, Some(UnsignedInterval { min, max }) if min > max) + } +} + +impl AbstractFacts { + fn merge(&mut self, other: &Self) { + if self.unsat || other.unsat { + self.unsat = true; + return; + } + for (atom, other_facts) in &other.values { + let facts = self.values.entry(atom.clone()).or_default(); + if let Some(value) = other_facts.exact { + facts.update_exact(value); + } + if let Some(interval) = &other_facts.interval { + facts.tighten_interval(interval.min, interval.max); + } + if other_facts.nonzero { + facts.mark_nonzero(); + } + if facts.is_unsat() { + self.unsat = true; + return; + } + } + } + + fn record_eq(&mut self, atom: DependencyAtom, value: u64) { + let facts = self.values.entry(atom).or_default(); + facts.update_exact(value); + if facts.is_unsat() { + self.unsat = true; + } + } + + fn record_ne(&mut self, atom: DependencyAtom, value: u64) { + let facts = self.values.entry(atom).or_default(); + if facts.exact == Some(value) { + self.unsat = true; + return; + } + if value == 0 { + facts.mark_nonzero(); + if facts.is_unsat() { + self.unsat = true; + } + } + } + + fn record_min(&mut self, atom: DependencyAtom, value: u64) { + let facts = self.values.entry(atom).or_default(); + facts.tighten_interval(value, u64::MAX); + if facts.is_unsat() { + self.unsat = true; + } + } + + fn record_max(&mut self, atom: DependencyAtom, value: u64) { + let facts = self.values.entry(atom).or_default(); + facts.tighten_interval(0, value); + if facts.is_unsat() { + self.unsat = true; + } + } } impl<'ctx> SymSolver<'ctx> { + fn configured_solver(timeout_ms: u32) -> Solver { + let solver = Solver::new(); + if timeout_ms != 0 { + let mut params = Params::new(); + params.set_u32("timeout", timeout_ms); + solver.set_params(¶ms); + } + solver + } + /// Create a new solver. pub fn new(ctx: &'ctx Context) -> Self { - let solver = Solver::new(); Self { ctx, - solver, - _timeout_ms: 0, + solver: Self::configured_solver(0), + state_solver: Self::configured_solver(0), + selection_solver: Self::configured_solver(0), + scratch_solver: Self::configured_solver(0), + timeout_ms: 0, + sat_cache: RefCell::new(HashMap::new()), + analysis_cache: RefCell::new(ConstraintAnalysisCache::default()), + state_session: RefCell::new(SolverCursorSession::default()), + selection_session: RefCell::new(SelectionSolverSession::default()), + stats: RefCell::new(SolverStats::default()), } } /// Create a solver with a timeout. pub fn with_timeout(ctx: &'ctx Context, timeout: Duration) -> Self { - let solver = Solver::new(); let timeout_ms = timeout.as_millis() as u32; - // Set timeout parameter - let mut params = Params::new(); - params.set_u32("timeout", timeout_ms); - solver.set_params(¶ms); - Self { ctx, - solver, - _timeout_ms: timeout_ms, + solver: Self::configured_solver(timeout_ms), + state_solver: Self::configured_solver(timeout_ms), + selection_solver: Self::configured_solver(timeout_ms), + scratch_solver: Self::configured_solver(timeout_ms), + timeout_ms, + sat_cache: RefCell::new(HashMap::new()), + analysis_cache: RefCell::new(ConstraintAnalysisCache::default()), + state_session: RefCell::new(SolverCursorSession::default()), + selection_session: RefCell::new(SelectionSolverSession::default()), + stats: RefCell::new(SolverStats::default()), } } @@ -78,11 +349,67 @@ impl<'ctx> SymSolver<'ctx> { /// Add a constraint to the solver. pub fn assert(&self, constraint: &Bool) { - self.solver.assert(constraint); + self.clear_sat_cache(); + self.solver_assert(constraint); } /// Add multiple constraints. pub fn assert_all(&self, constraints: &[Bool]) { + if !constraints.is_empty() { + self.clear_sat_cache(); + } + self.solver_assert_all(constraints); + } + + /// Return current solver/cache counters. + pub fn stats(&self) -> SolverStats { + self.stats.borrow().clone() + } + + /// Reset solver/cache counters without touching constraints. + pub fn clear_stats(&self) { + *self.stats.borrow_mut() = SolverStats::default(); + } + + /// Drop memoized satisfiability results. + pub fn clear_sat_cache(&self) { + self.sat_cache.borrow_mut().clear(); + } + + fn reset_state_solver(&self) { + self.state_solver.reset(); + if self.timeout_ms != 0 { + let mut params = Params::new(); + params.set_u32("timeout", self.timeout_ms); + self.state_solver.set_params(¶ms); + } + *self.state_session.borrow_mut() = SolverCursorSession::default(); + } + + fn reset_scratch_solver(&self) { + self.scratch_solver.reset(); + if self.timeout_ms != 0 { + let mut params = Params::new(); + params.set_u32("timeout", self.timeout_ms); + self.scratch_solver.set_params(¶ms); + } + } + + fn reset_selection_solver(&self) { + self.selection_solver.reset(); + if self.timeout_ms != 0 { + let mut params = Params::new(); + params.set_u32("timeout", self.timeout_ms); + self.selection_solver.set_params(¶ms); + } + *self.selection_session.borrow_mut() = SelectionSolverSession::default(); + } + + fn solver_assert(&self, constraint: &Bool) { + self.solver.assert(constraint); + } + + fn solver_assert_all(&self, constraints: &[Bool]) { for c in constraints { self.solver.assert(c); } @@ -105,41 +432,950 @@ impl<'ctx> SymSolver<'ctx> { /// Push a new scope (for backtracking). pub fn push(&self) { + self.clear_sat_cache(); self.solver.push(); } /// Pop a scope. pub fn pop(&self, n: u32) { + self.clear_sat_cache(); self.solver.pop(n); } /// Reset the solver. pub fn reset(&self) { + self.clear_sat_cache(); self.solver.reset(); } + fn state_sat_cache_key(&self, state: &SymState<'ctx>) -> ConstraintCursorKey { + state.constraint_cursor_key() + } + + fn ensure_state_solver_cursor(&self, state: &SymState<'ctx>) { + let target_suffix = match state.constraint_suffix_from_cursor(ConstraintCursorKey::ROOT) { + Some(suffix) => suffix, + None => { + self.reset_state_solver(); + return; + } + }; + let target_keys: Vec<_> = std::iter::once(ConstraintCursorKey::ROOT) + .chain(target_suffix.iter().map(|(key, _)| *key)) + .collect(); + + let mut session = self.state_session.borrow_mut(); + let common_prefix_len = session + .asserted_stack + .iter() + .zip(target_keys.iter()) + .take_while(|(a, b)| a == b) + .count(); + + if common_prefix_len == 0 { + drop(session); + self.reset_state_solver(); + session = self.state_session.borrow_mut(); + } else { + let current_len = session.asserted_stack.len(); + if current_len > common_prefix_len { + self.state_solver + .pop((current_len - common_prefix_len) as u32); + session.asserted_stack.truncate(common_prefix_len); + } + } + + for (key, constraint) in target_suffix + .iter() + .skip(session.asserted_stack.len().saturating_sub(1)) + { + self.state_solver.push(); + self.state_solver.assert(constraint); + session.asserted_stack.push(*key); + } + } + + fn eval_value_in_model(&self, model: &Model, value: &SymValue<'ctx>) -> Option { + let bv = value.to_bv(self.ctx); + model.eval(&bv, true)?.as_u64() + } + + fn extract_dependency_atom(expr: &Dynamic) -> Option { + let is_uninterpreted_leaf = expr.is_const() + && matches!( + expr.safe_decl().map(|decl| decl.kind()), + Ok(DeclKind::UNINTERPRETED) + ); + if is_uninterpreted_leaf { + return Some(DependencyAtom(expr.clone())); + } + + if expr.children().is_empty() && !matches!(expr.kind(), AstKind::Numeral) { + return Some(DependencyAtom(expr.clone())); + } + + None + } + + fn extract_const_u64(expr: &Dynamic) -> Option { + expr.as_bv()?.as_u64() + } + + fn extract_leaf_const_pair(left: &Dynamic, right: &Dynamic) -> Option<(DependencyAtom, u64)> { + if let (Some(atom), Some(value)) = ( + Self::extract_dependency_atom(left), + Self::extract_const_u64(right), + ) { + return Some((atom, value)); + } + if let (Some(value), Some(atom)) = ( + Self::extract_const_u64(left), + Self::extract_dependency_atom(right), + ) { + return Some((atom, value)); + } + None + } + + fn apply_abstract_constraint(facts: &mut AbstractFacts, expr: &Dynamic, positive: bool) { + if facts.unsat { + return; + } + + if let Some(bool_ast) = expr.as_bool() + && let Some(value) = bool_ast.as_bool() + { + if value != positive { + facts.unsat = true; + } + return; + } + + let decl = match expr.safe_decl() { + Ok(decl) => decl, + Err(_) => return, + }; + let children = expr.children(); + + match (decl.kind(), positive) { + (DeclKind::AND, true) => { + for child in &children { + Self::apply_abstract_constraint(facts, child, true); + if facts.unsat { + return; + } + } + } + (DeclKind::NOT, _) => { + if let Some(child) = children.first() { + Self::apply_abstract_constraint(facts, child, !positive); + } + } + (DeclKind::EQ, true) if children.len() == 2 => { + if let Some((atom, value)) = + Self::extract_leaf_const_pair(&children[0], &children[1]) + { + facts.record_eq(atom, value); + } + } + (DeclKind::EQ, false) if children.len() == 2 => { + if let Some((atom, value)) = + Self::extract_leaf_const_pair(&children[0], &children[1]) + { + facts.record_ne(atom, value); + } + } + (DeclKind::ULEQ, true) if children.len() == 2 => { + if let Some((atom, value)) = + Self::extract_leaf_const_pair(&children[0], &children[1]) + { + if Self::extract_dependency_atom(&children[0]).is_some() { + facts.record_max(atom, value); + } else { + facts.record_min(atom, value); + } + } + } + (DeclKind::UGEQ, true) if children.len() == 2 => { + if let Some((atom, value)) = + Self::extract_leaf_const_pair(&children[0], &children[1]) + { + if Self::extract_dependency_atom(&children[0]).is_some() { + facts.record_min(atom, value); + } else { + facts.record_max(atom, value); + } + } + } + (DeclKind::ULT, true) if children.len() == 2 => { + if let Some((atom, value)) = + Self::extract_leaf_const_pair(&children[0], &children[1]) + { + if Self::extract_dependency_atom(&children[0]).is_some() { + if value == 0 { + facts.unsat = true; + } else { + facts.record_max(atom, value - 1); + } + } else { + facts.record_min(atom, value.saturating_add(1)); + } + } + } + (DeclKind::UGT, true) if children.len() == 2 => { + if let Some((atom, value)) = + Self::extract_leaf_const_pair(&children[0], &children[1]) + { + if Self::extract_dependency_atom(&children[0]).is_some() { + facts.record_min(atom, value.saturating_add(1)); + } else if value == 0 { + facts.unsat = true; + } else { + facts.record_max(atom, value - 1); + } + } + } + _ => {} + } + } + + fn abstract_facts_for_constraints(constraints: &[Bool]) -> AbstractFacts { + let mut facts = AbstractFacts::default(); + for constraint in constraints { + Self::apply_abstract_constraint(&mut facts, &Dynamic::from_ast(constraint), true); + if facts.unsat { + break; + } + } + facts + } + + fn abstract_summary_for_value( + &self, + facts: &AbstractFacts, + value: &SymValue<'ctx>, + ) -> Option { + match value { + SymValue::Concrete { value, .. } => Some(AbstractValueFacts { + exact: Some(*value), + interval: Some(UnsignedInterval { + min: *value, + max: *value, + }), + nonzero: *value != 0, + }), + SymValue::Symbolic { ast, .. } => { + let atom = Self::extract_dependency_atom(&Dynamic::from_ast(ast))?; + facts.values.get(&atom).cloned() + } + SymValue::Unknown { .. } => None, + } + } + + fn collect_dynamic_dependencies( + expr: &Dynamic, + out: &mut HashSet, + seen: &mut HashSet, + ) { + if !seen.insert(expr.clone()) { + return; + } + + if let Some(bool_ast) = expr.as_bool() + && bool_ast.as_bool().is_some() + { + return; + } + + if let Some(atom) = Self::extract_dependency_atom(expr) { + out.insert(atom); + return; + } + + let children = expr.children(); + if children.is_empty() { + if !matches!(expr.kind(), AstKind::Numeral) { + out.insert(DependencyAtom(expr.clone())); + } + return; + } + + for child in children { + Self::collect_dynamic_dependencies(&child, out, seen); + } + } + + fn expr_dependencies_bool(&self, expr: &Bool) -> HashSet { + let mut deps = HashSet::new(); + let mut seen = HashSet::new(); + Self::collect_dynamic_dependencies(&Dynamic::from_ast(expr), &mut deps, &mut seen); + deps + } + + fn expr_dependencies_bv(&self, expr: &BV) -> HashSet { + let mut deps = HashSet::new(); + let mut seen = HashSet::new(); + Self::collect_dynamic_dependencies(&Dynamic::from_ast(expr), &mut deps, &mut seen); + deps + } + + fn state_constraint_pairs(&self, state: &SymState<'ctx>) -> Vec<(ConstraintCursorKey, Bool)> { + state + .constraint_suffix_from_cursor(ConstraintCursorKey::ROOT) + .unwrap_or_default() + } + + fn constraint_facts_for_key( + &self, + key: ConstraintCursorKey, + parent: ConstraintCursorKey, + constraint: &Bool, + ) -> CursorConstraintFacts { + if let Some(cached) = self.analysis_cache.borrow().cursor_facts.get(&key) { + return cached.clone(); + } + + let deps = self.expr_dependencies_bool(constraint); + let abstract_delta = Self::abstract_facts_for_constraints(std::slice::from_ref(constraint)); + let facts = CursorConstraintFacts { + parent, + is_ground: deps.is_empty(), + deps, + abstract_delta, + }; + self.analysis_cache + .borrow_mut() + .cursor_facts + .insert(key, facts.clone()); + facts + } + + fn ensure_cursor_facts( + &self, + state: &SymState<'ctx>, + ) -> Vec<( + ConstraintCursorKey, + ConstraintCursorKey, + CursorConstraintFacts, + )> { + let pairs = self.state_constraint_pairs(state); + let mut out = Vec::with_capacity(pairs.len()); + let mut parent = ConstraintCursorKey::ROOT; + for (key, constraint) in pairs { + let facts = self.constraint_facts_for_key(key, parent, &constraint); + parent = key; + out.push((key, facts.parent, facts)); + } + out + } + + fn extend_state_constraint_analysis( + &self, + base: &StateConstraintAnalysis, + constraint: &Bool, + facts: &CursorConstraintFacts, + ) -> StateConstraintAnalysis { + let mut next = base.clone(); + if facts.is_ground { + next.ground_constraints.push(constraint.clone()); + next.ground_abstract_facts.merge(&facts.abstract_delta); + return next; + } + + let mut merged_partition = ConstraintPartition { + id: 0, + constraints: vec![constraint.clone()], + atoms: facts.deps.clone(), + abstract_facts: facts.abstract_delta.clone(), + }; + let mut first_match = None; + let mut rebuilt = Vec::with_capacity(next.partitions.len() + 1); + for partition in next.partitions { + if partition.atoms.is_disjoint(&facts.deps) { + rebuilt.push(partition); + continue; + } + if first_match.is_none() { + first_match = Some(rebuilt.len()); + } + merged_partition.constraints.extend(partition.constraints); + merged_partition.atoms.extend(partition.atoms); + merged_partition + .abstract_facts + .merge(&partition.abstract_facts); + } + + let insert_at = first_match.unwrap_or(rebuilt.len()); + rebuilt.insert(insert_at, merged_partition); + for (id, partition) in rebuilt.iter_mut().enumerate() { + partition.id = id; + } + next.partitions = rebuilt; + next + } + + fn state_constraint_pairs_with_facts( + &self, + state: &SymState<'ctx>, + ) -> Vec<( + ConstraintCursorKey, + ConstraintCursorKey, + Bool, + CursorConstraintFacts, + )> { + self.state_constraint_pairs(state) + .into_iter() + .zip(self.ensure_cursor_facts(state)) + .map(|((key, constraint), (fact_key, parent, facts))| { + debug_assert_eq!(key, fact_key); + (key, parent, constraint, facts) + }) + .collect() + } + + fn distinct_dependency_atom_count(&self, state: &SymState<'ctx>) -> usize { + let mut atoms = HashSet::new(); + for (_, _, facts) in self.ensure_cursor_facts(state) { + atoms.extend(facts.deps); + } + atoms.len() + } + + fn state_constraint_analysis(&self, state: &SymState<'ctx>) -> Rc { + let cache_key = state.constraint_cursor_key(); + if let Some(cached) = self.analysis_cache.borrow().state_cache.get(&cache_key) { + return cached.clone(); + } + + let pairs = self.state_constraint_pairs_with_facts(state); + if pairs.is_empty() { + let empty = Rc::new(StateConstraintAnalysis::default()); + self.analysis_cache + .borrow_mut() + .state_cache + .insert(cache_key, empty.clone()); + return empty; + } + + let mut cached_prefix = StateConstraintAnalysis::default(); + let mut start_index = 0usize; + for (index, (key, _, _, _)) in pairs.iter().enumerate().rev() { + if let Some(existing) = self.analysis_cache.borrow().state_cache.get(key).cloned() { + cached_prefix = (*existing).clone(); + start_index = index + 1; + break; + } + } + + let mut current = cached_prefix; + for (key, _, constraint, facts) in pairs.iter().skip(start_index) { + current = self.extend_state_constraint_analysis(¤t, constraint, facts); + let cached = Rc::new(current.clone()); + self.analysis_cache + .borrow_mut() + .state_cache + .insert(*key, cached); + } + + self.analysis_cache + .borrow() + .state_cache + .get(&cache_key) + .cloned() + .unwrap_or_else(|| Rc::new(current)) + } + + fn cached_state_constraint_analysis( + &self, + state: &SymState<'ctx>, + ) -> Option> { + self.analysis_cache + .borrow() + .state_cache + .get(&state.constraint_cursor_key()) + .cloned() + } + + fn should_build_partition_analysis(&self, state: &SymState<'ctx>, kind: DeepQueryKind) -> bool { + if matches!(kind, DeepQueryKind::Model) { + return true; + } + + let constraint_count = state.constraints().len(); + if constraint_count >= 6 { + return true; + } + if constraint_count < 4 { + return false; + } + + self.distinct_dependency_atom_count(state) >= 2 + } + + fn analysis_for_mode( + &self, + state: &SymState<'ctx>, + mode: SolverMode, + kind: DeepQueryKind, + ) -> Option> { + match mode { + SolverMode::ExploreFast => self.cached_state_constraint_analysis(state), + SolverMode::QueryDeep if self.should_build_partition_analysis(state, kind) => { + Some(self.state_constraint_analysis(state)) + } + SolverMode::QueryDeep => None, + } + } + + fn selection_for_query_with_analysis( + &self, + state: &SymState<'ctx>, + analysis: &StateConstraintAnalysis, + seed_deps: &HashSet, + ) -> ConstraintSelection { + let mut selected = analysis.ground_constraints.clone(); + let mut abstract_facts = analysis.ground_abstract_facts.clone(); + let mut partition_ids = Vec::new(); + if seed_deps.is_empty() { + let is_full_state = selected.len() == state.constraints().len(); + return ConstraintSelection { + key: ConstraintSelectionKey { + state_key: state.constraint_cursor_key(), + full_state: false, + partition_ids, + }, + constraints: selected, + abstract_facts, + is_full_state, + }; + } + + for partition in &analysis.partitions { + if !partition.atoms.is_disjoint(seed_deps) { + selected.extend(partition.constraints.iter().cloned()); + abstract_facts.merge(&partition.abstract_facts); + partition_ids.push(partition.id); + } + } + + ConstraintSelection { + key: ConstraintSelectionKey { + state_key: state.constraint_cursor_key(), + full_state: false, + partition_ids, + }, + constraints: selected, + abstract_facts, + is_full_state: false, + } + } + + fn full_selection_from_analysis( + &self, + state: &SymState<'ctx>, + analysis: &StateConstraintAnalysis, + ) -> ConstraintSelection { + let mut abstract_facts = analysis.ground_abstract_facts.clone(); + for partition in &analysis.partitions { + abstract_facts.merge(&partition.abstract_facts); + } + ConstraintSelection { + key: ConstraintSelectionKey { + state_key: state.constraint_cursor_key(), + full_state: true, + partition_ids: Vec::new(), + }, + constraints: state.constraints().to_vec(), + abstract_facts, + is_full_state: true, + } + } + + fn full_selection_without_analysis(&self, state: &SymState<'ctx>) -> ConstraintSelection { + ConstraintSelection { + key: ConstraintSelectionKey { + state_key: state.constraint_cursor_key(), + full_state: true, + partition_ids: Vec::new(), + }, + constraints: state.constraints().to_vec(), + abstract_facts: Self::abstract_facts_for_constraints(state.constraints()), + is_full_state: true, + } + } + + fn selection_is_effectively_full_state( + &self, + analysis: &StateConstraintAnalysis, + selection: &ConstraintSelection, + total_constraints: usize, + ) -> bool { + if selection.is_full_state { + return true; + } + if analysis.partitions.len() <= 1 { + return true; + } + if selection.key.partition_ids.len() == analysis.partitions.len() { + return true; + } + selection.constraints.len().saturating_mul(4) >= total_constraints.saturating_mul(3) + || (total_constraints > 6 + && selection.constraints.len().saturating_add(2) >= total_constraints) + } + + fn preferred_selection_for_query( + &self, + state: &SymState<'ctx>, + seed_deps: &HashSet, + kind: DeepQueryKind, + ) -> ConstraintSelection { + let Some(analysis) = self.analysis_for_mode(state, SolverMode::QueryDeep, kind) else { + return self.full_selection_without_analysis(state); + }; + + let selection = self.selection_for_query_with_analysis(state, &analysis, seed_deps); + if self.selection_is_effectively_full_state( + &analysis, + &selection, + state.constraints().len(), + ) { + self.full_selection_from_analysis(state, &analysis) + } else { + selection + } + } + + fn ensure_selection_solver(&self, selection: &ConstraintSelection) { + if self.selection_session.borrow().asserted_key.as_ref() == Some(&selection.key) { + return; + } + self.reset_selection_solver(); + for constraint in &selection.constraints { + self.selection_solver.assert(constraint); + } + self.selection_session.borrow_mut().asserted_key = Some(selection.key.clone()); + } + + fn selection_sat_with_constraint( + &self, + selection: &ConstraintSelection, + constraint: &Bool, + ) -> SatResult { + self.ensure_selection_solver(selection); + self.selection_solver.push(); + self.selection_solver.assert(constraint); + let result = self.selection_solver.check().into(); + self.selection_solver.pop(1); + result + } + + fn selection_find_value( + &self, + selection: &ConstraintSelection, + target: &SymValue<'ctx>, + constraint: &Bool, + ) -> Option { + self.ensure_selection_solver(selection); + self.selection_solver.push(); + self.selection_solver.assert(constraint); + let sat = self.selection_solver.check(); + let out = if sat == z3::SatResult::Sat { + let model = self.selection_solver.get_model()?; + self.eval_value_in_model(&model, target) + } else { + None + }; + self.selection_solver.pop(1); + out + } + + fn selection_contradicts_constraint( + &self, + selection: &ConstraintSelection, + constraint: &Bool, + ) -> bool { + if selection.abstract_facts.unsat { + return true; + } + let mut facts = selection.abstract_facts.clone(); + Self::apply_abstract_constraint(&mut facts, &Dynamic::from_ast(constraint), true); + facts.unsat + } + + fn partitioned_state_result( + &self, + state: &SymState<'ctx>, + mode: SolverMode, + kind: DeepQueryKind, + ) -> Option { + let cache_key = state.constraint_cursor_key(); + let analysis = self.analysis_for_mode(state, mode, kind)?; + if analysis.partitions.len() <= 1 { + return None; + } + + if analysis.ground_abstract_facts.unsat { + return Some(SatResult::Unsat); + } + + if !analysis.ground_constraints.is_empty() { + if let Some(cached) = self + .analysis_cache + .borrow() + .ground_sat_cache + .get(&cache_key) + .copied() + { + match cached { + SatResult::Sat => {} + other => return Some(other), + } + } else { + self.scratch_assert_constraints(&analysis.ground_constraints); + let result: SatResult = self.scratch_solver.check().into(); + if matches!(result, SatResult::Sat | SatResult::Unsat) { + self.analysis_cache + .borrow_mut() + .ground_sat_cache + .insert(cache_key, result); + } + match result { + SatResult::Sat => {} + other => return Some(other), + } + } + } + + self.scratch_assert_constraints(&analysis.ground_constraints); + for partition in &analysis.partitions { + if partition.abstract_facts.unsat { + return Some(SatResult::Unsat); + } + let cache_key = PartitionSatCacheKey { + state_key: state.constraint_cursor_key(), + partition_id: partition.id, + }; + if let Some(cached) = self + .analysis_cache + .borrow() + .partition_sat_cache + .get(&cache_key) + .copied() + { + match cached { + SatResult::Sat => continue, + other => return Some(other), + } + } + + self.scratch_solver.push(); + for constraint in &partition.constraints { + self.scratch_solver.assert(constraint); + } + let result: SatResult = self.scratch_solver.check().into(); + self.scratch_solver.pop(1); + if matches!(result, SatResult::Sat | SatResult::Unsat) { + self.analysis_cache + .borrow_mut() + .partition_sat_cache + .insert(cache_key, result); + } + match result { + SatResult::Sat => {} + other => return Some(other), + } + } + Some(SatResult::Sat) + } + + fn scratch_assert_constraints(&self, constraints: &[Bool]) { + self.reset_scratch_solver(); + for constraint in constraints { + self.scratch_solver.assert(constraint); + } + } + + fn cached_state_sat_result( + &self, + cache_key: ConstraintCursorKey, + mode: SolverMode, + ) -> Option { + match self.sat_cache.borrow().get(&cache_key).copied() { + Some(SatResult::Unknown) if matches!(mode, SolverMode::QueryDeep) => None, + other => other, + } + } + + fn remember_state_sat_result(&self, cache_key: ConstraintCursorKey, result: SatResult) { + if matches!(result, SatResult::Sat | SatResult::Unsat) { + self.sat_cache.borrow_mut().insert(cache_key, result); + } + } + + fn compute_state_sat_result( + &self, + state: &SymState<'ctx>, + mode: SolverMode, + kind: DeepQueryKind, + ) -> SatResult { + let partitioned = match mode { + SolverMode::ExploreFast => { + self.partitioned_state_result(state, mode, DeepQueryKind::Predicate) + } + SolverMode::QueryDeep if self.should_build_partition_analysis(state, kind) => { + self.partitioned_state_result(state, mode, kind) + } + SolverMode::QueryDeep => None, + }; + match partitioned { + Some(SatResult::Unknown) => { + self.ensure_state_solver_cursor(state); + self.state_solver.check().into() + } + Some(other) => other, + None => { + self.ensure_state_solver_cursor(state); + self.state_solver.check().into() + } + } + } + + fn state_sat_result( + &self, + state: &SymState<'ctx>, + mode: SolverMode, + kind: DeepQueryKind, + ) -> SatResult { + let cache_key = self.state_sat_cache_key(state); + if let Some(result) = self.cached_state_sat_result(cache_key, mode) { + return result; + } + + let result = self.compute_state_sat_result(state, mode, kind); + self.remember_state_sat_result(cache_key, result); + result + } + /// Check if a state's path constraints are satisfiable. pub fn is_sat(&self, state: &SymState<'ctx>) -> bool { - self.push(); - self.assert_all(state.constraints()); - let result = self.check(); - self.pop(1); - result == SatResult::Sat + self.stats.borrow_mut().sat_queries += 1; + + let cache_key = self.state_sat_cache_key(state); + if let Some(result) = self.cached_state_sat_result(cache_key, SolverMode::ExploreFast) { + self.stats.borrow_mut().sat_cache_hits += 1; + return result == SatResult::Sat; + } + + self.stats.borrow_mut().sat_cache_misses += 1; + let result = + self.compute_state_sat_result(state, SolverMode::ExploreFast, DeepQueryKind::Predicate); + self.remember_state_sat_result(cache_key, result); + // Exploration pruning must be conservative. `Unknown` usually means the + // fast partitioned solver hit a complex bit-vector predicate; treating + // that as UNSAT drops real paths before the deeper query/model phase can + // classify them honestly. + result != SatResult::Unsat + } + + /// Check whether a state's constraints remain satisfiable with one extra constraint. + pub fn sat_with_constraint(&self, state: &SymState<'ctx>, constraint: &Bool) -> SatResult { + if self.state_sat_result(state, SolverMode::QueryDeep, DeepQueryKind::Predicate) + != SatResult::Sat + { + return SatResult::Unsat; + } + let seed_deps = self.expr_dependencies_bool(constraint); + let selected = + self.preferred_selection_for_query(state, &seed_deps, DeepQueryKind::Predicate); + if self.selection_contradicts_constraint(&selected, constraint) { + return SatResult::Unsat; + } + match self.selection_sat_with_constraint(&selected, constraint) { + SatResult::Unknown if !selected.is_full_state => { + let full = self.full_selection_without_analysis(state); + if self.selection_contradicts_constraint(&full, constraint) { + SatResult::Unsat + } else { + self.selection_sat_with_constraint(&full, constraint) + } + } + other => other, + } } /// Get a concrete model for a state's constraints. pub fn solve(&self, state: &SymState<'ctx>) -> Option> { - self.push(); - self.assert_all(state.constraints()); + self.stats.borrow_mut().solve_calls += 1; + + let cache_key = self.state_sat_cache_key(state); + if matches!( + self.cached_state_sat_result(cache_key, SolverMode::QueryDeep), + Some(SatResult::Unsat) + ) { + self.stats.borrow_mut().solve_unsat_shortcuts += 1; + return None; + } - let result = if self.check() == SatResult::Sat { - self.get_model().map(|m| SymModel::new(self.ctx, m)) + if matches!( + self.partitioned_state_result(state, SolverMode::QueryDeep, DeepQueryKind::Model), + Some(SatResult::Unsat) + ) { + self.remember_state_sat_result(cache_key, SatResult::Unsat); + self.stats.borrow_mut().solve_unsat_shortcuts += 1; + return None; + } + + self.ensure_state_solver_cursor(state); + let sat_result: SatResult = self.state_solver.check().into(); + let result = if sat_result == SatResult::Sat { + self.state_solver + .get_model() + .map(|m| SymModel::new(self.ctx, m)) } else { None }; - self.pop(1); - result + self.remember_state_sat_result(cache_key, sat_result); + result + } + + /// Check whether one state's path condition implies another's. + pub fn implies( + &self, + antecedent: &SymState<'ctx>, + consequent: &SymState<'ctx>, + ) -> Option { + if antecedent.constraints_imply_by_prefix(consequent) { + return Some(true); + } + if self.state_sat_result(antecedent, SolverMode::QueryDeep, DeepQueryKind::Predicate) + != SatResult::Sat + { + return Some(true); + } + let consequent_not = consequent.path_condition().not(); + let seed_deps = self.expr_dependencies_bool(&consequent_not); + let selected = + self.preferred_selection_for_query(antecedent, &seed_deps, DeepQueryKind::Predicate); + if self.selection_contradicts_constraint(&selected, &consequent_not) { + return Some(true); + } + match match self.selection_sat_with_constraint(&selected, &consequent_not) { + SatResult::Unknown if !selected.is_full_state => { + let full = self.full_selection_without_analysis(antecedent); + if self.selection_contradicts_constraint(&full, &consequent_not) { + SatResult::Unsat + } else { + self.selection_sat_with_constraint(&full, &consequent_not) + } + } + other => other, + } { + SatResult::Unsat => Some(true), + SatResult::Sat => Some(false), + SatResult::Unknown => None, + } } /// Evaluate a symbolic value under the current model. @@ -157,18 +1393,26 @@ impl<'ctx> SymSolver<'ctx> { target: &SymValue<'ctx>, constraint: &Bool, ) -> Option { - self.push(); - self.assert_all(state.constraints()); - self.assert(constraint); - - let result = if self.check() == SatResult::Sat { - self.eval(target) - } else { - None - }; - - self.pop(1); - result + if self.state_sat_result(state, SolverMode::QueryDeep, DeepQueryKind::Model) + != SatResult::Sat + { + return None; + } + let mut seed_deps = self.expr_dependencies_bool(constraint); + seed_deps.extend(self.expr_dependencies_bv(&target.to_bv(self.ctx))); + let selected = self.preferred_selection_for_query(state, &seed_deps, DeepQueryKind::Model); + if self.selection_contradicts_constraint(&selected, constraint) { + return None; + } + let selection_result = self.selection_find_value(&selected, target, constraint); + if selection_result.is_some() || selected.is_full_state { + return selection_result; + } + let full = self.full_selection_without_analysis(state); + if self.selection_contradicts_constraint(&full, constraint) { + return None; + } + self.selection_find_value(&full, target, constraint) } /// Check if two symbolic values can be equal. @@ -192,63 +1436,110 @@ impl<'ctx> SymSolver<'ctx> { b.to_bv(self.ctx), ) }; + let mut deps = self.expr_dependencies_bv(&a_bv); + deps.extend(self.expr_dependencies_bv(&b_bv)); + let selection = self.preferred_selection_for_query(state, &deps, DeepQueryKind::Predicate); + if let (Some(left), Some(right)) = ( + self.abstract_summary_for_value(&selection.abstract_facts, a), + self.abstract_summary_for_value(&selection.abstract_facts, b), + ) { + if let (Some(left_exact), Some(right_exact)) = (left.exact, right.exact) { + return left_exact == right_exact; + } + if let (Some(left_interval), Some(right_interval)) = (left.interval, right.interval) + && (left_interval.max < right_interval.min + || right_interval.max < left_interval.min) + { + return false; + } + } let eq = a_bv.eq(&b_bv); - - self.push(); - self.assert_all(state.constraints()); - self.assert(&eq); - let result = self.check() == SatResult::Sat; - self.pop(1); - - result + matches!(self.sat_with_constraint(state, &eq), SatResult::Sat) } /// Check if a value can be zero. pub fn can_be_zero(&self, state: &SymState<'ctx>, value: &SymValue<'ctx>) -> bool { + let seed_deps = self.expr_dependencies_bv(&value.to_bv(self.ctx)); + let selection = + self.preferred_selection_for_query(state, &seed_deps, DeepQueryKind::Predicate); + if let Some(summary) = self.abstract_summary_for_value(&selection.abstract_facts, value) { + if summary.exact == Some(0) { + return true; + } + if summary.nonzero { + return false; + } + if let Some(interval) = summary.interval + && interval.min > 0 + { + return false; + } + } let bv = value.to_bv(self.ctx); let zero = BV::from_i64(0, value.bits()); let eq = bv.eq(&zero); - - self.push(); - self.assert_all(state.constraints()); - self.assert(&eq); - let result = self.check() == SatResult::Sat; - self.pop(1); - - result + matches!(self.sat_with_constraint(state, &eq), SatResult::Sat) } /// Check if a value must be zero (cannot be non-zero). pub fn must_be_zero(&self, state: &SymState<'ctx>, value: &SymValue<'ctx>) -> bool { + let seed_deps = self.expr_dependencies_bv(&value.to_bv(self.ctx)); + let selection = + self.preferred_selection_for_query(state, &seed_deps, DeepQueryKind::Predicate); + if let Some(summary) = self.abstract_summary_for_value(&selection.abstract_facts, value) { + if summary.exact == Some(0) { + return true; + } + if summary.nonzero { + return false; + } + if let Some(interval) = summary.interval + && interval.min > 0 + { + return false; + } + } let bv = value.to_bv(self.ctx); let zero = BV::from_i64(0, value.bits()); let neq = bv.eq(&zero).not(); - - self.push(); - self.assert_all(state.constraints()); - self.assert(&neq); - let result = self.check() == SatResult::Unsat; - self.pop(1); - - result + matches!(self.sat_with_constraint(state, &neq), SatResult::Unsat) } /// Get the minimum value for a symbolic expression. pub fn minimize(&self, state: &SymState<'ctx>, value: &SymValue<'ctx>) -> Option { - // Binary search for minimum - self.push(); - self.assert_all(state.constraints()); - + if self.state_sat_result(state, SolverMode::QueryDeep, DeepQueryKind::Model) + != SatResult::Sat + { + return None; + } let bv = value.to_bv(self.ctx); + let seed_deps = self.expr_dependencies_bv(&bv); + let mut selection = + self.preferred_selection_for_query(state, &seed_deps, DeepQueryKind::Model); + if selection.abstract_facts.unsat { + return None; + } let bits = value.bits(); - // Start with full range - let mut lo: u64 = 0; - let mut hi: u64 = if bits >= 64 { + if let Some(summary) = self.abstract_summary_for_value(&selection.abstract_facts, value) + && let Some(exact) = summary.exact + { + return Some(exact); + } + + let max_value = if bits >= 64 { u64::MAX } else { (1u64 << bits) - 1 }; + let mut lo: u64 = self + .abstract_summary_for_value(&selection.abstract_facts, value) + .and_then(|summary| summary.interval.map(|interval| interval.min)) + .unwrap_or(0); + let mut hi: u64 = self + .abstract_summary_for_value(&selection.abstract_facts, value) + .and_then(|summary| summary.interval.map(|interval| interval.max)) + .unwrap_or(max_value); let mut result = None; while lo <= hi { @@ -256,41 +1547,62 @@ impl<'ctx> SymSolver<'ctx> { let mid_bv = BV::from_u64(mid, bits); let constraint = bv.bvule(&mid_bv); - self.push(); - self.assert(&constraint); + let mut sat = self.selection_sat_with_constraint(&selection, &constraint); + if sat == SatResult::Unknown && !selection.is_full_state { + selection = self.full_selection_without_analysis(state); + sat = self.selection_sat_with_constraint(&selection, &constraint); + } - if self.check() == SatResult::Sat { - result = self.eval(value); + if sat == SatResult::Sat { + result = self.selection_find_value(&selection, value, &constraint); hi = mid.saturating_sub(1); } else { lo = mid.saturating_add(1); } - self.pop(1); - if lo == 0 && hi == u64::MAX { break; // Prevent infinite loop } } - self.pop(1); result } /// Get the maximum value for a symbolic expression. pub fn maximize(&self, state: &SymState<'ctx>, value: &SymValue<'ctx>) -> Option { - self.push(); - self.assert_all(state.constraints()); - + if self.state_sat_result(state, SolverMode::QueryDeep, DeepQueryKind::Model) + != SatResult::Sat + { + return None; + } let bv = value.to_bv(self.ctx); + let seed_deps = self.expr_dependencies_bv(&bv); + let mut selection = + self.preferred_selection_for_query(state, &seed_deps, DeepQueryKind::Model); + if selection.abstract_facts.unsat { + return None; + } let bits = value.bits(); - let mut lo: u64 = 0; - let mut hi: u64 = if bits >= 64 { + if let Some(summary) = self.abstract_summary_for_value(&selection.abstract_facts, value) + && let Some(exact) = summary.exact + { + return Some(exact); + } + + let max_value = if bits >= 64 { u64::MAX } else { (1u64 << bits) - 1 }; + let mut lo: u64 = self + .abstract_summary_for_value(&selection.abstract_facts, value) + .and_then(|summary| summary.interval.map(|interval| interval.min)) + .unwrap_or(0); + let mut hi: u64 = self + .abstract_summary_for_value(&selection.abstract_facts, value) + .and_then(|summary| summary.interval.map(|interval| interval.max)) + .unwrap_or(max_value); let mut result = None; while lo <= hi { @@ -298,24 +1610,24 @@ impl<'ctx> SymSolver<'ctx> { let mid_bv = BV::from_u64(mid, bits); let constraint = bv.bvuge(&mid_bv); - self.push(); - self.assert(&constraint); + let mut sat = self.selection_sat_with_constraint(&selection, &constraint); + if sat == SatResult::Unknown && !selection.is_full_state { + selection = self.full_selection_without_analysis(state); + sat = self.selection_sat_with_constraint(&selection, &constraint); + } - if self.check() == SatResult::Sat { - result = self.eval(value); + if sat == SatResult::Sat { + result = self.selection_find_value(&selection, value, &constraint); lo = mid.saturating_add(1); } else { hi = mid.saturating_sub(1); } - self.pop(1); - if lo == 0 && hi == u64::MAX { break; } } - self.pop(1); result } } @@ -451,6 +1763,27 @@ mod tests { assert!(solver.is_sat(&state)); } + #[test] + fn test_state_sat_cache_hit() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + let five = SymValue::concrete(5, 32); + let cond = x.ult(&ctx, &five); + state.add_true_constraint(&cond); + + let solver = SymSolver::new(&ctx); + assert!(solver.is_sat(&state)); + assert!(solver.is_sat(&state)); + + let stats = solver.stats(); + assert_eq!(stats.sat_queries, 2); + assert_eq!(stats.sat_cache_hits, 1); + assert_eq!(stats.sat_cache_misses, 1); + } + #[test] fn test_solve() { let ctx = Context::thread_local(); @@ -470,4 +1803,427 @@ mod tests { let x_value = model.eval(&x); assert_eq!(x_value, Some(10)); } + + #[test] + fn test_solve_uses_cached_unsat_shortcut() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + + let x = state.get_register("x"); + let five = SymValue::concrete(5, 32); + let three = SymValue::concrete(3, 32); + state.add_true_constraint(&x.ult(&ctx, &three)); + state.add_true_constraint(&x.eq(&ctx, &five)); + + let solver = SymSolver::new(&ctx); + assert!(!solver.is_sat(&state)); + assert!(solver.solve(&state).is_none()); + + let stats = solver.stats(); + assert_eq!(stats.solve_calls, 1); + assert_eq!(stats.solve_unsat_shortcuts, 1); + } + + #[test] + fn test_sat_with_constraint_short_circuits_unsat_base_state() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + + let x = state.get_register("x"); + let y = state.get_register("y"); + state.add_true_constraint(&x.ult(&ctx, &SymValue::concrete(3, 32))); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(5, 32))); + + let solver = SymSolver::new(&ctx); + let y_is_zero = y.to_bv(&ctx).eq(BV::from_u64(0, 32)); + assert_eq!( + solver.sat_with_constraint(&state, &y_is_zero), + SatResult::Unsat + ); + assert_eq!(solver.find_value(&state, &y, &y_is_zero), None); + } + + #[test] + fn test_find_value_and_extrema_with_irrelevant_constraints() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + + let x = state.get_register("x"); + let y = state.get_register("y"); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(10, 32))); + state.constrain_range(&y, 3, 9); + + let solver = SymSolver::new(&ctx); + let y_is_seven = y.to_bv(&ctx).eq(BV::from_u64(7, 32)); + assert_eq!(solver.find_value(&state, &y, &y_is_seven), Some(7)); + assert_eq!(solver.minimize(&state, &y), Some(3)); + assert_eq!(solver.maximize(&state, &y), Some(9)); + } + + #[test] + fn test_state_solver_reuses_prefix_cursor() { + let ctx = Context::thread_local(); + + let mut base = SymState::new(&ctx, 0x1000); + base.make_symbolic("x", 32); + + let x = base.get_register("x"); + base.add_true_constraint(&x.ult(&ctx, &SymValue::concrete(10, 32))); + let shared_key = base.constraint_cursor_key(); + + let mut left = base.fork(); + left.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(1, 32))); + let left_key = left.constraint_cursor_key(); + + let mut right = base.fork(); + right.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(2, 32))); + let right_key = right.constraint_cursor_key(); + + let solver = SymSolver::new(&ctx); + assert!(solver.is_sat(&left)); + assert_eq!( + solver.state_session.borrow().asserted_stack, + vec![ConstraintCursorKey::ROOT, shared_key, left_key] + ); + + assert!(solver.is_sat(&right)); + assert_eq!( + solver.state_session.borrow().asserted_stack, + vec![ConstraintCursorKey::ROOT, shared_key, right_key] + ); + + assert!(solver.is_sat(&base)); + assert_eq!( + solver.state_session.borrow().asserted_stack, + vec![ConstraintCursorKey::ROOT, shared_key] + ); + } + + #[test] + fn test_state_constraint_analysis_partitions_independent_constraints() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + + let x = state.get_register("x"); + let y = state.get_register("y"); + state.add_true_constraint(&x.ult(&ctx, &SymValue::concrete(5, 32))); + state.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(42, 32))); + + let solver = SymSolver::new(&ctx); + let analysis = solver.state_constraint_analysis(&state); + assert_eq!(analysis.ground_constraints.len(), 0); + assert_eq!(analysis.partitions.len(), 2); + } + + #[test] + fn test_query_selection_drops_unrelated_symbolic_constraints() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("w", 32); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + state.make_symbolic("z", 32); + + let w = state.get_register("w"); + let x = state.get_register("x"); + let y = state.get_register("y"); + let z = state.get_register("z"); + state.add_true_constraint(&w.eq(&ctx, &SymValue::concrete(1, 32))); + state.add_true_constraint(&x.ult(&ctx, &SymValue::concrete(5, 32))); + state.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(42, 32))); + state.add_true_constraint(&z.eq(&ctx, &SymValue::concrete(9, 32))); + + let solver = SymSolver::new(&ctx); + let seed_deps = solver.expr_dependencies_bv(&x.to_bv(&ctx)); + let selected = + solver.preferred_selection_for_query(&state, &seed_deps, DeepQueryKind::Predicate); + + assert_eq!(selected.constraints.len(), 1); + let x_is_three = x.to_bv(&ctx).eq(BV::from_u64(3, 32)); + assert_eq!( + solver.sat_with_constraint(&state, &x_is_three), + SatResult::Sat + ); + } + + #[test] + fn test_query_selection_keeps_ground_constraints() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.add_true_constraint(&x.ult(&ctx, &SymValue::concrete(5, 32))); + state.add_constraint(Bool::from_bool(false)); + + let solver = SymSolver::new(&ctx); + let seed_deps = solver.expr_dependencies_bv(&x.to_bv(&ctx)); + let selected = + solver.preferred_selection_for_query(&state, &seed_deps, DeepQueryKind::Predicate); + + assert_eq!(selected.constraints.len(), 2); + let x_is_three = x.to_bv(&ctx).eq(BV::from_u64(3, 32)); + assert_eq!( + solver.sat_with_constraint(&state, &x_is_three), + SatResult::Unsat + ); + } + + #[test] + fn test_partitioned_state_result_detects_unsat_component() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + + let x = state.get_register("x"); + let y = state.get_register("y"); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(1, 32))); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(2, 32))); + state.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(9, 32))); + + let solver = SymSolver::new(&ctx); + assert_eq!( + solver.partitioned_state_result(&state, SolverMode::QueryDeep, DeepQueryKind::Model), + Some(SatResult::Unsat) + ); + assert!(!solver.is_sat(&state)); + } + + #[test] + fn test_partition_sat_cache_reuses_checked_components() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + + let x = state.get_register("x"); + let y = state.get_register("y"); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(1, 32))); + state.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(9, 32))); + + let solver = SymSolver::new(&ctx); + assert_eq!( + solver.partitioned_state_result(&state, SolverMode::QueryDeep, DeepQueryKind::Model), + Some(SatResult::Sat) + ); + assert_eq!(solver.analysis_cache.borrow().partition_sat_cache.len(), 2); + assert_eq!( + solver.partitioned_state_result(&state, SolverMode::QueryDeep, DeepQueryKind::Model), + Some(SatResult::Sat) + ); + assert_eq!(solver.analysis_cache.borrow().partition_sat_cache.len(), 2); + } + + #[test] + fn test_explore_fast_sat_does_not_build_fresh_partition_analysis() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("w", 32); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + state.make_symbolic("z", 32); + + let w = state.get_register("w"); + let x = state.get_register("x"); + let y = state.get_register("y"); + let z = state.get_register("z"); + state.add_true_constraint(&w.eq(&ctx, &SymValue::concrete(1, 32))); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(2, 32))); + state.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(3, 32))); + state.add_true_constraint(&z.eq(&ctx, &SymValue::concrete(4, 32))); + + let solver = SymSolver::new(&ctx); + assert!(solver.analysis_cache.borrow().state_cache.is_empty()); + assert!(solver.is_sat(&state)); + assert!(solver.analysis_cache.borrow().state_cache.is_empty()); + } + + #[test] + fn test_query_deep_builds_analysis_after_fast_sat_skips_it() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("w", 32); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + state.make_symbolic("z", 32); + + let w = state.get_register("w"); + let x = state.get_register("x"); + let y = state.get_register("y"); + let z = state.get_register("z"); + state.add_true_constraint(&w.eq(&ctx, &SymValue::concrete(1, 32))); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(2, 32))); + state.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(3, 32))); + state.add_true_constraint(&z.eq(&ctx, &SymValue::concrete(4, 32))); + + let solver = SymSolver::new(&ctx); + assert!(solver.is_sat(&state)); + assert!(solver.analysis_cache.borrow().state_cache.is_empty()); + + let x_is_two = x.to_bv(&ctx).eq(BV::from_u64(2, 32)); + assert_eq!( + solver.sat_with_constraint(&state, &x_is_two), + SatResult::Sat + ); + assert!(!solver.analysis_cache.borrow().state_cache.is_empty()); + } + + #[test] + fn test_selection_solver_reuses_same_selection_key() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + let x = state.get_register("x"); + let y = state.get_register("y"); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(5, 32))); + state.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(9, 32))); + + let solver = SymSolver::new(&ctx); + let selection = solver.preferred_selection_for_query( + &state, + &solver.expr_dependencies_bv(&x.to_bv(&ctx)), + DeepQueryKind::Predicate, + ); + solver.ensure_selection_solver(&selection); + let first_key = solver.selection_session.borrow().asserted_key.clone(); + solver.ensure_selection_solver(&selection); + assert_eq!(solver.selection_session.borrow().asserted_key, first_key); + } + + #[test] + fn test_cursor_fact_cache_reuses_dependency_extraction_for_descendants() { + let ctx = Context::thread_local(); + + let mut base = SymState::new(&ctx, 0x1000); + base.make_symbolic("x", 32); + let x = base.get_register("x"); + base.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(1, 32))); + + let mut child = base.fork(); + child.make_symbolic("y", 32); + let y = child.get_register("y"); + child.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(2, 32))); + + let solver = SymSolver::new(&ctx); + let _ = solver.state_constraint_analysis(&base); + let cursor_facts_after_base = solver.analysis_cache.borrow().cursor_facts.len(); + assert_eq!(cursor_facts_after_base, 1); + + let _ = solver.state_constraint_analysis(&child); + let cursor_facts_after_child = solver.analysis_cache.borrow().cursor_facts.len(); + assert_eq!(cursor_facts_after_child, 2); + assert!( + solver + .analysis_cache + .borrow() + .state_cache + .contains_key(&base.constraint_cursor_key()) + ); + assert!( + solver + .analysis_cache + .borrow() + .state_cache + .contains_key(&child.constraint_cursor_key()) + ); + } + + #[test] + fn test_small_connected_query_prefers_full_state_selection() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(5, 32))); + + let solver = SymSolver::new(&ctx); + let selection = solver.preferred_selection_for_query( + &state, + &solver.expr_dependencies_bv(&x.to_bv(&ctx)), + DeepQueryKind::Predicate, + ); + assert!(selection.is_full_state); + assert_eq!(selection.constraints.len(), state.constraints().len()); + } + + #[test] + fn test_multi_component_model_query_keeps_partitioned_selection() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + for name in ["a", "b", "c", "d", "e", "f"] { + state.make_symbolic(name, 32); + } + + for (index, name) in ["a", "b", "c", "d", "e"].iter().enumerate() { + let value = state.get_register(name); + state.add_true_constraint(&value.eq(&ctx, &SymValue::concrete(index as u64, 32))); + } + + let f = state.get_register("f"); + state.constrain_range(&f, 3, 9); + + let solver = SymSolver::new(&ctx); + let selection = solver.preferred_selection_for_query( + &state, + &solver.expr_dependencies_bv(&f.to_bv(&ctx)), + DeepQueryKind::Model, + ); + assert!(!selection.is_full_state); + assert!(selection.constraints.len() < state.constraints().len()); + assert_eq!(selection.key.partition_ids.len(), 1); + } + + #[test] + fn test_abstract_prefilter_detects_conflicting_eq() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.constrain_eq(&x, 5); + + let solver = SymSolver::new(&ctx); + let impossible = x.to_bv(&ctx).eq(BV::from_u64(7, 32)); + assert_eq!( + solver.sat_with_constraint(&state, &impossible), + SatResult::Unsat + ); + } + + #[test] + fn test_abstract_prefilter_uses_range_for_zero_queries() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.constrain_range(&x, 3, 9); + + let solver = SymSolver::new(&ctx); + assert!(!solver.can_be_zero(&state, &x)); + assert!(!solver.must_be_zero(&state, &x)); + assert_eq!(solver.minimize(&state, &x), Some(3)); + assert_eq!(solver.maximize(&state, &x), Some(9)); + } } diff --git a/crates/r2sym/src/spec.rs b/crates/r2sym/src/spec.rs new file mode 100644 index 0000000..4987f3a --- /dev/null +++ b/crates/r2sym/src/spec.rs @@ -0,0 +1,354 @@ +use serde::{Deserialize, Serialize}; + +use crate::path::{ExploreConfig, ExploreStrategy}; +use crate::state::SymState; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(untagged)] +pub enum AddressValue { + Integer(u64), + String(String), +} + +impl AddressValue { + pub fn parse(&self) -> Result { + match self { + Self::Integer(value) => Ok(*value), + Self::String(text) => parse_address_text(text), + } + } +} + +fn parse_address_text(text: &str) -> Result { + let trimmed = text.trim(); + let has_minus = trimmed.char_indices().skip(1).any(|(_, ch)| ch == '-'); + if trimmed.contains('+') || has_minus { + return parse_address_expression(trimmed); + } + parse_address_atom(trimmed) +} + +fn parse_address_expression(text: &str) -> Result { + let mut expr = text.trim(); + let mut total = if let Some(rest) = expr.strip_prefix('-') { + expr = rest.trim_start(); + 0i128 + } else { + let (head, tail) = split_next_term(expr); + expr = tail; + parse_address_atom(head)? as i128 + }; + + while !expr.is_empty() { + let op = expr + .chars() + .next() + .ok_or_else(|| format!("invalid address expression: {}", text))?; + if op != '+' && op != '-' { + return Err(format!("invalid address expression: {}", text)); + } + let remainder = expr[op.len_utf8()..].trim_start(); + let (term, tail) = split_next_term(remainder); + let value = parse_address_atom(term)? as i128; + total = if op == '+' { + total + value + } else { + total - value + }; + expr = tail; + } + + u64::try_from(total).map_err(|_| format!("address expression underflow/overflow: {}", text)) +} + +fn split_next_term(text: &str) -> (&str, &str) { + let mut depth = 0usize; + for (idx, ch) in text.char_indices() { + match ch { + '(' => depth += 1, + ')' => depth = depth.saturating_sub(1), + '+' | '-' if depth == 0 && idx > 0 => { + return (text[..idx].trim_end(), text[idx..].trim_start()); + } + _ => {} + } + } + (text.trim(), "") +} + +fn parse_address_atom(text: &str) -> Result { + let trimmed = text.trim().trim_matches(|ch| ch == '(' || ch == ')'); + if let Some(hex) = trimmed + .strip_prefix("0x") + .or_else(|| trimmed.strip_prefix("0X")) + { + return u64::from_str_radix(hex, 16) + .map_err(|_| format!("invalid hex address: {}", trimmed)); + } + trimmed + .parse::() + .or_else(|_| u64::from_str_radix(trimmed, 16)) + .map_err(|_| format!("invalid address: {}", trimmed)) +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum StartSpec { + #[default] + Entry, + Address { + addr: AddressValue, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum PredicateSpec { + Address { addr: AddressValue }, + AddressSet { addrs: Vec }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum InputSpec { + Fd { + fd: i32, + len: usize, + #[serde(default)] + name: Option, + #[serde(default)] + alphabet: Option, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +pub struct BudgetSpec { + #[serde(default)] + pub max_states: Option, + #[serde(default)] + pub max_depth: Option, + #[serde(default)] + pub max_finds: Option, + #[serde(default)] + pub timeout_ms: Option, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum StrategySpec { + #[default] + Dfs, + Bfs, + Random, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum MergeSpec { + #[default] + Off, + SamePc, +} + +fn default_skip_sleep_calls() -> bool { + true +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct RuntimeSpec { + #[serde(default)] + pub tty_fds: Vec, + #[serde(default = "default_skip_sleep_calls")] + pub skip_sleep_calls: bool, +} + +impl Default for RuntimeSpec { + fn default() -> Self { + Self { + tty_fds: Vec::new(), + skip_sleep_calls: default_skip_sleep_calls(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +pub struct ExplorationSpec { + #[serde(default)] + pub start: StartSpec, + #[serde(default)] + pub find: Vec, + #[serde(default)] + pub avoid: Vec, + #[serde(default)] + pub inputs: Vec, + #[serde(default)] + pub budget: BudgetSpec, + #[serde(default)] + pub strategy: StrategySpec, + #[serde(default)] + pub merge: MergeSpec, + #[serde(default)] + pub runtime: RuntimeSpec, +} + +impl ExplorationSpec { + pub fn validate(&self) -> Result<(), String> { + if self.find.is_empty() { + return Err("exploration spec requires at least one find predicate".to_string()); + } + for input in &self.inputs { + match input { + InputSpec::Fd { + len, + alphabet, + name, + .. + } => { + if *len == 0 { + return Err("fd input length must be greater than zero".to_string()); + } + if let Some(alphabet) = alphabet + && alphabet.is_empty() + { + return Err("fd input alphabet must not be empty".to_string()); + } + if let Some(name) = name + && name.trim().is_empty() + { + return Err("fd input name must not be empty".to_string()); + } + } + } + } + let _ = self.find_addresses()?; + let _ = self.avoid_addresses()?; + Ok(()) + } + + pub fn start_pc(&self, entry_pc: u64) -> Result { + match &self.start { + StartSpec::Entry => Ok(entry_pc), + StartSpec::Address { addr } => addr.parse(), + } + } + + pub fn max_finds(&self) -> usize { + self.budget.max_finds.unwrap_or(1).max(1) + } + + pub fn to_explore_config(&self, defaults: &ExploreConfig) -> ExploreConfig { + let mut config = defaults.clone(); + if let Some(max_states) = self.budget.max_states { + config.max_states = max_states; + } + if let Some(max_depth) = self.budget.max_depth { + config.max_depth = max_depth; + } + if let Some(timeout_ms) = self.budget.timeout_ms { + config.timeout = Some(std::time::Duration::from_millis(timeout_ms)); + } + config.strategy = match self.strategy { + StrategySpec::Dfs => ExploreStrategy::Dfs, + StrategySpec::Bfs => ExploreStrategy::Bfs, + StrategySpec::Random => ExploreStrategy::Random, + }; + config.merge_states = matches!(self.merge, MergeSpec::SamePc); + config + } + + pub fn find_addresses(&self) -> Result, String> { + flatten_predicates(&self.find) + } + + pub fn avoid_addresses(&self) -> Result, String> { + flatten_predicates(&self.avoid) + } + + pub fn apply_to_state<'ctx>(&self, state: &mut SymState<'ctx>) { + for fd in &self.runtime.tty_fds { + state.set_tty_fd(*fd, true); + } + state.set_skip_sleep_calls(self.runtime.skip_sleep_calls); + for input in &self.inputs { + match input { + InputSpec::Fd { + fd, + len, + name, + alphabet, + } => { + let default_name = format!("fd{}_input", fd); + state.add_symbolic_fd_input( + *fd, + *len, + name.as_deref().unwrap_or(&default_name), + alphabet.as_deref(), + ); + } + } + } + } +} + +fn flatten_predicates(predicates: &[PredicateSpec]) -> Result, String> { + let mut out = Vec::new(); + for predicate in predicates { + match predicate { + PredicateSpec::Address { addr } => out.push(addr.parse()?), + PredicateSpec::AddressSet { addrs } => { + for addr in addrs { + out.push(addr.parse()?); + } + } + } + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_hex_addresses() { + assert_eq!( + AddressValue::String("0x401000".to_string()) + .parse() + .unwrap(), + 0x401000 + ); + } + + #[test] + fn parses_simple_address_expressions() { + assert_eq!( + AddressValue::String("0x401000 + 0x20 - 4".to_string()) + .parse() + .unwrap(), + 0x40101c + ); + } + + #[test] + fn validates_fd_inputs() { + let spec = ExplorationSpec { + find: vec![PredicateSpec::Address { + addr: AddressValue::Integer(0x401000), + }], + inputs: vec![InputSpec::Fd { + fd: 0, + len: 4, + name: Some("stdin".to_string()), + alphabet: Some("abcd".to_string()), + }], + ..Default::default() + }; + assert!(spec.validate().is_ok()); + } + + #[test] + fn rejects_empty_find_list() { + let spec = ExplorationSpec::default(); + assert!(spec.validate().is_err()); + } +} diff --git a/crates/r2sym/src/state.rs b/crates/r2sym/src/state.rs index 69123a8..a654731 100644 --- a/crates/r2sym/src/state.rs +++ b/crates/r2sym/src/state.rs @@ -3,17 +3,139 @@ //! This module provides the `SymState` type which represents the state //! of the program during symbolic execution. -use std::collections::{HashMap, HashSet}; +use std::cell::OnceCell; +use std::collections::hash_map::DefaultHasher; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::hash::{Hash, Hasher}; +use std::rc::Rc; +use std::sync::atomic::{AtomicUsize, Ordering}; use z3::Context; use z3::ast::{BV, Bool}; -use crate::memory::SymMemory; +use crate::memory::{MemoryRegionId, MemoryRegionKind, SymMemory}; use crate::value::SymValue; +fn debug_runtime_continuation_log(message: &str) { + if std::env::var_os("R2SLEIGH_DEBUG_RUNTIME_CONTINUATION").is_none() { + return; + } + let path = std::env::var("R2SLEIGH_DEBUG_RUNTIME_CONTINUATION_LOG") + .unwrap_or_else(|_| "/tmp/r2sleigh_runtime_continuation.log".to_string()); + if let Ok(mut file) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(path) + { + use std::io::Write; + let _ = writeln!(file, "{message}"); + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) struct ConstraintCursorKey(usize); + +impl ConstraintCursorKey { + pub(crate) const ROOT: Self = Self(0); +} + +#[derive(Clone, Default)] +struct ConstraintCursor { + node: Option>, +} + +struct ConstraintNode { + id: ConstraintCursorKey, + parent: Option>, + constraint: Bool, + depth: usize, + hash: u64, +} + +impl ConstraintCursor { + fn key(&self) -> ConstraintCursorKey { + self.node + .as_ref() + .map_or(ConstraintCursorKey::ROOT, |node| node.id) + } + + fn depth(&self) -> usize { + self.node.as_ref().map_or(0, |node| node.depth) + } + + fn hash(&self) -> u64 { + self.node.as_ref().map_or(0, |node| node.hash) + } + + fn push(&self, constraint: Bool) -> Self { + let constraint_hash = structural_hash(&constraint); + let hash = mix_hash(self.hash(), constraint_hash); + let depth = self.depth().saturating_add(1); + Self { + node: Some(Rc::new(ConstraintNode { + id: next_constraint_cursor_key(), + parent: self.node.clone(), + constraint, + depth, + hash, + })), + } + } + + fn materialize(&self) -> Vec { + let mut values = Vec::with_capacity(self.depth()); + let mut current = self.node.as_ref().cloned(); + while let Some(node) = current { + values.push(node.constraint.clone()); + current = node.parent.clone(); + } + values.reverse(); + values + } + + fn is_descendant_of(&self, ancestor: &Self) -> bool { + if ancestor.node.is_none() { + return true; + } + let ancestor_key = ancestor.key(); + let mut current = self.node.as_ref().cloned(); + while let Some(node) = current { + if node.id == ancestor_key { + return true; + } + current = node.parent.clone(); + } + false + } + + fn suffix_from_key( + &self, + ancestor_key: ConstraintCursorKey, + ) -> Option> { + let mut suffix = Vec::new(); + let mut current = self.node.as_ref().cloned(); + while let Some(node) = current { + if node.id == ancestor_key { + suffix.reverse(); + return Some(suffix); + } + suffix.push((node.id, node.constraint.clone())); + current = node.parent.clone(); + } + if ancestor_key == ConstraintCursorKey::ROOT { + suffix.reverse(); + Some(suffix) + } else { + None + } + } +} + /// A tracked symbolic memory region (usually an input buffer). #[derive(Debug, Clone)] pub struct SymbolicMemoryRegion<'ctx> { + /// Canonical region backing this symbolic buffer. + pub region_id: MemoryRegionId, /// Name of the symbolic buffer. pub name: String, /// Concrete address of the buffer. @@ -24,6 +146,76 @@ pub struct SymbolicMemoryRegion<'ctx> { pub value: SymValue<'ctx>, } +/// A tracked symbolic input stream for a file descriptor. +#[derive(Debug, Clone)] +pub struct SymbolicFdInput<'ctx> { + /// Stable user-facing name of the stream. + pub name: String, + /// File descriptor identifier. + pub fd: i32, + /// Symbolic bytes available to the runtime model. + pub bytes: Vec>, + /// Current read cursor. + pub cursor: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RuntimeBlockReason { + MissingExceptionHandler, + MissingRuntimeMaterializedCode, + MissingContinuationSeed, + RuntimeRegionProvenanceUnknown, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct RuntimeValueProvenance { + pub source_addr: u64, + pub size: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct RuntimeRegionAlias { + pub runtime_base: u64, + pub size: u64, + pub source_base: Option, + pub executable: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PendingExceptionContinuation { + pub handler_addr: u64, + pub exception_code: u64, + pub exception_pointers_addr: u64, + pub exception_record_addr: u64, + pub context_addr: u64, +} + +#[derive(Debug, Clone)] +pub struct RuntimeBreakpointContinuation<'ctx> { + pub handler_addr: u64, + pub exception_code: u64, + pub breakpoint: SymValue<'ctx>, +} + +/// Runtime policy carried with each symbolic state. +#[derive(Debug, Clone, Default)] +pub struct RuntimeState<'ctx> { + /// File descriptors that should report tty=true via isatty(). + pub tty_fds: HashSet, + /// Whether sleep-family calls should become zero-cost no-ops. + pub skip_sleep_calls: bool, + /// Registered exception handlers discovered by runtime hooks. + pub exception_handlers: BTreeSet, + /// Runtime regions that may alias materialized executable code. + pub runtime_regions: BTreeMap, + /// Provenance for recently loaded values used to detect copy loops. + pub value_provenance: BTreeMap, + /// Pending exception continuation state that should resume on handler return. + pub pending_exception: Option, + /// Active runtime breakpoint that re-enters an exception handler when reached. + pub active_breakpoint: Option>, +} + /// The state of a symbolic execution. /// /// Contains registers, memory, path constraints, and program counter. @@ -31,11 +223,16 @@ pub struct SymState<'ctx> { /// The Z3 context. ctx: &'ctx Context, /// Register values (register name -> value). - registers: HashMap>, + registers: Rc>>, /// Memory state. pub memory: SymMemory<'ctx>, /// Path constraints (conditions that must be true for this path). - constraints: Vec, + constraints: ConstraintCursor, + /// Materialized constraint list, populated lazily from the shared cursor chain. + materialized_constraints: OnceCell>, + /// Syntactic value facts derived while adding constraints. + known_zero_values: Rc>, + known_nonzero_values: Rc>, /// Current program counter. pub pc: u64, /// Previous program counter (block predecessor). @@ -47,13 +244,17 @@ pub struct SymState<'ctx> { /// Execution depth (number of steps taken). pub depth: usize, /// Named symbolic inputs (registers or buffers). - symbolic_inputs: HashMap>, + symbolic_inputs: Rc>>, /// Tracked symbolic memory regions. - symbolic_memory: Vec>, + symbolic_memory: Rc>>, + /// Symbolic external input streams keyed by file descriptor. + symbolic_fd_inputs: Rc>>, + /// Runtime policy/state for summaries. + runtime: RuntimeState<'ctx>, } /// Exit status of a symbolic execution path. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ExitStatus { /// Normal return. Return, @@ -61,6 +262,8 @@ pub enum ExitStatus { Exit(u64), /// Hit an error/exception. Error(String), + /// Runtime execution was blocked because a required dynamic fact was missing. + RuntimeBlocked(RuntimeBlockReason), /// Hit an unimplemented operation. Unimplemented, /// Reached maximum depth. @@ -74,16 +277,21 @@ impl<'ctx> SymState<'ctx> { pub fn new(ctx: &'ctx Context, entry_pc: u64) -> Self { Self { ctx, - registers: HashMap::new(), + registers: Rc::new(HashMap::new()), memory: SymMemory::new(ctx), - constraints: Vec::new(), + constraints: ConstraintCursor::default(), + materialized_constraints: OnceCell::new(), + known_zero_values: Rc::new(BTreeSet::new()), + known_nonzero_values: Rc::new(BTreeSet::new()), pc: entry_pc, prev_pc: None, active: true, exit_status: None, depth: 0, - symbolic_inputs: HashMap::new(), - symbolic_memory: Vec::new(), + symbolic_inputs: Rc::new(HashMap::new()), + symbolic_memory: Rc::new(Vec::new()), + symbolic_fd_inputs: Rc::new(HashMap::new()), + runtime: RuntimeState::default(), } } @@ -91,16 +299,21 @@ impl<'ctx> SymState<'ctx> { pub fn new_symbolic(ctx: &'ctx Context, entry_pc: u64) -> Self { Self { ctx, - registers: HashMap::new(), + registers: Rc::new(HashMap::new()), memory: SymMemory::new_symbolic(ctx), - constraints: Vec::new(), + constraints: ConstraintCursor::default(), + materialized_constraints: OnceCell::new(), + known_zero_values: Rc::new(BTreeSet::new()), + known_nonzero_values: Rc::new(BTreeSet::new()), pc: entry_pc, prev_pc: None, active: true, exit_status: None, depth: 0, - symbolic_inputs: HashMap::new(), - symbolic_memory: Vec::new(), + symbolic_inputs: Rc::new(HashMap::new()), + symbolic_memory: Rc::new(Vec::new()), + symbolic_fd_inputs: Rc::new(HashMap::new()), + runtime: RuntimeState::default(), } } @@ -124,7 +337,7 @@ impl<'ctx> SymState<'ctx> { /// Set a register value. pub fn set_register(&mut self, name: &str, value: SymValue<'ctx>) { - self.registers.insert(name.to_string(), value); + Rc::make_mut(&mut self.registers).insert(name.to_string(), value); } /// Make a register symbolic with a given name. @@ -136,13 +349,13 @@ impl<'ctx> SymState<'ctx> { /// Make a register symbolic with an explicit symbol name. pub fn make_symbolic_named(&mut self, reg_name: &str, sym_name: &str, bits: u32) { let value = SymValue::new_symbolic(self.ctx, sym_name, bits); - self.registers.insert(reg_name.to_string(), value.clone()); - self.symbolic_inputs.insert(sym_name.to_string(), value); + Rc::make_mut(&mut self.registers).insert(reg_name.to_string(), value.clone()); + Rc::make_mut(&mut self.symbolic_inputs).insert(sym_name.to_string(), value); } /// Set a register to a concrete value. pub fn set_concrete(&mut self, reg_name: &str, value: u64, bits: u32) { - self.registers + Rc::make_mut(&mut self.registers) .insert(reg_name.to_string(), SymValue::concrete(value, bits)); } @@ -163,29 +376,474 @@ impl<'ctx> SymState<'ctx> { /// Get all registers. pub fn registers(&self) -> &HashMap> { - &self.registers + self.registers.as_ref() } /// Get tracked symbolic inputs. pub fn symbolic_inputs(&self) -> &HashMap> { - &self.symbolic_inputs + self.symbolic_inputs.as_ref() } /// Get tracked symbolic memory regions. pub fn symbolic_memory(&self) -> &[SymbolicMemoryRegion<'ctx>] { - &self.symbolic_memory + self.symbolic_memory.as_slice() + } + + /// Return whether every byte in a concrete address range is known concrete. + pub fn is_concrete_memory_range(&self, addr: u64, size: u32) -> bool { + self.memory.is_concrete_range(addr, size) + } + + /// Read concrete bytes from memory when the full range is concrete. + pub fn read_memory_bytes(&self, addr: u64, size: usize) -> Option> { + self.memory.read_bytes(addr, size) + } + + /// Get tracked symbolic file-descriptor inputs. + pub fn symbolic_fd_inputs(&self) -> &HashMap> { + self.symbolic_fd_inputs.as_ref() + } + + /// Get runtime policy/state. + pub fn runtime(&self) -> &RuntimeState<'ctx> { + &self.runtime + } + + pub fn register_exception_handler(&mut self, addr: u64) { + self.runtime.exception_handlers.insert(addr); + } + + pub fn primary_exception_handler(&self) -> Option { + self.runtime.exception_handlers.iter().next().copied() + } + + pub fn define_runtime_region( + &mut self, + name: &str, + base_addr: u64, + size: u64, + executable: bool, + ) -> MemoryRegionId { + let region_id = + self.define_memory_region(MemoryRegionKind::Heap, name, Some(base_addr), Some(size)); + self.runtime + .runtime_regions + .entry(base_addr) + .or_insert(RuntimeRegionAlias { + runtime_base: base_addr, + size, + source_base: None, + executable, + }); + region_id + } + + pub fn register_runtime_region_alias(&mut self, base_addr: u64, size: u64, executable: bool) { + self.runtime + .runtime_regions + .entry(base_addr) + .and_modify(|region| { + region.size = region.size.max(size); + region.executable |= executable; + }) + .or_insert(RuntimeRegionAlias { + runtime_base: base_addr, + size, + source_base: None, + executable, + }); + } + + pub fn mark_runtime_region_executable(&mut self, base_addr: u64, size: u64) -> bool { + let mut updated = false; + for region in self.runtime.runtime_regions.values_mut() { + let region_end = region.runtime_base.saturating_add(region.size); + let end = base_addr.saturating_add(size); + if base_addr < region_end && region.runtime_base < end { + region.executable = true; + updated = true; + } + } + updated + } + + pub fn runtime_region_for_pc(&self, pc: u64) -> Option<&RuntimeRegionAlias> { + self.runtime.runtime_regions.values().find(|region| { + pc >= region.runtime_base && pc < region.runtime_base.saturating_add(region.size) + }) + } + + pub fn resolve_runtime_pc(&self, pc: u64) -> Option { + let region = self.runtime_region_for_pc(pc)?; + if !region.executable { + return None; + } + let source_base = region.source_base?; + let offset = pc.checked_sub(region.runtime_base)?; + (offset < region.size).then_some(source_base.saturating_add(offset)) + } + + pub fn remap_static_pc_to_runtime(&self, static_pc: u64) -> Option { + self.runtime.runtime_regions.values().find_map(|region| { + let source_base = region.source_base?; + let offset = static_pc.checked_sub(source_base)?; + (offset < region.size && region.executable) + .then_some(region.runtime_base.saturating_add(offset)) + }) + } + + pub fn set_value_provenance(&mut self, name: &str, provenance: Option) { + if let Some(provenance) = provenance { + self.runtime + .value_provenance + .insert(name.to_string(), provenance); + } else { + self.runtime.value_provenance.remove(name); + } + } + + pub fn value_provenance(&self, name: &str) -> Option<&RuntimeValueProvenance> { + self.runtime.value_provenance.get(name) + } + + pub fn note_runtime_store_copy( + &mut self, + store_addr: u64, + size: u32, + provenance: Option<&RuntimeValueProvenance>, + ) { + let Some(provenance) = provenance else { + return; + }; + let Some(region_base) = self + .runtime + .runtime_regions + .values() + .find(|region| { + store_addr >= region.runtime_base + && store_addr < region.runtime_base.saturating_add(region.size) + }) + .map(|region| region.runtime_base) + else { + return; + }; + let Some(region) = self.runtime.runtime_regions.get_mut(®ion_base) else { + return; + }; + let Some(offset) = store_addr.checked_sub(region.runtime_base) else { + return; + }; + let Some(candidate_source_base) = provenance.source_addr.checked_sub(offset) else { + return; + }; + match region.source_base { + None => region.source_base = Some(candidate_source_base), + Some(existing) if existing == candidate_source_base => {} + Some(_) => region.source_base = None, + } + let copied_extent = offset.saturating_add(size as u64); + if copied_extent > region.size { + region.size = copied_extent; + } + } + + pub fn set_pending_exception(&mut self, pending: PendingExceptionContinuation) { + self.runtime.pending_exception = Some(pending); + } + + pub fn pending_exception(&self) -> Option<&PendingExceptionContinuation> { + self.runtime.pending_exception.as_ref() + } + + pub(crate) fn clear_pending_exception(&mut self) { + self.runtime.pending_exception = None; + } + + fn write_u32(&mut self, addr: u64, value: u32) { + self.mem_write( + &SymValue::concrete(addr, 64), + &SymValue::concrete(value as u64, 32), + 4, + ); + } + + fn write_u64(&mut self, addr: u64, value: u64) { + self.mem_write( + &SymValue::concrete(addr, 64), + &SymValue::concrete(value, 64), + 8, + ); + } + + fn read_register_family(&self, base: &str) -> SymValue<'ctx> { + let lower = base.to_ascii_lowercase(); + let mut best: Option<(u64, &String)> = None; + for key in self.registers.keys() { + let key_lower = key.to_ascii_lowercase(); + if let Some((prefix, suffix)) = key_lower.rsplit_once('_') + && prefix == lower + && let Ok(version) = suffix.parse::() + { + if best.is_none_or(|(best_version, _)| version > best_version) { + best = Some((version, key)); + } + } else if key_lower == lower { + return self.get_register_sized(key, 64); + } + } + best.map_or_else( + || SymValue::unknown(64), + |(_, key)| self.get_register_sized(key, 64), + ) + } + + fn write_register_to_context(&mut self, context_addr: u64, base: &str, offset: u64) { + let value = self.read_register_family(base); + self.mem_write( + &SymValue::concrete(context_addr.saturating_add(offset), 64), + &value, + 8, + ); + } + + pub(crate) fn seed_exception_continuation(&mut self, exception_code: u64, handler_addr: u64) { + let (_, record_addr) = self.allocate_heap_region("veh_exception_record", 0x20); + let (_, context_addr) = self.allocate_heap_region("veh_exception_context", 0x400); + let (_, pointers_addr) = self.allocate_heap_region("veh_exception_pointers", 0x10); + + self.write_u32(record_addr, exception_code as u32); + self.write_u64(pointers_addr, record_addr); + self.write_u64(pointers_addr.saturating_add(8), context_addr); + self.write_register_to_context(context_addr, "RAX", 0x78); + self.write_register_to_context(context_addr, "RCX", 0x80); + self.write_register_to_context(context_addr, "RDX", 0x88); + self.write_register_to_context(context_addr, "RBX", 0x90); + self.write_register_to_context(context_addr, "RSP", 0x98); + self.write_register_to_context(context_addr, "RBP", 0xA0); + self.write_register_to_context(context_addr, "RSI", 0xA8); + self.write_register_to_context(context_addr, "RDI", 0xB0); + self.write_register_to_context(context_addr, "R8", 0xB8); + self.write_register_to_context(context_addr, "R9", 0xC0); + self.write_register_to_context(context_addr, "R10", 0xC8); + self.write_register_to_context(context_addr, "R11", 0xD0); + self.write_register_to_context(context_addr, "R12", 0xD8); + self.write_register_to_context(context_addr, "R13", 0xE0); + self.write_register_to_context(context_addr, "R14", 0xE8); + self.write_register_to_context(context_addr, "R15", 0xF0); + self.write_u64(context_addr.saturating_add(0xF8), self.pc); + self.set_concrete("RCX_0", pointers_addr, 64); + self.set_pending_exception(PendingExceptionContinuation { + handler_addr, + exception_code, + exception_pointers_addr: pointers_addr, + exception_record_addr: record_addr, + context_addr, + }); + } + + pub(crate) fn dispatch_runtime_breakpoint_if_ready(&mut self) -> bool { + let Some(breakpoint) = self.runtime.active_breakpoint.clone() else { + return false; + }; + let Some(breakpoint_addr) = breakpoint.breakpoint.as_concrete() else { + return false; + }; + if self.pc != breakpoint_addr { + return false; + } + self.runtime.active_breakpoint = None; + self.seed_exception_continuation(breakpoint.exception_code, breakpoint.handler_addr); + self.pc = breakpoint.handler_addr; + debug_runtime_continuation_log(&format!( + "runtime_breakpoint_dispatch pc=0x{:x} handler=0x{:x} code=0x{:x}", + breakpoint_addr, breakpoint.handler_addr, breakpoint.exception_code + )); + true + } + + pub(crate) fn fork_symbolic_runtime_breakpoint_at(&mut self, pc: u64) -> Option { + let breakpoint = self.runtime.active_breakpoint.clone()?; + if breakpoint.breakpoint.as_concrete().is_some() { + return None; + } + self.runtime_region_for_pc(pc)?; + + let mut dispatched = self.fork(); + dispatched.constrain_eq(&breakpoint.breakpoint, pc); + dispatched.runtime.active_breakpoint = None; + dispatched.seed_exception_continuation(breakpoint.exception_code, breakpoint.handler_addr); + dispatched.pc = breakpoint.handler_addr; + self.constrain_ne(&breakpoint.breakpoint, pc); + + debug_runtime_continuation_log(&format!( + "runtime_breakpoint_symbolic_fork pc=0x{:x} handler=0x{:x} code=0x{:x}", + pc, breakpoint.handler_addr, breakpoint.exception_code + )); + Some(dispatched) + } + + pub fn set_pending_exception_resume_pc(&mut self, resume_pc: u64) -> bool { + let Some(pending) = self.runtime.pending_exception.as_ref() else { + return false; + }; + self.mem_write( + &SymValue::concrete(pending.context_addr.saturating_add(0xF8), 64), + &SymValue::concrete(resume_pc, 64), + 8, + ); + true + } + + pub fn resume_pending_exception_continuation( + &mut self, + ) -> Result, RuntimeBlockReason> { + let Some(pending) = self.runtime.pending_exception.clone() else { + return Ok(None); + }; + let read_u32 = |state: &Self, addr: u64| state.mem_read(&SymValue::concrete(addr, 64), 4); + let read_u64 = |state: &Self, addr: u64| state.mem_read(&SymValue::concrete(addr, 64), 8); + let restore_register = |state: &mut Self, name: &str, offset: u64| { + let value = read_u64(state, pending.context_addr.saturating_add(offset)); + state.set_register(&format!("{name}_0"), value); + }; + restore_register(self, "RAX", 0x78); + restore_register(self, "RCX", 0x80); + restore_register(self, "RDX", 0x88); + restore_register(self, "RBX", 0x90); + restore_register(self, "RSP", 0x98); + restore_register(self, "RBP", 0xA0); + restore_register(self, "RSI", 0xA8); + restore_register(self, "RDI", 0xB0); + restore_register(self, "R8", 0xB8); + restore_register(self, "R9", 0xC0); + restore_register(self, "R10", 0xC8); + restore_register(self, "R11", 0xD0); + restore_register(self, "R12", 0xD8); + restore_register(self, "R13", 0xE0); + restore_register(self, "R14", 0xE8); + restore_register(self, "R15", 0xF0); + let rip = read_u64(self, pending.context_addr.saturating_add(0xF8)); + let Some(rip) = rip.as_concrete() else { + return Err(RuntimeBlockReason::MissingContinuationSeed); + }; + let breakpoint = read_u64(self, pending.context_addr.saturating_add(0x48)); + let debug_context_requested = read_u32(self, pending.context_addr.saturating_add(0x30)) + .as_concrete() + .is_some_and(|value| value & 0x10 != 0); + let dr7_enabled = read_u64(self, pending.context_addr.saturating_add(0x70)) + .as_concrete() + .is_some_and(|value| value != 0); + let breakpoint_enabled = debug_context_requested || dr7_enabled; + self.runtime.active_breakpoint = if pending.exception_code == 0x8000_0004 + && breakpoint_enabled + && self.runtime_region_for_pc(rip).is_some() + && !breakpoint.is_unknown() + { + Some(RuntimeBreakpointContinuation { + handler_addr: pending.handler_addr, + exception_code: pending.exception_code, + breakpoint: breakpoint.clone(), + }) + } else { + None + }; + debug_runtime_continuation_log(&format!( + "runtime_resume handler=0x{:x} code=0x{:x} rip=0x{:x} breakpoint={} debug_context={} dr7={} active={}", + pending.handler_addr, + pending.exception_code, + rip, + breakpoint.as_concrete().map_or_else( + || format!("symbolic:{}b", breakpoint.bits()), + |value| format!("0x{value:x}") + ), + debug_context_requested, + dr7_enabled, + self.runtime.active_breakpoint.is_some() + )); + self.runtime.pending_exception = None; + Ok(Some(rip)) + } + + pub(crate) fn semantic_fingerprint(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.pc.hash(&mut hasher); + self.active.hash(&mut hasher); + self.exit_status.hash(&mut hasher); + self.memory.semantic_fingerprint().hash(&mut hasher); + self.runtime.skip_sleep_calls.hash(&mut hasher); + + let mut tty_fds: Vec<_> = self.runtime.tty_fds.iter().copied().collect(); + tty_fds.sort_unstable(); + tty_fds.hash(&mut hasher); + + self.runtime.exception_handlers.hash(&mut hasher); + for (base, region) in &self.runtime.runtime_regions { + base.hash(&mut hasher); + region.hash(&mut hasher); + } + let mut provenance = self.runtime.value_provenance.iter().collect::>(); + provenance.sort_unstable_by(|a, b| a.0.cmp(b.0)); + for (name, source) in provenance { + name.hash(&mut hasher); + source.hash(&mut hasher); + } + self.runtime.pending_exception.hash(&mut hasher); + if let Some(breakpoint) = &self.runtime.active_breakpoint { + breakpoint.handler_addr.hash(&mut hasher); + breakpoint.exception_code.hash(&mut hasher); + hash_sym_value(self.ctx, &breakpoint.breakpoint, &mut hasher); + } + + let mut register_names: Vec<_> = self.registers.keys().collect(); + register_names.sort_unstable(); + for name in register_names { + name.hash(&mut hasher); + hash_sym_value(self.ctx, &self.registers[name], &mut hasher); + } + + self.known_zero_values.hash(&mut hasher); + self.known_nonzero_values.hash(&mut hasher); + + let mut symbolic_input_names: Vec<_> = self.symbolic_inputs.keys().collect(); + symbolic_input_names.sort_unstable(); + for name in symbolic_input_names { + name.hash(&mut hasher); + hash_sym_value(self.ctx, &self.symbolic_inputs[name], &mut hasher); + } + + for region in self.symbolic_memory.iter() { + region.region_id.hash(&mut hasher); + region.name.hash(&mut hasher); + region.addr.hash(&mut hasher); + region.size.hash(&mut hasher); + hash_sym_value(self.ctx, ®ion.value, &mut hasher); + } + + let mut fd_inputs: Vec<_> = self.symbolic_fd_inputs.iter().collect(); + fd_inputs.sort_unstable_by(|a, b| a.0.cmp(b.0)); + for (fd, input) in fd_inputs { + fd.hash(&mut hasher); + input.name.hash(&mut hasher); + input.cursor.hash(&mut hasher); + for byte in &input.bytes { + hash_sym_value(self.ctx, byte, &mut hasher); + } + } + + hasher.finish() } /// Read from memory. pub fn mem_read(&self, addr: &SymValue<'ctx>, size: u32) -> SymValue<'ctx> { self.memory - .read_with_constraints(addr, size, &self.constraints) + .read_with_constraints(addr, size, self.constraints()) } /// Write to memory. pub fn mem_write(&mut self, addr: &SymValue<'ctx>, value: &SymValue<'ctx>, size: u32) { + let constraints = self.constraints().to_vec(); self.memory - .write_with_constraints(addr, value, size, &self.constraints); + .write_with_constraints(addr, value, size, &constraints); } /// Set the maximum number of symbolic address targets to enumerate. @@ -195,7 +853,7 @@ impl<'ctx> SymState<'ctx> { /// Compute the path condition (AND of all constraints). pub fn path_condition(&self) -> Bool { - and_all(self.ctx, &self.constraints) + and_all(self.ctx, self.constraints()) } /// Merge this state with another state at the same program counter. @@ -213,7 +871,21 @@ impl<'ctx> SymState<'ctx> { merged.active = self.active && other.active; merged.exit_status = None; merged.depth = self.depth.max(other.depth); - merged.constraints = vec![cond_self | cond_other.clone()]; + merged.constraints = ConstraintCursor::default(); + merged.materialized_constraints = OnceCell::new(); + merged.add_constraint(cond_self | cond_other.clone()); + merged.known_zero_values = Rc::new( + self.known_zero_values + .intersection(&other.known_zero_values) + .cloned() + .collect(), + ); + merged.known_nonzero_values = Rc::new( + self.known_nonzero_values + .intersection(&other.known_nonzero_values) + .cloned() + .collect(), + ); let mut keys = HashSet::new(); keys.extend(self.registers.keys().cloned()); @@ -242,57 +914,137 @@ impl<'ctx> SymState<'ctx> { let merged_val = merge_values(self.ctx, &cond_other, &val_self, &val_other); registers.insert(key, merged_val); } - merged.registers = registers; + merged.registers = Rc::new(registers); - let mut memory = self.memory.fork(); - let mut addrs = HashSet::new(); - addrs.extend(self.memory.merge_addrs()); - addrs.extend(other.memory.merge_addrs()); - for addr in addrs { - let addr_val = SymValue::concrete(addr, 64); - let val_self = self - .memory - .read_with_constraints(&addr_val, 1, &self.constraints); - let val_other = other - .memory - .read_with_constraints(&addr_val, 1, &other.constraints); - let merged_val = merge_values(self.ctx, &cond_other, &val_self, &val_other); - memory.write(&addr_val, &merged_val, 1); - } - - for (addr, value, size) in other.memory.symbolic_writes() { - if addr.as_concrete().is_none() { - memory.push_symbolic_write(addr.clone(), value.clone(), *size); - } - } - - merged.memory = memory; + merged.memory = self.memory.merge_with( + &other.memory, + self.constraints(), + other.constraints(), + &cond_other, + ); merged.symbolic_inputs = self.symbolic_inputs.clone(); - for (name, value) in &other.symbolic_inputs { - merged - .symbolic_inputs + for (name, value) in other.symbolic_inputs.iter() { + Rc::make_mut(&mut merged.symbolic_inputs) .entry(name.clone()) .or_insert_with(|| value.clone()); } merged.symbolic_memory = self.symbolic_memory.clone(); - for region in &other.symbolic_memory { - let exists = merged - .symbolic_memory - .iter() - .any(|r| r.name == region.name && r.addr == region.addr && r.size == region.size); + for region in other.symbolic_memory.iter() { + let exists = merged.symbolic_memory.iter().any(|r| { + r.region_id == region.region_id + && r.name == region.name + && r.addr == region.addr + && r.size == region.size + }); if !exists { - merged.symbolic_memory.push(region.clone()); + Rc::make_mut(&mut merged.symbolic_memory).push(region.clone()); } } + merged.symbolic_fd_inputs = self.symbolic_fd_inputs.clone(); + for (fd, other_input) in other.symbolic_fd_inputs.iter() { + let fd_inputs = Rc::make_mut(&mut merged.symbolic_fd_inputs); + match fd_inputs.get_mut(fd) { + Some(existing) => { + if existing.bytes.len() < other_input.bytes.len() { + existing.bytes = other_input.bytes.clone(); + } + existing.cursor = existing.cursor.max(other_input.cursor); + } + None => { + fd_inputs.insert(*fd, other_input.clone()); + } + } + } + + merged.runtime = self.runtime.clone(); + merged.runtime.skip_sleep_calls |= other.runtime.skip_sleep_calls; + merged + .runtime + .tty_fds + .extend(other.runtime.tty_fds.iter().copied()); + merged + .runtime + .exception_handlers + .extend(other.runtime.exception_handlers.iter().copied()); + for (base, other_region) in &other.runtime.runtime_regions { + match merged.runtime.runtime_regions.get_mut(base) { + Some(existing) => { + existing.size = existing.size.max(other_region.size); + existing.executable |= other_region.executable; + if existing.source_base != other_region.source_base { + existing.source_base = None; + } + } + None => { + merged + .runtime + .runtime_regions + .insert(*base, other_region.clone()); + } + } + } + merged.runtime.pending_exception = (self.runtime.pending_exception + == other.runtime.pending_exception) + .then(|| self.runtime.pending_exception.clone()) + .flatten(); + merged.runtime.active_breakpoint = match ( + &self.runtime.active_breakpoint, + &other.runtime.active_breakpoint, + ) { + (Some(left), Some(right)) if runtime_breakpoint_equal(self.ctx, left, right) => { + Some(left.clone()) + } + _ => None, + }; + merged.runtime.value_provenance.retain(|name, source| { + other + .runtime + .value_provenance + .get(name) + .is_some_and(|other_source| other_source == source) + }); + merged } /// Add a path constraint. pub fn add_constraint(&mut self, constraint: Bool) { - self.constraints.push(constraint); + self.constraints = self.constraints.push(constraint.clone()); + if let Some(mut cached) = self.materialized_constraints.take() { + cached.push(constraint); + let _ = self.materialized_constraints.set(cached); + } + } + + fn mark_value_zero(&mut self, value: &SymValue<'ctx>) { + let Some(key) = value.symbolic_key() else { + return; + }; + Rc::make_mut(&mut self.known_nonzero_values).remove(&key); + Rc::make_mut(&mut self.known_zero_values).insert(key); + } + + fn mark_value_nonzero(&mut self, value: &SymValue<'ctx>) { + let Some(key) = value.symbolic_key() else { + return; + }; + Rc::make_mut(&mut self.known_zero_values).remove(&key); + Rc::make_mut(&mut self.known_nonzero_values).insert(key); + } + + pub(crate) fn value_known_zero(&self, value: &SymValue<'ctx>) -> bool { + value + .symbolic_key() + .is_some_and(|key| self.known_zero_values.contains(&key)) + } + + pub(crate) fn value_known_nonzero(&self, value: &SymValue<'ctx>) -> bool { + value + .symbolic_key() + .is_some_and(|key| self.known_nonzero_values.contains(&key)) } /// Constrain a value to equal a concrete constant. @@ -300,6 +1052,9 @@ impl<'ctx> SymState<'ctx> { let bv = value.to_bv(self.ctx); let rhs_bv = BV::from_u64(rhs, value.bits()); self.add_constraint(bv.eq(&rhs_bv)); + if rhs == 0 { + self.mark_value_zero(value); + } } /// Constrain a value to not equal a concrete constant. @@ -307,6 +1062,9 @@ impl<'ctx> SymState<'ctx> { let bv = value.to_bv(self.ctx); let rhs_bv = BV::from_u64(rhs, value.bits()); self.add_constraint(bv.eq(&rhs_bv).not()); + if rhs == 0 { + self.mark_value_nonzero(value); + } } /// Constrain a value to be within an unsigned range [min, max]. @@ -422,7 +1180,8 @@ impl<'ctx> SymState<'ctx> { let bv = value.to_bv(self.ctx); let zero = BV::from_u64(0, value.bits()); let cond = bv.eq(&zero).not(); - self.constraints.push(cond); + self.add_constraint(cond); + self.mark_value_nonzero(value); } /// Add a constraint that a value is false (zero). @@ -430,17 +1189,35 @@ impl<'ctx> SymState<'ctx> { let bv = value.to_bv(self.ctx); let zero = BV::from_u64(0, value.bits()); let cond = bv.eq(&zero); - self.constraints.push(cond); + self.add_constraint(cond); + self.mark_value_zero(value); } /// Get all path constraints. pub fn constraints(&self) -> &[Bool] { - &self.constraints + self.materialized_constraints + .get_or_init(|| self.constraints.materialize()) + .as_slice() } /// Get the number of constraints. pub fn num_constraints(&self) -> usize { - self.constraints.len() + self.constraints.depth() + } + + pub(crate) fn constraint_cursor_key(&self) -> ConstraintCursorKey { + self.constraints.key() + } + + pub(crate) fn constraints_imply_by_prefix(&self, other: &Self) -> bool { + self.constraints.is_descendant_of(&other.constraints) + } + + pub(crate) fn constraint_suffix_from_cursor( + &self, + ancestor: ConstraintCursorKey, + ) -> Option> { + self.constraints.suffix_from_key(ancestor) } /// Terminate this state with the given status. @@ -459,6 +1236,11 @@ impl<'ctx> SymState<'ctx> { self.depth += 1; } + /// Increment the execution depth by a known number of concrete steps. + pub fn step_by(&mut self, steps: usize) { + self.depth = self.depth.saturating_add(steps); + } + /// Fork this state (for branching). pub fn fork(&self) -> Self { Self { @@ -466,6 +1248,9 @@ impl<'ctx> SymState<'ctx> { registers: self.registers.clone(), memory: self.memory.fork(), constraints: self.constraints.clone(), + materialized_constraints: OnceCell::new(), + known_zero_values: self.known_zero_values.clone(), + known_nonzero_values: self.known_nonzero_values.clone(), pc: self.pc, prev_pc: self.prev_pc, active: self.active, @@ -473,6 +1258,8 @@ impl<'ctx> SymState<'ctx> { depth: self.depth, symbolic_inputs: self.symbolic_inputs.clone(), symbolic_memory: self.symbolic_memory.clone(), + symbolic_fd_inputs: self.symbolic_fd_inputs.clone(), + runtime: self.runtime.clone(), } } @@ -486,7 +1273,7 @@ impl<'ctx> SymState<'ctx> { /// Create a named symbolic input value. pub fn new_symbolic_input(&mut self, name: &str, bits: u32) -> SymValue<'ctx> { let value = SymValue::new_symbolic(self.ctx, name, bits); - self.symbolic_inputs.insert(name.to_string(), value.clone()); + Rc::make_mut(&mut self.symbolic_inputs).insert(name.to_string(), value.clone()); value } @@ -498,7 +1285,7 @@ impl<'ctx> SymState<'ctx> { taint: u64, ) -> SymValue<'ctx> { let value = SymValue::new_symbolic_tainted(self.ctx, name, bits, taint); - self.symbolic_inputs.insert(name.to_string(), value.clone()); + Rc::make_mut(&mut self.symbolic_inputs).insert(name.to_string(), value.clone()); value } @@ -520,10 +1307,13 @@ impl<'ctx> SymState<'ctx> { } else { SymValue::new_symbolic_tainted(self.ctx, name, size * 8, taint) }; + let region_id = + self.define_memory_region(MemoryRegionKind::Input, name, Some(addr), Some(size as u64)); let addr_val = SymValue::concrete(addr, 64); self.mem_write(&addr_val, &value, size); - self.symbolic_inputs.insert(name.to_string(), value.clone()); - self.symbolic_memory.push(SymbolicMemoryRegion { + Rc::make_mut(&mut self.symbolic_inputs).insert(name.to_string(), value.clone()); + Rc::make_mut(&mut self.symbolic_memory).push(SymbolicMemoryRegion { + region_id, name: name.to_string(), addr, size, @@ -531,6 +1321,145 @@ impl<'ctx> SymState<'ctx> { }); value } + + /// Define a canonical memory region for the symbolic state. + pub fn define_memory_region( + &mut self, + kind: MemoryRegionKind, + name: &str, + base_addr: Option, + extent: Option, + ) -> MemoryRegionId { + self.memory.define_region(kind, name, base_addr, extent) + } + + /// Seed concrete bytes into an existing memory region. + pub fn seed_region_bytes(&mut self, region_id: MemoryRegionId, offset: u64, bytes: &[u8]) { + self.memory.seed_region_bytes(region_id, offset, bytes); + } + + /// Allocate a deterministic heap region and return its concrete base pointer. + pub fn allocate_heap_region(&mut self, name: &str, size: u64) -> (MemoryRegionId, u64) { + self.memory.allocate_heap_region(name, size) + } + + /// Configure tty behavior for a concrete file descriptor. + pub fn set_tty_fd(&mut self, fd: i32, is_tty: bool) { + if is_tty { + self.runtime.tty_fds.insert(fd); + } else { + self.runtime.tty_fds.remove(&fd); + } + } + + /// Check whether a file descriptor should behave like a tty. + pub fn is_tty_fd(&self, fd: i32) -> bool { + self.runtime.tty_fds.contains(&fd) + } + + /// Configure whether sleep-family calls should be skipped. + pub fn set_skip_sleep_calls(&mut self, enabled: bool) { + self.runtime.skip_sleep_calls = enabled; + } + + /// Check whether sleep-family calls should be skipped. + pub fn skip_sleep_calls(&self) -> bool { + self.runtime.skip_sleep_calls + } + + /// Register a symbolic byte stream for a file descriptor. + pub fn add_symbolic_fd_input( + &mut self, + fd: i32, + len: usize, + name: &str, + alphabet: Option<&str>, + ) { + let mut bytes = Vec::with_capacity(len); + for idx in 0..len { + let byte_name = format!("{}_{}", name, idx); + let byte = SymValue::new_symbolic(self.ctx, &byte_name, 8); + if let Some(alphabet) = alphabet { + constrain_symbolic_byte_to_alphabet(self, &byte, alphabet); + } + Rc::make_mut(&mut self.symbolic_inputs).insert(byte_name, byte.clone()); + bytes.push(byte); + } + Rc::make_mut(&mut self.symbolic_fd_inputs).insert( + fd, + SymbolicFdInput { + name: name.to_string(), + fd, + bytes, + cursor: 0, + }, + ); + } + + /// Read up to `count` bytes from a tracked symbolic file descriptor. + pub fn read_symbolic_fd_bytes(&mut self, fd: i32, count: usize) -> Option>> { + let input = Rc::make_mut(&mut self.symbolic_fd_inputs).get_mut(&fd)?; + if input.cursor >= input.bytes.len() { + return Some(Vec::new()); + } + let end = input.cursor.saturating_add(count).min(input.bytes.len()); + let bytes = input.bytes[input.cursor..end].to_vec(); + input.cursor = end; + Some(bytes) + } +} + +fn mix_hash(seed: u64, value: u64) -> u64 { + seed.rotate_left(7) ^ value.wrapping_mul(0x9e37_79b9_7f4a_7c15) +} + +fn next_constraint_cursor_key() -> ConstraintCursorKey { + static NEXT_ID: AtomicUsize = AtomicUsize::new(1); + ConstraintCursorKey(NEXT_ID.fetch_add(1, Ordering::Relaxed)) +} + +fn structural_hash(value: &T) -> u64 { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + hasher.finish() +} + +fn runtime_breakpoint_equal<'ctx>( + ctx: &'ctx Context, + left: &RuntimeBreakpointContinuation<'ctx>, + right: &RuntimeBreakpointContinuation<'ctx>, +) -> bool { + if left.handler_addr != right.handler_addr || left.exception_code != right.exception_code { + return false; + } + let mut left_hasher = DefaultHasher::new(); + hash_sym_value(ctx, &left.breakpoint, &mut left_hasher); + let mut right_hasher = DefaultHasher::new(); + hash_sym_value(ctx, &right.breakpoint, &mut right_hasher); + left_hasher.finish() == right_hasher.finish() +} + +fn hash_sym_value<'ctx, H: Hasher>(ctx: &'ctx Context, value: &SymValue<'ctx>, hasher: &mut H) { + value.bits().hash(hasher); + value.get_taint().hash(hasher); + value.to_bv(ctx).hash(hasher); +} + +fn constrain_symbolic_byte_to_alphabet<'ctx>( + state: &mut SymState<'ctx>, + value: &SymValue<'ctx>, + alphabet: &str, +) { + let allowed = alphabet.as_bytes(); + if allowed.is_empty() { + return; + } + let byte_bv = value.to_bv(state.context()); + let ors: Vec = allowed + .iter() + .map(|byte| byte_bv.eq(BV::from_u64(*byte as u64, 8))) + .collect(); + state.add_constraint(or_all(state.context(), &ors)); } fn parse_byte_ranges(pattern: &str) -> Vec<(u8, u8)> { @@ -619,7 +1548,7 @@ impl<'ctx> std::fmt::Debug for SymState<'ctx> { .field("pc", &format!("0x{:x}", self.pc)) .field("prev_pc", &self.prev_pc.map(|pc| format!("0x{:x}", pc))) .field("registers", &self.registers.len()) - .field("constraints", &self.constraints.len()) + .field("constraints", &self.num_constraints()) .field("depth", &self.depth) .field("symbolic_inputs", &self.symbolic_inputs.len()) .field("symbolic_memory", &self.symbolic_memory.len()) @@ -765,4 +1694,231 @@ mod tests { .unwrap(); assert_eq!(val, 2); } + + #[test] + fn test_merge_memory_preserves_region_only_present_on_one_side() { + let ctx = Context::thread_local(); + + let mut state_a = SymState::new(&ctx, 0x1000); + let x_a = SymValue::new_symbolic(&ctx, "x", 32); + state_a.set_register("x", x_a.clone()); + state_a.add_constraint(x_a.to_bv(&ctx).eq(BV::from_u64(0, 32))); + + let mut state_b = SymState::new(&ctx, 0x1000); + let x_b = SymValue::new_symbolic(&ctx, "x", 32); + state_b.set_register("x", x_b.clone()); + state_b.add_constraint(x_b.to_bv(&ctx).eq(BV::from_u64(1, 32))); + state_b.define_memory_region(MemoryRegionKind::Replay, "replay", Some(0x9000), Some(0x10)); + state_b.mem_write( + &SymValue::concrete(0x9000, 64), + &SymValue::concrete(0xab, 8), + 1, + ); + + let merged = state_a.merge_with(&state_b); + let merged_byte = merged.mem_read(&SymValue::concrete(0x9000, 64), 1); + + let solver = Solver::new(); + solver.assert(merged.path_condition()); + solver.assert(x_b.to_bv(&ctx).eq(BV::from_u64(0, 32))); + assert_eq!(solver.check(), SatResult::Sat); + let model = solver.get_model().unwrap(); + let value = model + .eval(&merged_byte.to_bv(&ctx), true) + .unwrap() + .as_u64() + .unwrap(); + assert_eq!(value, 0); + + let solver = Solver::new(); + solver.assert(merged.path_condition()); + solver.assert(x_b.to_bv(&ctx).eq(BV::from_u64(1, 32))); + assert_eq!(solver.check(), SatResult::Sat); + let model = solver.get_model().unwrap(); + let value = model + .eval(&merged_byte.to_bv(&ctx), true) + .unwrap() + .as_u64() + .unwrap(); + assert_eq!(value, 0xab); + } + + #[test] + fn test_runtime_region_copy_alias_resolves_runtime_pc() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + + let _ = state.define_runtime_region("jit_blob", 0x6000_0000, 0x1000, true); + state.note_runtime_store_copy( + 0x6000_0010, + 1, + Some(&RuntimeValueProvenance { + source_addr: 0x1400_1000, + size: 1, + }), + ); + + assert_eq!(state.resolve_runtime_pc(0x6000_0010), Some(0x1400_1000)); + assert_eq!( + state.remap_static_pc_to_runtime(0x1400_1010), + Some(0x6000_0020) + ); + } + + #[test] + fn test_resume_pending_exception_continuation_restores_rip() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let (_, context_addr) = state.allocate_heap_region("context", 0x400); + state.mem_write( + &SymValue::concrete(context_addr.saturating_add(0x80), 64), + &SymValue::concrete(0x1122_3344_5566_7788, 64), + 8, + ); + state.mem_write( + &SymValue::concrete(context_addr.saturating_add(0xF8), 64), + &SymValue::concrete(0x6000_1234, 64), + 8, + ); + state.set_pending_exception(PendingExceptionContinuation { + handler_addr: 0x401000, + exception_code: 0x8000_0004, + exception_pointers_addr: 0x7000_0000, + exception_record_addr: 0x7000_0100, + context_addr, + }); + + let resumed = state + .resume_pending_exception_continuation() + .expect("continuation should resume"); + + assert_eq!(resumed, Some(0x6000_1234)); + assert_eq!( + state.get_register("RCX_0").as_concrete(), + Some(0x1122_3344_5566_7788) + ); + assert!(state.pending_exception().is_none()); + } + + #[test] + fn test_set_pending_exception_resume_pc_updates_context_rip() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let (_, context_addr) = state.allocate_heap_region("context", 0x400); + state.set_pending_exception(PendingExceptionContinuation { + handler_addr: 0x401000, + exception_code: 0x8000_0004, + exception_pointers_addr: 0x7000_0000, + exception_record_addr: 0x7000_0100, + context_addr, + }); + + assert!(state.set_pending_exception_resume_pc(0x6000_5678)); + assert_eq!( + state + .mem_read( + &SymValue::concrete(context_addr.saturating_add(0xF8), 64), + 8 + ) + .as_concrete(), + Some(0x6000_5678) + ); + } + + #[test] + fn test_runtime_breakpoint_continuation_reenters_handler() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let _ = state.define_runtime_region("jit", 0x6000_0000, 0x1000, true); + state.note_runtime_store_copy( + 0x6000_0000, + 0x1000, + Some(&RuntimeValueProvenance { + source_addr: 0x1400_0000, + size: 0x1000, + }), + ); + let (_, context_addr) = state.allocate_heap_region("context", 0x400); + state.mem_write( + &SymValue::concrete(context_addr.saturating_add(0x48), 64), + &SymValue::concrete(0x6000_0020, 64), + 8, + ); + state.mem_write( + &SymValue::concrete(context_addr.saturating_add(0x70), 64), + &SymValue::concrete(1, 64), + 8, + ); + state.mem_write( + &SymValue::concrete(context_addr.saturating_add(0xF8), 64), + &SymValue::concrete(0x6000_0010, 64), + 8, + ); + state.set_pending_exception(PendingExceptionContinuation { + handler_addr: 0x401000, + exception_code: 0x8000_0004, + exception_pointers_addr: 0x7000_0000, + exception_record_addr: 0x7000_0100, + context_addr, + }); + + let resumed = state + .resume_pending_exception_continuation() + .expect("runtime continuation should resume"); + + assert_eq!(resumed, Some(0x6000_0010)); + state.pc = 0x6000_0020; + assert!(state.dispatch_runtime_breakpoint_if_ready()); + assert_eq!(state.pc, 0x401000); + assert!(state.pending_exception().is_some()); + } + + #[test] + fn test_runtime_breakpoint_continuation_accepts_debug_context_flag() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let _ = state.define_runtime_region("jit", 0x6000_0000, 0x1000, true); + state.note_runtime_store_copy( + 0x6000_0000, + 0x1000, + Some(&RuntimeValueProvenance { + source_addr: 0x1400_0000, + size: 0x1000, + }), + ); + let (_, context_addr) = state.allocate_heap_region("context", 0x400); + state.mem_write( + &SymValue::concrete(context_addr.saturating_add(0x30), 64), + &SymValue::concrete(0x100010, 32), + 4, + ); + state.mem_write( + &SymValue::concrete(context_addr.saturating_add(0x48), 64), + &SymValue::concrete(0x6000_0030, 64), + 8, + ); + state.mem_write( + &SymValue::concrete(context_addr.saturating_add(0xF8), 64), + &SymValue::concrete(0x6000_0010, 64), + 8, + ); + state.set_pending_exception(PendingExceptionContinuation { + handler_addr: 0x401000, + exception_code: 0x8000_0004, + exception_pointers_addr: 0x7000_0000, + exception_record_addr: 0x7000_0100, + context_addr, + }); + + assert_eq!( + state + .resume_pending_exception_continuation() + .expect("debug-register continuation should resume"), + Some(0x6000_0010) + ); + state.pc = 0x6000_0030; + assert!(state.dispatch_runtime_breakpoint_if_ready()); + assert_eq!(state.pc, 0x401000); + assert!(state.pending_exception().is_some()); + } } diff --git a/crates/r2sym/src/tactics.rs b/crates/r2sym/src/tactics.rs new file mode 100644 index 0000000..b1019b8 --- /dev/null +++ b/crates/r2sym/src/tactics.rs @@ -0,0 +1,1930 @@ +//! Solver tactics over canonical semantic evidence. +//! +//! Tactics must not claim proof. They only add typed constraints that bias +//! model extraction toward useful candidates; replay/verification remains the +//! authority for `solved`. + +use std::collections::{BTreeMap, BTreeSet}; + +use z3::ast::{BV, Bool}; + +use crate::constraints::{ + FinalConstraintGraph, RecurrenceAggregateConstraint, RecurrenceAggregateRangeConstraint, +}; +use crate::loops::{ + ExactLoopFoldEvidence, ExactLoopRecurrenceEvidence, ExactLoopRecurrenceKind, LoopFoldOperation, + LoopMemoryTerm, LoopMemoryTermKind, LoopRotateDirection, +}; +use crate::{SymState, SymValue, aggregate_exact_fold_bytes}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InputByteDomain { + Any, + PrintableAscii, + AlphaNum, + Ranges(Vec<(u8, u8)>), + Bytes(Vec), +} + +impl InputByteDomain { + pub fn printable_ascii() -> Self { + Self::PrintableAscii + } + + pub fn ranges(&self) -> Vec<(u8, u8)> { + match self { + Self::Any => vec![(0x00, 0xff)], + Self::PrintableAscii => vec![(0x20, 0x7e)], + Self::AlphaNum => vec![(b'0', b'9'), (b'A', b'Z'), (b'a', b'z')], + Self::Ranges(ranges) => normalize_ranges(ranges.iter().copied()), + Self::Bytes(bytes) => normalize_ranges(bytes.iter().copied().map(|byte| (byte, byte))), + } + } + + fn is_any(&self) -> bool { + matches!(self, Self::Any) + } + + pub fn allowed_bytes(&self) -> Vec { + self.ranges() + .into_iter() + .flat_map(|(start, end)| start..=end) + .collect() + } + + pub fn contains(&self, byte: u8) -> bool { + self.ranges() + .into_iter() + .any(|(start, end)| byte >= start && byte <= end) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SolveTacticConfig { + pub enabled: bool, + pub preferred_domains: Vec, + pub max_constrained_bytes: usize, + pub max_candidates: usize, + pub max_mitm_table: usize, + pub max_target_enumeration: usize, +} + +impl Default for SolveTacticConfig { + fn default() -> Self { + Self { + enabled: true, + preferred_domains: vec![InputByteDomain::PrintableAscii, InputByteDomain::Any], + max_constrained_bytes: 256, + max_candidates: 32, + max_mitm_table: 32_768, + max_target_enumeration: 256, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ConstraintCandidateStrategy { + Algebraic, + Mitm, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TacticConstraintReport { + pub constrained_bytes: usize, + pub skipped_reasons: Vec, +} + +impl TacticConstraintReport { + fn new(constrained_bytes: usize, skipped_reasons: Vec) -> Self { + Self { + constrained_bytes, + skipped_reasons, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SolveTacticCandidate { + pub recurrence: ExactLoopRecurrenceEvidence, + pub domain: InputByteDomain, + pub bytes: Vec, + pub target: u64, + pub used_mitm: bool, + pub reason: String, +} + +pub fn constrain_exact_fold_inputs<'ctx>( + state: &mut SymState<'ctx>, + folds: &[ExactLoopFoldEvidence], + domain: &InputByteDomain, + max_constrained_bytes: usize, +) -> TacticConstraintReport { + if domain.is_any() { + return TacticConstraintReport::new(0, Vec::new()); + } + + let mut addresses = BTreeSet::new(); + let mut skipped_reasons = Vec::new(); + for fold in folds { + if fold.term.kind != LoopMemoryTermKind::InputRead { + continue; + } + if fold.term.bytes != 1 { + push_unique( + &mut skipped_reasons, + format!("unsupported_fold_read_width:{}", fold.term.bytes), + ); + continue; + } + let (Some(base), Some(stride)) = (fold.term.base, fold.term.stride) else { + push_unique(&mut skipped_reasons, "missing_fold_input_address"); + continue; + }; + for iteration in 0..fold.iterations { + if addresses.len() >= max_constrained_bytes { + push_unique(&mut skipped_reasons, "tactic_input_byte_budget"); + break; + } + let Some(offset) = iteration.checked_mul(stride) else { + push_unique(&mut skipped_reasons, "fold_input_address_overflow"); + break; + }; + let Some(addr) = base.checked_add(offset) else { + push_unique(&mut skipped_reasons, "fold_input_address_overflow"); + break; + }; + addresses.insert(addr); + } + } + + let constrained_bytes = addresses.len(); + for addr in addresses { + let value = state.mem_read(&SymValue::concrete(addr, 64), 1); + constrain_byte_to_domain(state, &value, domain); + } + + TacticConstraintReport::new(constrained_bytes, skipped_reasons) +} + +pub fn constrain_exact_fold_candidate<'ctx>( + state: &mut SymState<'ctx>, + fold: &ExactLoopFoldEvidence, + bytes: &[u8], +) -> TacticConstraintReport { + let mut skipped_reasons = Vec::new(); + if fold.term.kind != LoopMemoryTermKind::InputRead { + return TacticConstraintReport::new(0, skipped_reasons); + } + if fold.term.bytes != 1 { + push_unique( + &mut skipped_reasons, + format!("unsupported_fold_read_width:{}", fold.term.bytes), + ); + return TacticConstraintReport::new(0, skipped_reasons); + } + if bytes.len() != fold.iterations as usize { + push_unique(&mut skipped_reasons, "candidate_length_mismatch"); + return TacticConstraintReport::new(0, skipped_reasons); + } + let (Some(base), Some(stride)) = (fold.term.base, fold.term.stride) else { + push_unique(&mut skipped_reasons, "missing_fold_input_address"); + return TacticConstraintReport::new(0, skipped_reasons); + }; + for (iteration, byte) in bytes.iter().copied().enumerate() { + let Some(offset) = (iteration as u64).checked_mul(stride) else { + push_unique(&mut skipped_reasons, "fold_input_address_overflow"); + continue; + }; + let Some(addr) = base.checked_add(offset) else { + push_unique(&mut skipped_reasons, "fold_input_address_overflow"); + continue; + }; + let value = state.mem_read(&SymValue::concrete(addr, 64), 1); + state.constrain_eq(&value, byte as u64); + } + TacticConstraintReport::new(bytes.len(), skipped_reasons) +} + +fn constrain_input_term_candidate<'ctx>( + state: &mut SymState<'ctx>, + term: &LoopMemoryTerm, + iterations: u64, + bytes: &[u8], +) -> TacticConstraintReport { + let mut skipped_reasons = Vec::new(); + if term.kind != LoopMemoryTermKind::InputRead { + return TacticConstraintReport::new(0, skipped_reasons); + } + if term.bytes != 1 { + push_unique( + &mut skipped_reasons, + format!("unsupported_recurrence_read_width:{}", term.bytes), + ); + return TacticConstraintReport::new(0, skipped_reasons); + } + if bytes.len() != iterations as usize { + push_unique(&mut skipped_reasons, "candidate_length_mismatch"); + return TacticConstraintReport::new(0, skipped_reasons); + } + let (Some(base), Some(stride)) = (term.base, term.stride) else { + push_unique(&mut skipped_reasons, "missing_recurrence_input_address"); + return TacticConstraintReport::new(0, skipped_reasons); + }; + for (iteration, byte) in bytes.iter().copied().enumerate() { + let Some(offset) = (iteration as u64).checked_mul(stride) else { + push_unique(&mut skipped_reasons, "recurrence_input_address_overflow"); + continue; + }; + let Some(addr) = base.checked_add(offset) else { + push_unique(&mut skipped_reasons, "recurrence_input_address_overflow"); + continue; + }; + let value = state.mem_read(&SymValue::concrete(addr, 64), 1); + state.constrain_eq(&value, byte as u64); + } + TacticConstraintReport::new(bytes.len(), skipped_reasons) +} + +fn recurrence_input_term(recurrence: &ExactLoopRecurrenceEvidence) -> Option<&LoopMemoryTerm> { + match &recurrence.kind { + ExactLoopRecurrenceKind::Fold { term, .. } + | ExactLoopRecurrenceKind::RotateMix { term, .. } => Some(term), + _ => None, + } +} + +pub fn constrain_exact_recurrence_candidate<'ctx>( + state: &mut SymState<'ctx>, + recurrence: &ExactLoopRecurrenceEvidence, + bytes: &[u8], +) -> TacticConstraintReport { + let Some(term) = recurrence_input_term(recurrence) else { + return TacticConstraintReport::new( + 0, + vec!["unsupported_recurrence_candidate".to_string()], + ); + }; + constrain_input_term_candidate(state, term, recurrence.iterations, bytes) +} + +pub fn algebraic_preimage_candidate( + fold: &ExactLoopFoldEvidence, + model_bytes: &[u8], + domain: &InputByteDomain, +) -> Option> { + if fold.term.kind != LoopMemoryTermKind::InputRead || fold.term.bytes != 1 { + return None; + } + if model_bytes.len() != fold.iterations as usize || model_bytes.is_empty() { + return None; + } + match fold.operation { + LoopFoldOperation::Xor => xor_preimage_candidate(model_bytes, domain), + LoopFoldOperation::Add => add_preimage_candidate(model_bytes, domain, fold.bits), + } +} + +pub fn algebraic_preimage_for_target( + fold: &ExactLoopFoldEvidence, + target: u64, + domain: &InputByteDomain, +) -> Option> { + if fold.term.kind != LoopMemoryTermKind::InputRead || fold.term.bytes != 1 { + return None; + } + if fold.iterations == 0 { + return None; + } + match fold.operation { + LoopFoldOperation::Xor => xor_preimage_for_target(fold.iterations as usize, target, domain), + LoopFoldOperation::Add => { + add_preimage_for_target(fold.iterations as usize, target, domain, fold.bits) + } + } +} + +pub fn tactic_candidates_for_constraint_graph<'ctx>( + graph: &FinalConstraintGraph, + state: Option<&SymState<'ctx>>, + config: &SolveTacticConfig, +) -> Vec { + if !config.enabled || graph.is_empty() { + return Vec::new(); + } + let mut candidates = Vec::new(); + for constraint in graph.recurrence_aggregate_constraints() { + for domain in &config.preferred_domains { + if candidates.len() >= config.max_candidates { + return candidates; + } + if let Some(candidate) = + tactic_candidate_for_recurrence_constraint(constraint, graph, state, domain, config) + { + candidates.push(candidate); + } + } + } + for constraint in graph.recurrence_aggregate_range_constraints() { + for domain in &config.preferred_domains { + if candidates.len() >= config.max_candidates { + return candidates; + } + if let Some(candidate) = tactic_candidate_for_recurrence_range_constraint( + constraint, graph, state, domain, config, + ) { + candidates.push(candidate); + } + } + } + candidates +} + +fn xor_preimage_candidate(model_bytes: &[u8], domain: &InputByteDomain) -> Option> { + let target = model_bytes.iter().fold(0u8, |acc, byte| acc ^ byte); + xor_preimage_for_target(model_bytes.len(), target as u64, domain) +} + +fn xor_preimage_for_target(count: usize, target: u64, domain: &InputByteDomain) -> Option> { + let allowed = domain.allowed_bytes(); + if allowed.is_empty() { + return None; + } + let target = target as u8; + if count == 1 { + return domain.contains(target).then_some(vec![target]); + } + + let preferred = preferred_byte(&allowed); + let mut candidate = vec![preferred; count]; + let prefix_xor = candidate + .iter() + .take(candidate.len().saturating_sub(2)) + .fold(0u8, |acc, byte| acc ^ byte); + let pair_target = target ^ prefix_xor; + for &lhs in &allowed { + let rhs = lhs ^ pair_target; + if domain.contains(rhs) { + let last = candidate.len() - 1; + candidate[last - 1] = lhs; + candidate[last] = rhs; + return Some(candidate); + } + } + None +} + +fn add_preimage_candidate( + model_bytes: &[u8], + domain: &InputByteDomain, + bits: u32, +) -> Option> { + let target = model_bytes + .iter() + .fold(0u128, |acc, byte| acc.saturating_add(*byte as u128)); + add_preimage_for_target(model_bytes.len(), target as u64, domain, bits) +} + +fn add_preimage_for_target( + count: usize, + target: u64, + domain: &InputByteDomain, + bits: u32, +) -> Option> { + let allowed = domain.allowed_bytes(); + if allowed.is_empty() { + return None; + } + let modulus = if bits >= 64 { + None + } else { + Some(1u128 << bits.max(1)) + }; + let target = target as u128; + let target_mod = modulus.map_or(target, |modulus| target % modulus); + let min_byte = *allowed.first()? as u128; + let max_byte = *allowed.last()? as u128; + let min_sum = min_byte.saturating_mul(count as u128); + let max_sum = max_byte.saturating_mul(count as u128); + let target_sum = match modulus { + Some(modulus) => { + let mut candidate = target_mod; + while candidate < min_sum { + candidate = candidate.saturating_add(modulus); + } + if candidate > max_sum { + return None; + } + candidate + } + None if target_mod >= min_sum && target_mod <= max_sum => target_mod, + None => return None, + }; + + let mut remaining = target_sum; + let mut candidate = Vec::with_capacity(count); + for index in 0..count { + let slots_left = count - index - 1; + let min_tail = min_byte.saturating_mul(slots_left as u128); + let max_tail = max_byte.saturating_mul(slots_left as u128); + let byte = allowed.iter().copied().find(|byte| { + let value = *byte as u128; + value <= remaining + && remaining.saturating_sub(value) >= min_tail + && remaining.saturating_sub(value) <= max_tail + })?; + candidate.push(byte); + remaining = remaining.saturating_sub(byte as u128); + } + (remaining == 0).then_some(candidate) +} + +fn tactic_candidate_for_recurrence_constraint( + constraint: &RecurrenceAggregateConstraint, + graph: &FinalConstraintGraph, + state: Option<&SymState<'_>>, + domain: &InputByteDomain, + config: &SolveTacticConfig, +) -> Option { + let allowed = allowed_bytes_for_recurrence(graph, &constraint.recurrence, domain)?; + let (bytes, strategy) = candidate_for_recurrence_target_with_allowed( + &constraint.recurrence, + state, + constraint.target, + &allowed, + config, + )?; + Some(SolveTacticCandidate { + recurrence: constraint.recurrence.clone(), + domain: domain.clone(), + bytes, + target: constraint.target, + used_mitm: strategy == ConstraintCandidateStrategy::Mitm, + reason: tactic_reason_for_recurrence(&constraint.recurrence, graph, "exact", strategy), + }) +} + +fn tactic_candidate_for_recurrence_range_constraint( + constraint: &RecurrenceAggregateRangeConstraint, + graph: &FinalConstraintGraph, + state: Option<&SymState<'_>>, + domain: &InputByteDomain, + config: &SolveTacticConfig, +) -> Option { + let allowed = allowed_bytes_for_recurrence(graph, &constraint.recurrence, domain)?; + let (target, bytes, strategy) = select_target_for_range_constraint( + &constraint.recurrence, + state, + &allowed, + constraint, + config, + )?; + Some(SolveTacticCandidate { + recurrence: constraint.recurrence.clone(), + domain: domain.clone(), + bytes, + target, + used_mitm: strategy == ConstraintCandidateStrategy::Mitm, + reason: tactic_reason_for_recurrence(&constraint.recurrence, graph, "range", strategy), + }) +} + +fn tactic_reason_for_recurrence( + recurrence: &ExactLoopRecurrenceEvidence, + graph: &FinalConstraintGraph, + prefix: &str, + strategy: ConstraintCandidateStrategy, +) -> String { + let strategy = match strategy { + ConstraintCandidateStrategy::Algebraic => "algebraic", + ConstraintCandidateStrategy::Mitm => "mitm", + }; + let base = match &recurrence.kind { + ExactLoopRecurrenceKind::Fold { operation, .. } => match operation { + LoopFoldOperation::Xor => { + format!("{prefix} {strategy} xor-fold preimage from constraint graph") + } + LoopFoldOperation::Add => { + format!("{prefix} {strategy} add-fold preimage from constraint graph") + } + }, + ExactLoopRecurrenceKind::RotateMix { operation, .. } => match operation { + LoopFoldOperation::Xor => { + format!("{prefix} {strategy} rotate-xor recurrence preimage from constraint graph") + } + LoopFoldOperation::Add => { + format!("{prefix} {strategy} rotate-add recurrence preimage from constraint graph") + } + }, + _ => format!("{prefix} {strategy} exact recurrence candidate from constraint graph"), + }; + if graph.input_byte_constraints.is_empty() { + base + } else { + format!("{base}; input assumptions applied") + } +} + +fn select_target_for_range_constraint( + recurrence: &ExactLoopRecurrenceEvidence, + state: Option<&SymState<'_>>, + allowed: &[Vec], + constraint: &RecurrenceAggregateRangeConstraint, + config: &SolveTacticConfig, +) -> Option<(u64, Vec, ConstraintCandidateStrategy)> { + let preferred = preferred_candidate_bytes(allowed)?; + let preferred_target = recurrence_target_for_bytes(recurrence, state, &preferred)?; + if preferred_target >= constraint.min + && preferred_target <= constraint.max + && let Some((bytes, strategy)) = candidate_for_recurrence_target_with_allowed( + recurrence, + state, + preferred_target, + allowed, + config, + ) + { + return Some((preferred_target, bytes, strategy)); + } + let budget = config.max_target_enumeration.max(1) as u64; + let upper = constraint + .max + .min(constraint.min.saturating_add(budget.saturating_sub(1))); + (constraint.min..=upper).find_map(|target| { + candidate_for_recurrence_target_with_allowed(recurrence, state, target, allowed, config) + .map(|(bytes, strategy)| (target, bytes, strategy)) + }) +} + +fn preferred_candidate_bytes(allowed: &[Vec]) -> Option> { + let mut out = Vec::with_capacity(allowed.len()); + for choices in allowed { + out.push(preferred_byte(choices)); + } + Some(out) +} + +fn allowed_bytes_for_recurrence( + graph: &FinalConstraintGraph, + recurrence: &ExactLoopRecurrenceEvidence, + default_domain: &InputByteDomain, +) -> Option>> { + let term = recurrence_input_term(recurrence)?; + if term.kind != LoopMemoryTermKind::InputRead || term.bytes != 1 { + return None; + } + let (Some(base), Some(stride)) = (term.base, term.stride) else { + return None; + }; + if stride == 0 { + return None; + } + if let Some(length) = graph + .input_length_constraints + .iter() + .find(|constraint| constraint.base_addr == base) + && length.len != recurrence.iterations as u32 + { + return None; + } + + let mut allowed = vec![default_domain.allowed_bytes(); recurrence.iterations as usize]; + let by_addr = graph.input_byte_constraints.iter().fold( + BTreeMap::>::new(), + |mut acc, constraint| { + let mut next = constraint.allowed.clone(); + next.sort_unstable(); + next.dedup(); + acc.entry(constraint.addr) + .and_modify(|existing| { + let existing_set = existing.iter().copied().collect::>(); + let next_set = next.iter().copied().collect::>(); + *existing = existing_set + .intersection(&next_set) + .copied() + .collect::>(); + }) + .or_insert(next); + acc + }, + ); + + for (index, bytes) in allowed.iter_mut().enumerate() { + let addr = base.checked_add((index as u64).checked_mul(stride)?)?; + if let Some(constrained) = by_addr.get(&addr) { + let constrained_set = constrained.iter().copied().collect::>(); + *bytes = bytes + .iter() + .copied() + .filter(|byte| constrained_set.contains(byte)) + .collect(); + } + bytes.sort_unstable(); + bytes.dedup(); + if bytes.is_empty() { + return None; + } + } + Some(allowed) +} + +fn candidate_for_recurrence_target_with_allowed( + recurrence: &ExactLoopRecurrenceEvidence, + state: Option<&SymState<'_>>, + target: u64, + allowed: &[Vec], + config: &SolveTacticConfig, +) -> Option<(Vec, ConstraintCandidateStrategy)> { + if let Some(fold) = recurrence.as_fold() { + return candidate_for_target_with_allowed(&fold, target, allowed, config); + } + let initial = concrete_initial_for_recurrence(state, recurrence)?; + if let Some(bytes) = + rotate_mix_preimage_for_target_with_allowed(recurrence, initial, target, allowed) + { + return Some((bytes, ConstraintCandidateStrategy::Algebraic)); + } + rotate_mix_mitm_preimage_for_target_with_allowed(recurrence, initial, target, allowed, config) + .map(|bytes| (bytes, ConstraintCandidateStrategy::Mitm)) +} + +fn recurrence_target_for_bytes( + recurrence: &ExactLoopRecurrenceEvidence, + state: Option<&SymState<'_>>, + bytes: &[u8], +) -> Option { + if let Some(fold) = recurrence.as_fold() { + return aggregate_exact_fold_bytes(&fold, bytes); + } + let initial = concrete_initial_for_recurrence(state, recurrence)?; + rotate_mix_apply_bytes(recurrence, initial, bytes) +} + +fn concrete_initial_for_recurrence( + state: Option<&SymState<'_>>, + recurrence: &ExactLoopRecurrenceEvidence, +) -> Option { + let initial_name = recurrence.initial.as_str(); + if initial_name.is_empty() { + return None; + } + let value = state?.get_register_sized(initial_name, recurrence.bits.max(1)); + value + .as_concrete() + .map(|value| mask_to_bits(value, recurrence.bits)) +} + +fn candidate_for_target_with_allowed( + fold: &ExactLoopFoldEvidence, + target: u64, + allowed: &[Vec], + config: &SolveTacticConfig, +) -> Option<(Vec, ConstraintCandidateStrategy)> { + if let Some(bytes) = algebraic_preimage_for_target_with_allowed(fold, target, allowed) { + return Some((bytes, ConstraintCandidateStrategy::Algebraic)); + } + mitm_preimage_for_target_with_allowed(fold, target, allowed, config) + .map(|bytes| (bytes, ConstraintCandidateStrategy::Mitm)) +} + +fn algebraic_preimage_for_target_with_allowed( + fold: &ExactLoopFoldEvidence, + target: u64, + allowed: &[Vec], +) -> Option> { + if fold.iterations as usize != allowed.len() { + return None; + } + match fold.operation { + LoopFoldOperation::Xor => xor_preimage_for_target_with_allowed(allowed, target), + LoopFoldOperation::Add => add_preimage_for_target_with_allowed(allowed, target, fold.bits), + } +} + +fn xor_preimage_for_target_with_allowed(allowed: &[Vec], target: u64) -> Option> { + if allowed.is_empty() { + return None; + } + if allowed.len() == 1 { + let target = target as u8; + return allowed[0].contains(&target).then_some(vec![target]); + } + let mut candidate = preferred_candidate_bytes(allowed)?; + let prefix_xor = candidate + .iter() + .take(candidate.len().saturating_sub(2)) + .fold(0u8, |acc, byte| acc ^ byte); + let pair_target = (target as u8) ^ prefix_xor; + let penultimate = candidate.len() - 2; + let last = candidate.len() - 1; + let final_allowed = allowed[last].iter().copied().collect::>(); + for &lhs in &allowed[penultimate] { + let rhs = lhs ^ pair_target; + if final_allowed.contains(&rhs) { + candidate[penultimate] = lhs; + candidate[last] = rhs; + return Some(candidate); + } + } + None +} + +fn add_preimage_for_target_with_allowed( + allowed: &[Vec], + target: u64, + bits: u32, +) -> Option> { + if allowed.is_empty() { + return None; + } + let modulus = if bits >= 64 { + None + } else { + Some(1u128 << bits.max(1)) + }; + let target = target as u128; + let target_mod = modulus.map_or(target, |modulus| target % modulus); + let min_sum = allowed + .iter() + .map(|bytes| *bytes.first().unwrap_or(&0) as u128) + .sum::(); + let max_sum = allowed + .iter() + .map(|bytes| *bytes.last().unwrap_or(&0) as u128) + .sum::(); + let target_sum = match modulus { + Some(modulus) => { + let mut candidate = target_mod; + while candidate < min_sum { + candidate = candidate.saturating_add(modulus); + } + if candidate > max_sum { + return None; + } + candidate + } + None if target_mod >= min_sum && target_mod <= max_sum => target_mod, + None => return None, + }; + let mut min_suffix = vec![0u128; allowed.len() + 1]; + let mut max_suffix = vec![0u128; allowed.len() + 1]; + for index in (0..allowed.len()).rev() { + min_suffix[index] = min_suffix[index + 1] + *allowed[index].first()? as u128; + max_suffix[index] = max_suffix[index + 1] + *allowed[index].last()? as u128; + } + let mut remaining = target_sum; + let mut candidate = Vec::with_capacity(allowed.len()); + for (index, bytes) in allowed.iter().enumerate() { + let min_tail = min_suffix[index + 1]; + let max_tail = max_suffix[index + 1]; + let byte = bytes.iter().copied().find(|byte| { + let value = *byte as u128; + value <= remaining + && remaining.saturating_sub(value) >= min_tail + && remaining.saturating_sub(value) <= max_tail + })?; + candidate.push(byte); + remaining = remaining.saturating_sub(byte as u128); + } + (remaining == 0).then_some(candidate) +} + +fn rotate_mix_preimage_for_target_with_allowed( + recurrence: &ExactLoopRecurrenceEvidence, + initial: u64, + target: u64, + allowed: &[Vec], +) -> Option> { + let spec = rotate_mix_spec(recurrence)?; + if allowed.len() != recurrence.iterations as usize || allowed.is_empty() { + return None; + } + let mut candidate = preferred_candidate_bytes(allowed)?; + let last = candidate.len() - 1; + let prefix_acc = rotate_mix_apply_prefix(initial, &candidate[..last], recurrence.bits, spec)?; + if let Some(byte) = rotate_mix_required_final_byte(prefix_acc, target, recurrence.bits, spec) + && allowed[last].contains(&byte) + { + candidate[last] = byte; + return Some(candidate); + } + if candidate.len() < 2 { + return None; + } + let penultimate = candidate.len() - 2; + let prefix_acc = + rotate_mix_apply_prefix(initial, &candidate[..penultimate], recurrence.bits, spec)?; + let last_allowed = allowed[last].iter().copied().collect::>(); + for &byte in &allowed[penultimate] { + let after_penultimate = rotate_mix_apply_step(prefix_acc, byte, recurrence.bits, spec); + let Some(last_byte) = + rotate_mix_required_final_byte(after_penultimate, target, recurrence.bits, spec) + else { + continue; + }; + if last_allowed.contains(&last_byte) { + candidate[penultimate] = byte; + candidate[last] = last_byte; + return Some(candidate); + } + } + None +} + +fn mitm_preimage_for_target_with_allowed( + fold: &ExactLoopFoldEvidence, + target: u64, + allowed: &[Vec], + config: &SolveTacticConfig, +) -> Option> { + if allowed.len() < 2 || config.max_mitm_table == 0 { + return None; + } + match fold.operation { + LoopFoldOperation::Xor => { + xor_mitm_preimage_for_target_with_allowed(allowed, target, config.max_mitm_table) + } + LoopFoldOperation::Add => { + add_mitm_preimage_for_target_with_allowed(allowed, target, fold.bits, config) + } + } +} + +fn rotate_mix_mitm_preimage_for_target_with_allowed( + recurrence: &ExactLoopRecurrenceEvidence, + initial: u64, + target: u64, + allowed: &[Vec], + config: &SolveTacticConfig, +) -> Option> { + if allowed.len() < 2 || config.max_mitm_table == 0 { + return None; + } + let spec = rotate_mix_spec(recurrence)?; + let split = allowed.len() / 2; + let (left, right) = allowed.split_at(split); + if left.is_empty() || right.is_empty() { + return None; + } + let left_table = + build_rotate_mix_half_table(left, initial, recurrence.bits, spec, config.max_mitm_table)?; + search_rotate_mix_half_table(right, target, recurrence.bits, spec, &left_table) +} + +fn xor_mitm_preimage_for_target_with_allowed( + allowed: &[Vec], + target: u64, + max_table: usize, +) -> Option> { + let split = allowed.len() / 2; + let (left, right) = allowed.split_at(split); + if left.is_empty() || right.is_empty() { + return None; + } + let left_table = build_xor_half_table(left, max_table)?; + search_xor_half_table(right, target as u8, &left_table) +} + +fn build_xor_half_table(allowed: &[Vec], max_table: usize) -> Option>> { + fn recurse( + allowed: &[Vec], + index: usize, + aggregate: u8, + current: &mut Vec, + table: &mut BTreeMap>, + max_table: usize, + ) -> bool { + if index == allowed.len() { + table.entry(aggregate).or_insert_with(|| current.clone()); + return table.len() <= max_table; + } + for &byte in &allowed[index] { + current.push(byte); + if !recurse( + allowed, + index + 1, + aggregate ^ byte, + current, + table, + max_table, + ) { + current.pop(); + return false; + } + current.pop(); + } + true + } + + let mut table = BTreeMap::new(); + let mut current = Vec::with_capacity(allowed.len()); + recurse(allowed, 0, 0, &mut current, &mut table, max_table).then_some(table) +} + +fn search_xor_half_table( + allowed: &[Vec], + target: u8, + left_table: &BTreeMap>, +) -> Option> { + fn recurse( + allowed: &[Vec], + index: usize, + aggregate: u8, + target: u8, + current: &mut Vec, + left_table: &BTreeMap>, + ) -> Option> { + if index == allowed.len() { + let needed = target ^ aggregate; + let mut candidate = left_table.get(&needed)?.clone(); + candidate.extend_from_slice(current); + return Some(candidate); + } + for &byte in &allowed[index] { + current.push(byte); + if let Some(candidate) = recurse( + allowed, + index + 1, + aggregate ^ byte, + target, + current, + left_table, + ) { + return Some(candidate); + } + current.pop(); + } + None + } + + let mut current = Vec::with_capacity(allowed.len()); + recurse(allowed, 0, 0, target, &mut current, left_table) +} + +fn add_mitm_preimage_for_target_with_allowed( + allowed: &[Vec], + target: u64, + bits: u32, + config: &SolveTacticConfig, +) -> Option> { + let split = allowed.len() / 2; + let (left, right) = allowed.split_at(split); + if left.is_empty() || right.is_empty() { + return None; + } + let modulus = add_fold_modulus(bits); + let target = modulus.map_or(target as u128, |modulus| (target as u128) % modulus); + let left_table = build_add_half_table(left, modulus, config.max_mitm_table)?; + search_add_half_table(right, target, modulus, &left_table) +} + +fn add_fold_modulus(bits: u32) -> Option { + if bits >= 64 { + None + } else { + Some(1u128 << bits.max(1)) + } +} + +fn add_fold_step(aggregate: u128, byte: u8, modulus: Option) -> u128 { + let next = aggregate + byte as u128; + modulus.map_or(next, |modulus| next % modulus) +} + +fn build_add_half_table( + allowed: &[Vec], + modulus: Option, + max_table: usize, +) -> Option>> { + fn recurse( + allowed: &[Vec], + index: usize, + aggregate: u128, + modulus: Option, + current: &mut Vec, + table: &mut BTreeMap>, + max_table: usize, + ) -> bool { + if index == allowed.len() { + table.entry(aggregate).or_insert_with(|| current.clone()); + return table.len() <= max_table; + } + for &byte in &allowed[index] { + current.push(byte); + let next = add_fold_step(aggregate, byte, modulus); + if !recurse(allowed, index + 1, next, modulus, current, table, max_table) { + current.pop(); + return false; + } + current.pop(); + } + true + } + + let mut table = BTreeMap::new(); + let mut current = Vec::with_capacity(allowed.len()); + recurse(allowed, 0, 0, modulus, &mut current, &mut table, max_table).then_some(table) +} + +fn search_add_half_table( + allowed: &[Vec], + target: u128, + modulus: Option, + left_table: &BTreeMap>, +) -> Option> { + fn recurse( + allowed: &[Vec], + index: usize, + aggregate: u128, + target: u128, + modulus: Option, + current: &mut Vec, + left_table: &BTreeMap>, + ) -> Option> { + if index == allowed.len() { + let needed = match modulus { + Some(modulus) => (target + modulus - (aggregate % modulus)) % modulus, + None if aggregate <= target => target - aggregate, + None => return None, + }; + let mut candidate = left_table.get(&needed)?.clone(); + candidate.extend_from_slice(current); + return Some(candidate); + } + for &byte in &allowed[index] { + current.push(byte); + let next = add_fold_step(aggregate, byte, modulus); + if let Some(candidate) = recurse( + allowed, + index + 1, + next, + target, + modulus, + current, + left_table, + ) { + return Some(candidate); + } + current.pop(); + } + None + } + + let mut current = Vec::with_capacity(allowed.len()); + recurse(allowed, 0, 0, target, modulus, &mut current, left_table) +} + +#[derive(Clone, Copy)] +struct RotateMixSpec { + direction: LoopRotateDirection, + amount: u32, + operation: LoopFoldOperation, +} + +fn rotate_mix_spec(recurrence: &ExactLoopRecurrenceEvidence) -> Option { + let ExactLoopRecurrenceKind::RotateMix { + direction, + amount, + operation, + .. + } = &recurrence.kind + else { + return None; + }; + Some(RotateMixSpec { + direction: *direction, + amount: *amount, + operation: *operation, + }) +} + +fn rotate_mix_apply_bytes( + recurrence: &ExactLoopRecurrenceEvidence, + initial: u64, + bytes: &[u8], +) -> Option { + let spec = rotate_mix_spec(recurrence)?; + rotate_mix_apply_prefix(initial, bytes, recurrence.bits, spec) +} + +fn rotate_mix_apply_prefix( + initial: u64, + bytes: &[u8], + bits: u32, + spec: RotateMixSpec, +) -> Option { + let mut acc = mask_to_bits(initial, bits); + for &byte in bytes { + acc = rotate_mix_apply_step(acc, byte, bits, spec); + } + Some(acc) +} + +fn rotate_mix_apply_step(acc: u64, byte: u8, bits: u32, spec: RotateMixSpec) -> u64 { + let rotated = rotate_bits(acc, spec.amount, bits, spec.direction); + let byte = u64::from(byte); + match spec.operation { + LoopFoldOperation::Xor => mask_to_bits(rotated ^ byte, bits), + LoopFoldOperation::Add => mask_to_bits(rotated.wrapping_add(byte), bits), + } +} + +fn rotate_mix_required_final_byte( + acc_before_final: u64, + target: u64, + bits: u32, + spec: RotateMixSpec, +) -> Option { + let rotated = rotate_bits(acc_before_final, spec.amount, bits, spec.direction); + let needed = match spec.operation { + LoopFoldOperation::Xor => mask_to_bits(target ^ rotated, bits), + LoopFoldOperation::Add => mask_to_bits(target.wrapping_sub(rotated), bits), + }; + (needed <= u8::MAX as u64).then_some(needed as u8) +} + +fn rotate_mix_reverse_step(acc_after: u64, byte: u8, bits: u32, spec: RotateMixSpec) -> u64 { + let unrotated = match spec.operation { + LoopFoldOperation::Xor => mask_to_bits(acc_after ^ u64::from(byte), bits), + LoopFoldOperation::Add => mask_to_bits(acc_after.wrapping_sub(u64::from(byte)), bits), + }; + rotate_bits( + unrotated, + spec.amount, + bits, + match spec.direction { + LoopRotateDirection::Left => LoopRotateDirection::Right, + LoopRotateDirection::Right => LoopRotateDirection::Left, + }, + ) +} + +fn build_rotate_mix_half_table( + allowed: &[Vec], + initial: u64, + bits: u32, + spec: RotateMixSpec, + max_table: usize, +) -> Option>> { + struct Env<'a> { + allowed: &'a [Vec], + bits: u32, + spec: RotateMixSpec, + max_table: usize, + } + + fn recurse( + env: &Env<'_>, + index: usize, + acc: u64, + current: &mut Vec, + table: &mut BTreeMap>, + ) -> bool { + if index == env.allowed.len() { + table.entry(acc).or_insert_with(|| current.clone()); + return table.len() <= env.max_table; + } + for &byte in &env.allowed[index] { + current.push(byte); + let next = rotate_mix_apply_step(acc, byte, env.bits, env.spec); + if !recurse(env, index + 1, next, current, table) { + current.pop(); + return false; + } + current.pop(); + } + true + } + + let env = Env { + allowed, + bits, + spec, + max_table, + }; + let mut table = BTreeMap::new(); + let mut current = Vec::with_capacity(allowed.len()); + recurse( + &env, + 0, + mask_to_bits(initial, bits), + &mut current, + &mut table, + ) + .then_some(table) +} + +fn search_rotate_mix_half_table( + allowed: &[Vec], + target: u64, + bits: u32, + spec: RotateMixSpec, + left_table: &BTreeMap>, +) -> Option> { + fn recurse( + allowed: &[Vec], + index: usize, + acc_after: u64, + bits: u32, + spec: RotateMixSpec, + current_rev: &mut Vec, + left_table: &BTreeMap>, + ) -> Option> { + if index == 0 { + let mut candidate = left_table.get(&acc_after)?.clone(); + current_rev.reverse(); + candidate.extend_from_slice(current_rev); + current_rev.reverse(); + return Some(candidate); + } + let choices = &allowed[index - 1]; + for &byte in choices { + current_rev.push(byte); + let prev = rotate_mix_reverse_step(acc_after, byte, bits, spec); + if let Some(candidate) = recurse( + allowed, + index - 1, + prev, + bits, + spec, + current_rev, + left_table, + ) { + return Some(candidate); + } + current_rev.pop(); + } + None + } + + let mut current_rev = Vec::with_capacity(allowed.len()); + recurse( + allowed, + allowed.len(), + mask_to_bits(target, bits), + bits, + spec, + &mut current_rev, + left_table, + ) +} + +fn rotate_bits(value: u64, amount: u32, bits: u32, direction: LoopRotateDirection) -> u64 { + let bits = bits.clamp(1, 64); + let value = mask_to_bits(value, bits); + let amount = normalize_rotate_amount(amount, bits); + if amount == 0 { + return value; + } + if bits == 64 { + return match direction { + LoopRotateDirection::Left => value.rotate_left(amount), + LoopRotateDirection::Right => value.rotate_right(amount), + }; + } + let lhs = match direction { + LoopRotateDirection::Left => value.wrapping_shl(amount), + LoopRotateDirection::Right => value >> amount, + }; + let rhs = match direction { + LoopRotateDirection::Left => value >> (bits - amount), + LoopRotateDirection::Right => value.wrapping_shl(bits - amount), + }; + mask_to_bits(lhs | rhs, bits) +} + +fn normalize_rotate_amount(amount: u32, bits: u32) -> u32 { + if bits == 0 { 0 } else { amount % bits.min(64) } +} + +fn mask_to_bits(value: u64, bits: u32) -> u64 { + if bits >= 64 { + value + } else if bits == 0 { + 0 + } else { + value & ((1u64 << bits) - 1) + } +} + +fn preferred_byte(allowed: &[u8]) -> u8 { + for preferred in [b'A', b'a', b'0', b' '] { + if allowed.binary_search(&preferred).is_ok() { + return preferred; + } + } + allowed[0] +} + +fn constrain_byte_to_domain<'ctx>( + state: &mut SymState<'ctx>, + value: &SymValue<'ctx>, + domain: &InputByteDomain, +) { + let ranges = domain.ranges(); + if ranges.len() == 1 && ranges[0] == (0, u8::MAX) { + return; + } + let ctx = state.context(); + let byte = value.extract(ctx, 7, 0).to_bv(ctx); + let mut range_predicates = Vec::new(); + for (start, end) in ranges { + if start == end { + range_predicates.push(byte.eq(BV::from_u64(start as u64, 8))); + } else { + let ge = byte.bvuge(BV::from_u64(start as u64, 8)); + let le = byte.bvule(BV::from_u64(end as u64, 8)); + range_predicates.push(ge & le); + } + } + if range_predicates.is_empty() { + return; + } + let predicate = if range_predicates.len() == 1 { + range_predicates.remove(0) + } else { + Bool::or(&range_predicates.iter().collect::>()) + }; + state.add_constraint(predicate); +} + +fn normalize_ranges(ranges: impl IntoIterator) -> Vec<(u8, u8)> { + let mut ranges = ranges + .into_iter() + .map(|(start, end)| { + if start <= end { + (start, end) + } else { + (end, start) + } + }) + .collect::>(); + ranges.sort_unstable(); + let mut out: Vec<(u8, u8)> = Vec::new(); + for (start, end) in ranges { + if let Some(last) = out.last_mut() + && start <= last.1.saturating_add(1) + { + last.1 = last.1.max(end); + continue; + } + out.push((start, end)); + } + out +} + +fn push_unique(reasons: &mut Vec, reason: impl Into) { + let reason = reason.into(); + if !reasons.iter().any(|existing| existing == &reason) { + reasons.push(reason); + } +} + +#[cfg(test)] +mod tests { + use z3::Context; + + use super::{ + InputByteDomain, SolveTacticConfig, algebraic_preimage_candidate, + algebraic_preimage_for_target, constrain_exact_fold_inputs, + constrain_exact_recurrence_candidate, recurrence_target_for_bytes, + tactic_candidates_for_constraint_graph, + }; + use crate::constraints::{ + FinalConstraint, FinalConstraintGraph, FinalConstraintPrecision, FinalConstraintSource, + RecurrenceAggregateConstraint, RecurrenceAggregateRangeConstraint, + }; + use crate::{ + ExactLoopFoldEvidence, ExactLoopRecurrenceEvidence, ExactLoopRecurrenceKind, + LoopFoldOperation, LoopMemoryTerm, LoopMemoryTermKind, LoopRotateDirection, SymSolver, + SymState, SymValue, + }; + + #[test] + fn exact_input_fold_constraints_bias_model_to_printable_bytes() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic_memory(0x7000, 2, "argv1"); + let lhs = state.mem_read(&SymValue::concrete(0x7000, 64), 1); + let rhs = state.mem_read(&SymValue::concrete(0x7001, 64), 1); + let folded = lhs.xor(&ctx, &rhs); + state.constrain_eq(&folded, 0); + + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 2, + accumulator: "RBX_2".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(2), + }, + }; + + let report = + constrain_exact_fold_inputs(&mut state, &[fold], &InputByteDomain::PrintableAscii, 16); + assert_eq!(report.constrained_bytes, 2); + assert!(report.skipped_reasons.is_empty()); + + let solver = SymSolver::new(&ctx); + let model = solver.solve(&state).expect("model"); + let bytes = model + .eval_bytes(&state.symbolic_memory()[0].value, 2) + .expect("bytes"); + assert!(bytes.iter().all(|byte| (0x20..=0x7e).contains(byte))); + assert_eq!(bytes[0] ^ bytes[1], 0); + } + + #[test] + fn xor_preimage_rewrites_nonprintable_model_to_printable_equivalent() { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 4, + accumulator: "RBX_2".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(4), + }, + }; + let model = [0x00, 0x01, 0x02, 0x03]; + let candidate = + algebraic_preimage_candidate(&fold, &model, &InputByteDomain::PrintableAscii) + .expect("candidate"); + assert_eq!(candidate.len(), model.len()); + assert!(candidate.iter().all(|byte| (0x20..=0x7e).contains(byte))); + assert_eq!( + candidate.iter().fold(0u8, |acc, byte| acc ^ byte), + model.iter().fold(0u8, |acc, byte| acc ^ byte) + ); + } + + #[test] + fn add_preimage_rewrites_sum_to_printable_equivalent() { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 3, + accumulator: "RBX_2".to_string(), + bits: 16, + operation: LoopFoldOperation::Add, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(3), + }, + }; + let model = [1, 100, 100]; + let candidate = + algebraic_preimage_candidate(&fold, &model, &InputByteDomain::PrintableAscii) + .expect("candidate"); + assert_eq!(candidate.len(), model.len()); + assert!(candidate.iter().all(|byte| (0x20..=0x7e).contains(byte))); + assert_eq!( + candidate.iter().map(|byte| *byte as u16).sum::(), + model.iter().map(|byte| *byte as u16).sum::() + ); + } + + #[test] + fn xor_preimage_for_target_solves_constraint_directly() { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 5, + accumulator: "RBX_2".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(5), + }, + }; + let candidate = + algebraic_preimage_for_target(&fold, 0x42, &InputByteDomain::PrintableAscii) + .expect("candidate"); + assert_eq!(candidate.len(), 5); + assert!(candidate.iter().all(|byte| (0x20..=0x7e).contains(byte))); + assert_eq!(candidate.iter().fold(0u8, |acc, byte| acc ^ byte), 0x42); + } + + #[test] + fn constraint_graph_scheduler_returns_bounded_candidates() { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 3, + accumulator: "RBX_2".to_string(), + bits: 16, + operation: LoopFoldOperation::Add, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(3), + }, + }; + let graph = FinalConstraintGraph { + constraints: vec![FinalConstraint::RecurrenceEquals( + RecurrenceAggregateConstraint { + recurrence: ExactLoopRecurrenceEvidence::from(fold), + target: 201, + bits: 16, + source: FinalConstraintSource::ExactRecurrenceAggregateModel, + precision: FinalConstraintPrecision::ModelConditioned, + reasons: Vec::new(), + }, + )], + input_byte_constraints: Vec::new(), + input_length_constraints: Vec::new(), + refusals: Vec::new(), + }; + let config = SolveTacticConfig { + max_candidates: 1, + ..SolveTacticConfig::default() + }; + let candidates = tactic_candidates_for_constraint_graph(&graph, None, &config); + assert_eq!(candidates.len(), 1); + assert_eq!( + candidates[0] + .bytes + .iter() + .map(|byte| *byte as u16) + .sum::(), + 201 + ); + } + + #[test] + fn range_constraint_scheduler_honors_input_byte_constraints() { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 3, + accumulator: "RBX_2".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(3), + }, + }; + let graph = FinalConstraintGraph { + constraints: vec![FinalConstraint::RecurrenceRange( + RecurrenceAggregateRangeConstraint { + recurrence: ExactLoopRecurrenceEvidence::from(fold.clone()), + min: 0x40, + max: 0x4f, + bits: 8, + source: FinalConstraintSource::TerminalCompareExact, + precision: FinalConstraintPrecision::Exact, + reasons: Vec::new(), + }, + )], + input_byte_constraints: vec![crate::InputByteConstraint { + addr: 0x7000, + allowed: vec![b'K'], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }], + input_length_constraints: vec![crate::InputLengthConstraint { + base_addr: 0x7000, + len: 3, + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }], + refusals: Vec::new(), + }; + let candidates = + tactic_candidates_for_constraint_graph(&graph, None, &SolveTacticConfig::default()); + assert!(!candidates.is_empty()); + assert_eq!(candidates[0].bytes[0], b'K'); + let folded = candidates[0].bytes.iter().fold(0u8, |acc, byte| acc ^ byte); + assert!((0x40..=0x4f).contains(&folded)); + } + + #[test] + fn mitm_scheduler_solves_xor_constraint_when_pairwise_algebraic_fails() { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 4, + accumulator: "RBX_2".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(4), + }, + }; + let graph = FinalConstraintGraph { + constraints: vec![FinalConstraint::RecurrenceEquals( + RecurrenceAggregateConstraint { + recurrence: ExactLoopRecurrenceEvidence::from(fold), + target: 0, + bits: 8, + source: FinalConstraintSource::TerminalCompareExact, + precision: FinalConstraintPrecision::Exact, + reasons: Vec::new(), + }, + )], + input_byte_constraints: vec![ + crate::InputByteConstraint { + addr: 0x7000, + allowed: vec![1, 2], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + crate::InputByteConstraint { + addr: 0x7001, + allowed: vec![3], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + crate::InputByteConstraint { + addr: 0x7002, + allowed: vec![4], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + crate::InputByteConstraint { + addr: 0x7003, + allowed: vec![5], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + ], + input_length_constraints: vec![crate::InputLengthConstraint { + base_addr: 0x7000, + len: 4, + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }], + refusals: Vec::new(), + }; + + let candidates = + tactic_candidates_for_constraint_graph(&graph, None, &SolveTacticConfig::default()); + assert!(!candidates.is_empty()); + assert!(candidates[0].used_mitm); + assert_eq!(candidates[0].bytes, vec![2, 3, 4, 5]); + assert_eq!( + candidates[0].bytes.iter().fold(0u8, |acc, byte| acc ^ byte), + 0 + ); + } + + #[test] + fn mitm_scheduler_solves_add_constraint_when_greedy_algebraic_fails() { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 3, + accumulator: "RBX_2".to_string(), + bits: 16, + operation: LoopFoldOperation::Add, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(3), + }, + }; + let graph = FinalConstraintGraph { + constraints: vec![FinalConstraint::RecurrenceEquals( + RecurrenceAggregateConstraint { + recurrence: ExactLoopRecurrenceEvidence::from(fold), + target: 7, + bits: 16, + source: FinalConstraintSource::TerminalCompareExact, + precision: FinalConstraintPrecision::Exact, + reasons: Vec::new(), + }, + )], + input_byte_constraints: vec![ + crate::InputByteConstraint { + addr: 0x7000, + allowed: vec![1, 2], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + crate::InputByteConstraint { + addr: 0x7001, + allowed: vec![1, 4], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + crate::InputByteConstraint { + addr: 0x7002, + allowed: vec![1, 4], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + ], + input_length_constraints: vec![crate::InputLengthConstraint { + base_addr: 0x7000, + len: 3, + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }], + refusals: Vec::new(), + }; + + let candidates = + tactic_candidates_for_constraint_graph(&graph, None, &SolveTacticConfig::default()); + assert!(!candidates.is_empty()); + assert!(candidates[0].used_mitm); + assert_eq!(candidates[0].bytes, vec![2, 1, 4]); + assert_eq!( + candidates[0] + .bytes + .iter() + .map(|byte| *byte as u16) + .sum::(), + 7 + ); + } + + #[test] + fn rotate_recurrence_constraint_produces_candidate_with_concrete_initial() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_register("RBX_1", SymValue::concrete(0xa5, 8)); + let recurrence = ExactLoopRecurrenceEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 3, + accumulator: "RBX_2".to_string(), + initial: "RBX_1".to_string(), + bits: 8, + kind: ExactLoopRecurrenceKind::RotateMix { + direction: LoopRotateDirection::Left, + amount: 3, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(3), + }, + }, + }; + let expected = vec![0x12, 0x34, 0x56]; + let target = + recurrence_target_for_bytes(&recurrence, Some(&state), &expected).expect("target"); + let graph = FinalConstraintGraph { + constraints: vec![FinalConstraint::RecurrenceEquals( + RecurrenceAggregateConstraint { + recurrence: recurrence.clone(), + target, + bits: 8, + source: FinalConstraintSource::TerminalCompareExact, + precision: FinalConstraintPrecision::Exact, + reasons: Vec::new(), + }, + )], + input_byte_constraints: vec![ + crate::InputByteConstraint { + addr: 0x7000, + allowed: vec![0x12, 0x13], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + crate::InputByteConstraint { + addr: 0x7001, + allowed: vec![0x34, 0x35], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + crate::InputByteConstraint { + addr: 0x7002, + allowed: vec![0x56, 0x57], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + ], + input_length_constraints: vec![crate::InputLengthConstraint { + base_addr: 0x7000, + len: 3, + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }], + refusals: Vec::new(), + }; + + let candidates = tactic_candidates_for_constraint_graph( + &graph, + Some(&state), + &SolveTacticConfig::default(), + ); + assert!(!candidates.is_empty()); + assert_eq!(candidates[0].recurrence, recurrence); + assert_eq!(candidates[0].bytes, expected); + } + + #[test] + fn exact_rotate_recurrence_candidate_constrains_symbolic_input_bytes() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic_memory(0x7000, 3, "argv1"); + let recurrence = ExactLoopRecurrenceEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 3, + accumulator: "RBX_2".to_string(), + initial: "RBX_1".to_string(), + bits: 8, + kind: ExactLoopRecurrenceKind::RotateMix { + direction: LoopRotateDirection::Left, + amount: 3, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(3), + }, + }, + }; + let expected = [0x12, 0x34, 0x56]; + let report = constrain_exact_recurrence_candidate(&mut state, &recurrence, &expected); + assert_eq!(report.constrained_bytes, 3); + assert!(report.skipped_reasons.is_empty()); + + let solver = SymSolver::new(&ctx); + let model = solver.solve(&state).expect("model"); + let bytes = model + .eval_bytes(&state.symbolic_memory()[0].value, 3) + .expect("bytes"); + assert_eq!(bytes, expected); + } +} diff --git a/crates/r2sym/src/value.rs b/crates/r2sym/src/value.rs index f466c76..140ba60 100644 --- a/crates/r2sym/src/value.rs +++ b/crates/r2sym/src/value.rs @@ -9,7 +9,8 @@ use std::fmt; use std::marker::PhantomData; use z3::Context; -use z3::ast::BV; +use z3::DeclKind; +use z3::ast::{Ast, BV, Dynamic}; /// A symbolic value that can be concrete, symbolic, or unknown. /// @@ -276,6 +277,57 @@ impl<'ctx> SymValue<'ctx> { } } + pub(crate) fn symbolic_key(&self) -> Option { + match self { + Self::Symbolic { ast, bits, .. } => Some(format!("{bits}:{}", ast)), + _ => None, + } + } + + fn is_concrete_value(&self, expected: u64) -> bool { + matches!(self, Self::Concrete { value, .. } if *value == expected) + } + + fn mask_for_bits(bits: u32) -> u64 { + if bits >= 64 { + u64::MAX + } else { + (1u64 << bits) - 1 + } + } + + fn concrete_logical_shift_left(value: u64, bits: u32, amount: u64) -> u64 { + if bits == 0 || amount >= u64::from(bits) { + return 0; + } + value.wrapping_shl(amount as u32) & Self::mask_for_bits(bits) + } + + fn concrete_logical_shift_right(value: u64, bits: u32, amount: u64) -> u64 { + if bits == 0 || amount >= u64::from(bits) { + return 0; + } + value >> (amount as u32) + } + + fn is_all_ones_for(&self, bits: u32) -> bool { + match self { + Self::Concrete { + value, + bits: value_bits, + .. + } => *value_bits == bits && *value == Self::mask_for_bits(bits), + _ => false, + } + } + + fn same_symbolic_ast(&self, other: &Self) -> bool { + match (self, other) { + (Self::Symbolic { ast: a, .. }, Self::Symbolic { ast: b, .. }) => a.ast_eq(b), + _ => false, + } + } + /// Convert to a Z3 bitvector (concretizing if needed). pub fn to_bv(&self, _ctx: &'ctx Context) -> BV { match self { @@ -309,7 +361,23 @@ impl<'ctx> SymValue<'ctx> { taint, .. } => { - let new_ast = ast.zero_ext(extend_by); + let new_ast = { + let dyn_ast = Dynamic::from_ast(ast); + if matches!( + dyn_ast.safe_decl().map(|decl| decl.kind()), + Ok(DeclKind::ZERO_EXT) + ) { + if let Some(child) = + dyn_ast.children().first().and_then(|child| child.as_bv()) + { + child.zero_ext(new_bits - child.get_size()) + } else { + ast.zero_ext(extend_by) + } + } else { + ast.zero_ext(extend_by) + } + }; Self::Symbolic { ast: new_ast, bits: new_bits, @@ -357,7 +425,23 @@ impl<'ctx> SymValue<'ctx> { } } Self::Symbolic { ast, .. } => { - let new_ast = ast.sign_ext(extend_by); + let new_ast = { + let dyn_ast = Dynamic::from_ast(ast); + if matches!( + dyn_ast.safe_decl().map(|decl| decl.kind()), + Ok(DeclKind::SIGN_EXT) + ) { + if let Some(child) = + dyn_ast.children().first().and_then(|child| child.as_bv()) + { + child.sign_ext(new_bits - child.get_size()) + } else { + ast.sign_ext(extend_by) + } + } else { + ast.sign_ext(extend_by) + } + }; Self::Symbolic { ast: new_ast, bits: new_bits, @@ -384,6 +468,9 @@ impl<'ctx> SymValue<'ctx> { return Self::Unknown { bits: 1, taint }; } let new_bits = clamped_high - low + 1; + if low == 0 && clamped_high + 1 == src_bits { + return self.clone(); + } match self { Self::Concrete { value, .. } => { if new_bits > 64 || clamped_high >= 64 { @@ -403,6 +490,20 @@ impl<'ctx> SymValue<'ctx> { } } Self::Symbolic { ast, .. } => { + if low == 0 { + let dyn_ast = Dynamic::from_ast(ast); + if matches!( + dyn_ast.safe_decl().map(|decl| decl.kind()), + Ok(DeclKind::ZERO_EXT | DeclKind::SIGN_EXT) + ) { + let children = dyn_ast.children(); + if let Some(child) = children.first().and_then(|child| child.as_bv()) + && child.get_size() == new_bits + { + return Self::symbolic_tainted(child, new_bits, taint); + } + } + } let new_ast = ast.extract(clamped_high, low); Self::Symbolic { ast: new_ast, @@ -480,6 +581,8 @@ impl<'ctx> SymValue<'ctx> { taint, } } + (_, rhs) if rhs.is_zero() => self.clone().with_taint(taint), + (lhs, _) if lhs.is_zero() => other.clone().with_taint(taint), _ => { let (a_bv, b_bv, result_bits) = self.normalize_widths(ctx, other); Self::Symbolic { @@ -516,6 +619,12 @@ impl<'ctx> SymValue<'ctx> { taint, } } + (_, rhs) if rhs.is_zero() => self.clone().with_taint(taint), + (lhs, rhs) if lhs.same_symbolic_ast(rhs) => Self::Concrete { + value: 0, + bits: self.bits().max(other.bits()), + taint, + }, _ => { let (a_bv, b_bv, result_bits) = self.normalize_widths(ctx, other); Self::Symbolic { @@ -552,6 +661,18 @@ impl<'ctx> SymValue<'ctx> { taint, } } + (_, rhs) if rhs.is_zero() => Self::Concrete { + value: 0, + bits: self.bits().max(other.bits()), + taint, + }, + (lhs, _) if lhs.is_zero() => Self::Concrete { + value: 0, + bits: self.bits().max(other.bits()), + taint, + }, + (_, rhs) if rhs.is_concrete_value(1) => self.clone().with_taint(taint), + (lhs, _) if lhs.is_concrete_value(1) => other.clone().with_taint(taint), _ => { let (a_bv, b_bv, result_bits) = self.normalize_widths(ctx, other); Self::Symbolic { @@ -590,6 +711,7 @@ impl<'ctx> SymValue<'ctx> { } } } + (_, rhs) if rhs.is_concrete_value(1) => self.clone().with_taint(taint), _ => { let (a_bv, b_bv, result_bits) = self.normalize_widths(ctx, other); Self::Symbolic { @@ -605,6 +727,9 @@ impl<'ctx> SymValue<'ctx> { /// Signed division. pub fn sdiv(&self, ctx: &'ctx Context, other: &Self) -> Self { let taint = self.get_taint() | other.get_taint(); + if other.is_concrete_value(1) { + return self.clone().with_taint(taint); + } let (a_bv, b_bv, result_bits) = self.normalize_widths_signed(ctx, other); Self::Symbolic { ast: a_bv.bvsdiv(&b_bv), @@ -640,6 +765,11 @@ impl<'ctx> SymValue<'ctx> { } } } + (_, rhs) if rhs.is_concrete_value(1) => Self::Concrete { + value: 0, + bits: self.bits().max(other.bits()), + taint, + }, _ => { let (a_bv, b_bv, result_bits) = self.normalize_widths(ctx, other); Self::Symbolic { @@ -655,6 +785,13 @@ impl<'ctx> SymValue<'ctx> { /// Signed remainder. pub fn srem(&self, ctx: &'ctx Context, other: &Self) -> Self { let taint = self.get_taint() | other.get_taint(); + if other.is_concrete_value(1) { + return Self::Concrete { + value: 0, + bits: self.bits().max(other.bits()), + taint, + }; + } let (a_bv, b_bv, result_bits) = self.normalize_widths_signed(ctx, other); Self::Symbolic { ast: a_bv.bvsrem(&b_bv), @@ -697,6 +834,7 @@ impl<'ctx> SymValue<'ctx> { /// Bitwise AND. pub fn and(&self, ctx: &'ctx Context, other: &Self) -> Self { let taint = self.get_taint() | other.get_taint(); + let result_bits = self.bits().max(other.bits()); match (self, other) { ( Self::Concrete { value: a, bits, .. }, @@ -710,6 +848,19 @@ impl<'ctx> SymValue<'ctx> { bits: (*bits).max(*b_bits), taint, }, + (_, rhs) if rhs.is_zero() => Self::Concrete { + value: 0, + bits: self.bits().max(other.bits()), + taint, + }, + (lhs, _) if lhs.is_zero() => Self::Concrete { + value: 0, + bits: self.bits().max(other.bits()), + taint, + }, + (_, rhs) if rhs.is_all_ones_for(result_bits) => self.clone().with_taint(taint), + (lhs, _) if lhs.is_all_ones_for(result_bits) => other.clone().with_taint(taint), + (lhs, rhs) if lhs.same_symbolic_ast(rhs) => self.clone().with_taint(taint), _ => { let (a_bv, b_bv, result_bits) = self.normalize_widths(ctx, other); Self::Symbolic { @@ -725,6 +876,7 @@ impl<'ctx> SymValue<'ctx> { /// Bitwise OR. pub fn or(&self, ctx: &'ctx Context, other: &Self) -> Self { let taint = self.get_taint() | other.get_taint(); + let result_bits = self.bits().max(other.bits()); match (self, other) { ( Self::Concrete { value: a, bits, .. }, @@ -738,6 +890,19 @@ impl<'ctx> SymValue<'ctx> { bits: (*bits).max(*b_bits), taint, }, + (_, rhs) if rhs.is_zero() => self.clone().with_taint(taint), + (lhs, _) if lhs.is_zero() => other.clone().with_taint(taint), + (_, rhs) if rhs.is_all_ones_for(result_bits) => Self::Concrete { + value: Self::mask_for_bits(result_bits), + bits: result_bits, + taint, + }, + (lhs, _) if lhs.is_all_ones_for(result_bits) => Self::Concrete { + value: Self::mask_for_bits(result_bits), + bits: result_bits, + taint, + }, + (lhs, rhs) if lhs.same_symbolic_ast(rhs) => self.clone().with_taint(taint), _ => { let (a_bv, b_bv, result_bits) = self.normalize_widths(ctx, other); Self::Symbolic { @@ -766,6 +931,13 @@ impl<'ctx> SymValue<'ctx> { bits: (*bits).max(*b_bits), taint, }, + (_, rhs) if rhs.is_zero() => self.clone().with_taint(taint), + (lhs, _) if lhs.is_zero() => other.clone().with_taint(taint), + (lhs, rhs) if lhs.same_symbolic_ast(rhs) => Self::Concrete { + value: 0, + bits: self.bits().max(other.bits()), + taint, + }, _ => { let (a_bv, b_bv, result_bits) = self.normalize_widths(ctx, other); Self::Symbolic { @@ -812,14 +984,10 @@ impl<'ctx> SymValue<'ctx> { pub fn shl(&self, ctx: &'ctx Context, amount: &Self) -> Self { let taint = self.get_taint() | amount.get_taint(); match (self, amount) { + (_, amt) if amt.is_zero() => self.clone().with_taint(taint), (Self::Concrete { value, bits, .. }, Self::Concrete { value: amt, .. }) => { - let mask = if *bits >= 64 { - u64::MAX - } else { - (1u64 << *bits) - 1 - }; Self::Concrete { - value: (*value << (*amt as u32)) & mask, + value: Self::concrete_logical_shift_left(*value, *bits, *amt), bits: *bits, taint, } @@ -841,9 +1009,10 @@ impl<'ctx> SymValue<'ctx> { pub fn lshr(&self, ctx: &'ctx Context, amount: &Self) -> Self { let taint = self.get_taint() | amount.get_taint(); match (self, amount) { + (_, amt) if amt.is_zero() => self.clone().with_taint(taint), (Self::Concrete { value, bits, .. }, Self::Concrete { value: amt, .. }) => { Self::Concrete { - value: *value >> (*amt as u32), + value: Self::concrete_logical_shift_right(*value, *bits, *amt), bits: *bits, taint, } @@ -864,6 +1033,9 @@ impl<'ctx> SymValue<'ctx> { /// Arithmetic right shift. pub fn ashr(&self, ctx: &'ctx Context, amount: &Self) -> Self { let taint = self.get_taint() | amount.get_taint(); + if amount.is_zero() { + return self.clone().with_taint(taint); + } let a_bv = self.to_bv(ctx); let b_bv = self.normalize_shift_amount(ctx, amount); Self::Symbolic { @@ -885,6 +1057,11 @@ impl<'ctx> SymValue<'ctx> { bits: 1, taint, }, + (lhs, rhs) if lhs.same_symbolic_ast(rhs) => Self::Concrete { + value: 1, + bits: 1, + taint, + }, _ => { let (a_bv, b_bv, _) = self.normalize_widths(ctx, other); let cond = a_bv.eq(&b_bv); @@ -909,6 +1086,11 @@ impl<'ctx> SymValue<'ctx> { bits: 1, taint, }, + (lhs, rhs) if lhs.same_symbolic_ast(rhs) => Self::Concrete { + value: 0, + bits: 1, + taint, + }, _ => { let (a_bv, b_bv, _) = self.normalize_widths(ctx, other); let cond = a_bv.bvult(&b_bv); @@ -933,6 +1115,11 @@ impl<'ctx> SymValue<'ctx> { bits: 1, taint, }, + (lhs, rhs) if lhs.same_symbolic_ast(rhs) => Self::Concrete { + value: 1, + bits: 1, + taint, + }, _ => { let (a_bv, b_bv, _) = self.normalize_widths(ctx, other); let cond = a_bv.bvule(&b_bv); @@ -951,6 +1138,13 @@ impl<'ctx> SymValue<'ctx> { /// Signed less than comparison. pub fn slt(&self, ctx: &'ctx Context, other: &Self) -> Self { let taint = self.get_taint() | other.get_taint(); + if self.same_symbolic_ast(other) { + return Self::Concrete { + value: 0, + bits: 1, + taint, + }; + } let (a_bv, b_bv, _) = self.normalize_widths_signed(ctx, other); let cond = a_bv.bvslt(&b_bv); let one = BV::from_i64(1, 1); @@ -966,6 +1160,13 @@ impl<'ctx> SymValue<'ctx> { /// Signed less than or equal comparison. pub fn sle(&self, ctx: &'ctx Context, other: &Self) -> Self { let taint = self.get_taint() | other.get_taint(); + if self.same_symbolic_ast(other) { + return Self::Concrete { + value: 1, + bits: 1, + taint, + }; + } let (a_bv, b_bv, _) = self.normalize_widths_signed(ctx, other); let cond = a_bv.bvsle(&b_bv); let one = BV::from_i64(1, 1); @@ -1114,6 +1315,84 @@ mod tests { assert_eq!(sym.bits(), 64); } + #[test] + fn test_symbolic_identity_fast_paths() { + let ctx = Context::thread_local(); + + let sym = SymValue::new_symbolic(&ctx, "x", 64); + let zero = SymValue::concrete(0, 64); + let one = SymValue::concrete(1, 64); + + let add = sym.add(&ctx, &zero); + let sub = sym.sub(&ctx, &zero); + let mul = sym.mul(&ctx, &one); + let and = sym.and(&ctx, &SymValue::concrete(u64::MAX, 64)); + let or = sym.or(&ctx, &zero); + let xor = sym.xor(&ctx, &zero); + let shl = sym.shl(&ctx, &zero); + let lshr = sym.lshr(&ctx, &zero); + let ashr = sym.ashr(&ctx, &zero); + + assert!(add.same_symbolic_ast(&sym)); + assert!(sub.same_symbolic_ast(&sym)); + assert!(mul.same_symbolic_ast(&sym)); + assert!(and.same_symbolic_ast(&sym)); + assert!(or.same_symbolic_ast(&sym)); + assert!(xor.same_symbolic_ast(&sym)); + assert!(shl.same_symbolic_ast(&sym)); + assert!(lshr.same_symbolic_ast(&sym)); + assert!(ashr.same_symbolic_ast(&sym)); + } + + #[test] + fn test_symbolic_division_remainder_fast_paths() { + let ctx = Context::thread_local(); + + let sym = SymValue::new_symbolic(&ctx, "x", 64); + let one = SymValue::concrete(1, 64); + + let udiv = sym.udiv(&ctx, &one); + let sdiv = sym.sdiv(&ctx, &one); + let urem = sym.urem(&ctx, &one); + let srem = sym.srem(&ctx, &one); + + assert!(udiv.same_symbolic_ast(&sym)); + assert!(sdiv.same_symbolic_ast(&sym)); + assert_eq!(urem.as_concrete(), Some(0)); + assert_eq!(srem.as_concrete(), Some(0)); + } + + #[test] + fn test_symbolic_self_rules() { + let ctx = Context::thread_local(); + + let sym = SymValue::new_symbolic(&ctx, "x", 64); + + assert_eq!(sym.sub(&ctx, &sym).as_concrete(), Some(0)); + assert_eq!(sym.xor(&ctx, &sym).as_concrete(), Some(0)); + assert_eq!(sym.eq(&ctx, &sym).as_concrete(), Some(1)); + assert_eq!(sym.ult(&ctx, &sym).as_concrete(), Some(0)); + assert_eq!(sym.ule(&ctx, &sym).as_concrete(), Some(1)); + assert_eq!(sym.slt(&ctx, &sym).as_concrete(), Some(0)); + assert_eq!(sym.sle(&ctx, &sym).as_concrete(), Some(1)); + } + + #[test] + fn test_all_ones_fast_paths_require_full_result_width() { + let ctx = Context::thread_local(); + + let sym = SymValue::new_symbolic(&ctx, "x", 64); + let byte_ones = SymValue::concrete(0xff, 8); + + let and = sym.and(&ctx, &byte_ones); + let or = sym.or(&ctx, &byte_ones); + + assert!(and.is_symbolic()); + assert!(or.is_symbolic()); + assert!(!and.same_symbolic_ast(&sym)); + assert!(!or.same_symbolic_ast(&sym)); + } + #[test] fn test_extension() { let ctx = Context::thread_local(); @@ -1142,6 +1421,50 @@ mod tests { let high = a.extract(&ctx, 15, 8); assert_eq!(high.as_concrete(), Some(0xAB)); } + + #[test] + fn test_extract_full_width_returns_same_symbolic_ast() { + let ctx = Context::thread_local(); + let sym = SymValue::new_symbolic(&ctx, "x", 32); + let extracted = sym.extract(&ctx, 31, 0); + assert!(extracted.same_symbolic_ast(&sym)); + } + + #[test] + fn test_extract_zero_extend_low_bits_returns_original_ast() { + let ctx = Context::thread_local(); + let sym = SymValue::new_symbolic(&ctx, "x", 32); + let widened = sym.zero_extend(&ctx, 64); + let extracted = widened.extract(&ctx, 31, 0); + assert!(extracted.same_symbolic_ast(&sym)); + } + + #[test] + fn test_extract_sign_extend_low_bits_returns_original_ast() { + let ctx = Context::thread_local(); + let sym = SymValue::new_symbolic(&ctx, "x", 32); + let widened = sym.sign_extend(&ctx, 64); + let extracted = widened.extract(&ctx, 31, 0); + assert!(extracted.same_symbolic_ast(&sym)); + } + + #[test] + fn test_zero_extend_flattens_nested_extensions() { + let ctx = Context::thread_local(); + let sym = SymValue::new_symbolic(&ctx, "x", 16); + let widened = sym.zero_extend(&ctx, 32).zero_extend(&ctx, 64); + let direct = sym.zero_extend(&ctx, 64); + assert!(widened.same_symbolic_ast(&direct)); + } + + #[test] + fn test_sign_extend_flattens_nested_extensions() { + let ctx = Context::thread_local(); + let sym = SymValue::new_symbolic(&ctx, "x", 16); + let widened = sym.sign_extend(&ctx, 32).sign_extend(&ctx, 64); + let direct = sym.sign_extend(&ctx, 64); + assert!(widened.same_symbolic_ast(&direct)); + } } #[cfg(test)] @@ -1177,6 +1500,25 @@ mod bitwidth_tests { assert_eq!(result.bits(), 64); // Result keeps value's width } + #[test] + fn test_concrete_shifts_past_bitwidth_zero_fill_instead_of_panicking() { + let ctx = Context::thread_local(); + + let val64 = SymValue::concrete(0x1234_5678_9abc_def0, 64); + let shift64 = SymValue::concrete(64, 64); + let shift65 = SymValue::concrete(65, 8); + + assert_eq!(val64.shl(&ctx, &shift64).as_concrete(), Some(0)); + assert_eq!(val64.lshr(&ctx, &shift64).as_concrete(), Some(0)); + assert_eq!(val64.shl(&ctx, &shift65).as_concrete(), Some(0)); + assert_eq!(val64.lshr(&ctx, &shift65).as_concrete(), Some(0)); + + let val8 = SymValue::concrete(0b1111_0000, 8); + let shift8 = SymValue::concrete(8, 8); + assert_eq!(val8.shl(&ctx, &shift8).as_concrete(), Some(0)); + assert_eq!(val8.lshr(&ctx, &shift8).as_concrete(), Some(0)); + } + #[test] fn test_symbolic_different_bitwidths() { let ctx = Context::thread_local(); @@ -1313,4 +1655,52 @@ mod bitwidth_tests { assert_eq!(out.bits(), 1); assert!(out.is_unknown()); } + + #[test] + fn test_symbolic_identity_normalization() { + let ctx = Context::thread_local(); + let x = SymValue::new_symbolic(&ctx, "x", 32); + let zero = SymValue::concrete(0, 32); + let one = SymValue::concrete(1, 32); + + let add = x.add(&ctx, &zero); + assert!(add.is_symbolic()); + assert!(add.as_ast().unwrap().ast_eq(x.as_ast().unwrap())); + + let mul = x.mul(&ctx, &one); + assert!(mul.is_symbolic()); + assert!(mul.as_ast().unwrap().ast_eq(x.as_ast().unwrap())); + + let shl = x.shl(&ctx, &zero); + assert!(shl.is_symbolic()); + assert!(shl.as_ast().unwrap().ast_eq(x.as_ast().unwrap())); + } + + #[test] + fn test_symbolic_self_rewrite_rules() { + let ctx = Context::thread_local(); + let x = SymValue::new_symbolic(&ctx, "x", 32); + + assert_eq!(x.sub(&ctx, &x).as_concrete(), Some(0)); + assert_eq!(x.xor(&ctx, &x).as_concrete(), Some(0)); + assert_eq!(x.eq(&ctx, &x).as_concrete(), Some(1)); + assert_eq!(x.ult(&ctx, &x).as_concrete(), Some(0)); + assert_eq!(x.ule(&ctx, &x).as_concrete(), Some(1)); + } + + #[test] + fn test_all_ones_normalization_is_width_sensitive() { + let ctx = Context::thread_local(); + let x = SymValue::new_symbolic(&ctx, "x", 32); + let mask8 = SymValue::concrete(0xff, 8); + let mask32 = SymValue::concrete(u32::MAX as u64, 32); + + let narrowed = x.and(&ctx, &mask8); + assert!(narrowed.is_symbolic()); + assert!(!narrowed.as_ast().unwrap().ast_eq(x.as_ast().unwrap())); + + let widened = x.and(&ctx, &mask32); + assert!(widened.is_symbolic()); + assert!(widened.as_ast().unwrap().ast_eq(x.as_ast().unwrap())); + } } diff --git a/crates/r2sym/src/verification.rs b/crates/r2sym/src/verification.rs new file mode 100644 index 0000000..f4c25d9 --- /dev/null +++ b/crates/r2sym/src/verification.rs @@ -0,0 +1,1122 @@ +//! Canonical solve verification and replay policy. +//! +//! Query exploration can prove that a target is reachable under the current +//! semantic model. This module decides whether that reachability is strong +//! enough to report a concrete solve. + +use r2ssa::SsaArtifact; + +use crate::constraints::{FinalConstraintGraph, FinalConstraintPrecision}; +use crate::kernel::FactPrecision; +use crate::loops::{ + ExactLoopFoldEvidence, ExactLoopRecurrenceEvidence, ExactLoopRecurrenceKind, LoopFoldOperation, + LoopMemoryTermKind, exact_fold_evidence_from_recurrences, +}; +use crate::path::{ExploreStats, PathExplorer, SolvedPath, SolvedPathGeneration}; +use crate::query::QueryCompletion; +use crate::semantics::TargetQueryExecutionRoute; +use crate::{PreparedFunctionScope, SymState}; + +/// Result status for concrete target solving. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SolveStatus { + Solved, + Candidate, + ResidualReachable, + Unverified, + Unsat, + Unknown, + BudgetExhausted, +} + +/// Concrete/model validation state for a reported solve candidate. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ModelValidation { + /// Pure symbolic semantics were exact enough; no external replay is required. + NotRequired, + /// A concrete replay/oracle accepted the candidate. + Verified, + /// A concrete replay/oracle rejected the candidate. + Failed { reason: String }, + /// Validation was required by residual/runtime evidence but no backend was available. + Unavailable { reason: String }, +} + +/// Truthfulness metadata for target solving. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SolveVerification { + pub target_reached_under_model: bool, + pub candidate_solution_verified: bool, + pub residual_reasons: Vec, + pub model_validation: ModelValidation, +} + +/// Shape of the concrete candidate extracted from a path model. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SolveCandidateShape { + pub input_scalars: usize, + pub input_buffers: usize, + pub registers: usize, + pub memory_regions: usize, + pub concrete_assignments: usize, + pub final_pc: u64, + pub num_constraints: usize, + pub generation: Option, +} + +impl SolveCandidateShape { + pub fn from_solution(solution: &SolvedPath) -> Self { + let concrete_assignments = solution.inputs.len() + + solution.input_buffers.values().map(Vec::len).sum::() + + solution.memory.values().map(Vec::len).sum::(); + Self { + input_scalars: solution.inputs.len(), + input_buffers: solution.input_buffers.len(), + registers: solution.registers.len(), + memory_regions: solution.memory.len(), + concrete_assignments, + final_pc: solution.final_pc, + num_constraints: solution.num_constraints, + generation: solution.generation.clone(), + } + } + + pub fn has_replayable_assignments(&self) -> bool { + self.concrete_assignments > 0 + } +} + +/// Compact precision summary for route/query evidence. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EvidenceSummary { + pub precision: FactPrecision, + pub reasons: Vec, + pub requires_replay: bool, + pub final_constraint_precision: FinalConstraintPrecision, + pub exact_final_constraints: usize, + pub model_conditioned_final_constraints: usize, + pub exact_loop_recurrences: Vec, + pub exact_loop_folds: Vec, + pub tactics: Vec, +} + +impl EvidenceSummary { + pub fn exact() -> Self { + Self { + precision: FactPrecision::Exact, + reasons: Vec::new(), + requires_replay: false, + final_constraint_precision: FinalConstraintPrecision::Unknown, + exact_final_constraints: 0, + model_conditioned_final_constraints: 0, + exact_loop_recurrences: Vec::new(), + exact_loop_folds: Vec::new(), + tactics: Vec::new(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SolveTacticKind { + XorFoldPreimage, + AddFoldPreimage, + RotateXorRecurrence, + RotateAddRecurrence, + ConcreteTableFold, + RuntimeBlobFold, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SolveTacticStatus { + Available, + EvidenceOnly, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SolveTacticEvidence { + pub kind: SolveTacticKind, + pub status: SolveTacticStatus, + pub reason: String, + pub recurrence: ExactLoopRecurrenceEvidence, +} + +/// Replay requirement derived from route precision and runtime evidence. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum VerificationRequirement { + NotRequired, + Required { reasons: Vec }, + Refused { reasons: Vec }, +} + +/// Canonical witness for a target solve result. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SolveWitness { + pub target_addr: u64, + pub selected_path_index: Option, + pub final_pc: Option, + pub candidate: Option, + pub evidence: EvidenceSummary, + pub verification_requirement: VerificationRequirement, + pub verification: SolveVerification, +} + +impl SolveWitness { + pub fn is_proven(&self) -> bool { + self.verification.target_reached_under_model + && (self.verification.candidate_solution_verified + || (self + .candidate + .as_ref() + .is_some_and(|candidate| !candidate.has_replayable_assignments()) + && self.evidence.reasons.is_empty() + && matches!( + self.verification.model_validation, + ModelValidation::NotRequired + ))) + } +} + +/// Input required to replay a candidate model against concrete/lifted semantics. +pub struct CandidateReplayRequest<'a, 'ctx> { + pub func: &'a SsaArtifact, + pub scope: Option<&'a PreparedFunctionScope>, + pub initial_state: SymState<'ctx>, + pub target_addr: u64, + pub solution: &'a SolvedPath, +} + +/// Backend-neutral candidate replay interface. +pub trait CandidateReplayBackend<'ctx> { + fn replay_candidate<'a>( + &mut self, + request: CandidateReplayRequest<'a, 'ctx>, + ) -> ModelValidation; +} + +/// Lifted-semantics replay backend used as the default verifier. +pub struct LiftedReplayBackend<'a, 'ctx> { + explorer: &'a mut PathExplorer<'ctx>, +} + +impl<'a, 'ctx> LiftedReplayBackend<'a, 'ctx> { + pub fn new(explorer: &'a mut PathExplorer<'ctx>) -> Self { + Self { explorer } + } +} + +impl<'ctx> CandidateReplayBackend<'ctx> for LiftedReplayBackend<'_, 'ctx> { + fn replay_candidate<'a>( + &mut self, + request: CandidateReplayRequest<'a, 'ctx>, + ) -> ModelValidation { + validate_solution_with_lifted_semantics( + self.explorer, + request.func, + request.scope, + request.initial_state, + request.target_addr, + request.solution, + ) + } +} + +/// Explicit no-backend verifier for tests and future external integrations. +#[derive(Debug, Clone)] +pub struct UnavailableReplayBackend { + reason: String, +} + +impl UnavailableReplayBackend { + pub fn new(reason: impl Into) -> Self { + Self { + reason: reason.into(), + } + } +} + +impl<'ctx> CandidateReplayBackend<'ctx> for UnavailableReplayBackend { + fn replay_candidate<'a>( + &mut self, + _request: CandidateReplayRequest<'a, 'ctx>, + ) -> ModelValidation { + ModelValidation::Unavailable { + reason: self.reason.clone(), + } + } +} + +/// Full verifier input for a target solve result. +pub struct SolveVerificationRequest<'a, 'ctx> { + pub func: &'a SsaArtifact, + pub scope: Option<&'a PreparedFunctionScope>, + pub selected_route: &'a crate::TargetQueryRoutePlan, + pub stats: &'a ExploreStats, + pub validation_initial_state: SymState<'ctx>, + pub target_addr: u64, + pub exact_unsat: bool, + pub selected_path_index: Option, + pub solution: Option<&'a SolvedPath>, + pub constraint_graph: &'a FinalConstraintGraph, +} + +/// Verify the target solve result and decide its final user-visible status. +pub fn verify_solve_result<'a, 'ctx, B>( + request: SolveVerificationRequest<'a, 'ctx>, + replay_backend: &mut B, +) -> (SolveStatus, SolveVerification, SolveWitness) +where + B: CandidateReplayBackend<'ctx>, +{ + let residual_reasons = solve_residual_reasons(request.selected_route, request.stats); + let evidence = evidence_summary_for_route_and_stats( + request.selected_route, + request.stats, + Some(request.constraint_graph), + ); + let verification_requirement = verification_requirement_for_route_and_stats( + request.selected_route, + request.stats, + Some(request.constraint_graph), + ); + let candidate = request.solution.map(SolveCandidateShape::from_solution); + let final_pc = request.solution.map(|solution| solution.final_pc); + + let model_validation = if let Some(solution) = request.solution { + let candidate_has_replayable_assignments = candidate + .as_ref() + .is_some_and(SolveCandidateShape::has_replayable_assignments); + if candidate_has_replayable_assignments { + replay_backend.replay_candidate(CandidateReplayRequest { + func: request.func, + scope: request.scope, + initial_state: request.validation_initial_state, + target_addr: request.target_addr, + solution, + }) + } else if residual_reasons.is_empty() { + ModelValidation::NotRequired + } else { + ModelValidation::Unavailable { + reason: "residual/runtime evidence produced no concrete replay candidate" + .to_string(), + } + } + } else if residual_reasons.is_empty() { + ModelValidation::NotRequired + } else { + ModelValidation::Unavailable { + reason: "residual/runtime evidence requires concrete replay validation".to_string(), + } + }; + + let verification = solve_verification( + request.selected_path_index.is_some(), + request.solution.is_some(), + residual_reasons, + model_validation, + ); + let status = classify_solve_status( + request.exact_unsat, + request.selected_path_index, + request.solution.is_some(), + completion_from_stats(request.stats), + &verification, + request.constraint_graph, + ); + let witness = SolveWitness { + target_addr: request.target_addr, + selected_path_index: request.selected_path_index, + final_pc, + candidate, + evidence, + verification_requirement, + verification: verification.clone(), + }; + (status, verification, witness) +} + +pub fn solution_extraction_allowed( + route: &crate::TargetQueryRoutePlan, + stats: &ExploreStats, +) -> bool { + let residual_reasons = solve_residual_reasons(route, stats); + !residual_summary_blocks_solution_extraction(stats, &residual_reasons) +} + +pub fn evidence_summary_for_route_and_stats( + route: &crate::TargetQueryRoutePlan, + stats: &ExploreStats, + constraint_graph: Option<&FinalConstraintGraph>, +) -> EvidenceSummary { + let mut reasons = solve_residual_reasons(route, stats); + if let Some(graph) = constraint_graph { + for reason in &graph.refusals { + push_unique_reason(&mut reasons, reason.clone()); + } + if graph.constraints.is_empty() { + push_unique_reason(&mut reasons, "final_constraint_missing"); + } else if graph.exact_constraint_count() == 0 + && graph.model_conditioned_constraint_count() > 0 + { + push_unique_reason(&mut reasons, "final_constraint_model_conditioned"); + } + } + let exact_loop_folds = + exact_fold_evidence_from_recurrences(&stats.runtime_loop_exact_recurrences); + let exact_loop_recurrences = stats.runtime_loop_exact_recurrences.clone(); + let tactics = solve_tactics_for_exact_recurrences(&stats.runtime_loop_exact_recurrences); + let final_constraint_precision = constraint_graph + .map(FinalConstraintGraph::strongest_precision) + .unwrap_or(FinalConstraintPrecision::Unknown); + let exact_final_constraints = constraint_graph + .map(FinalConstraintGraph::exact_constraint_count) + .unwrap_or(0); + let model_conditioned_final_constraints = constraint_graph + .map(FinalConstraintGraph::model_conditioned_constraint_count) + .unwrap_or(0); + if reasons.is_empty() { + return EvidenceSummary { + precision: FactPrecision::Exact, + reasons, + requires_replay: false, + final_constraint_precision, + exact_final_constraints, + model_conditioned_final_constraints, + exact_loop_recurrences, + exact_loop_folds, + tactics, + }; + } + EvidenceSummary { + precision: FactPrecision::Residual, + requires_replay: true, + reasons, + final_constraint_precision, + exact_final_constraints, + model_conditioned_final_constraints, + exact_loop_recurrences, + exact_loop_folds, + tactics, + } +} + +pub fn solve_tactics_for_exact_recurrences( + recurrences: &[ExactLoopRecurrenceEvidence], +) -> Vec { + let mut tactics = recurrences + .iter() + .filter_map(|recurrence| match &recurrence.kind { + ExactLoopRecurrenceKind::Fold { operation, term } => { + tactic_for_fold_recurrence(recurrence, *operation, term) + } + ExactLoopRecurrenceKind::RotateMix { + operation, term, .. + } => Some(SolveTacticEvidence { + kind: match operation { + LoopFoldOperation::Xor => SolveTacticKind::RotateXorRecurrence, + LoopFoldOperation::Add => SolveTacticKind::RotateAddRecurrence, + }, + status: SolveTacticStatus::Available, + reason: rotate_recurrence_reason(*operation, term.kind), + recurrence: recurrence.clone(), + }), + _ => None, + }) + .collect::>(); + tactics.sort_by(|lhs, rhs| { + ( + lhs.recurrence.header, + lhs.recurrence.exit_target, + lhs.recurrence.accumulator.as_str(), + format!("{:?}", lhs.recurrence.kind), + lhs.reason.as_str(), + ) + .cmp(&( + rhs.recurrence.header, + rhs.recurrence.exit_target, + rhs.recurrence.accumulator.as_str(), + format!("{:?}", rhs.recurrence.kind), + rhs.reason.as_str(), + )) + }); + tactics +} + +pub fn solve_tactics_for_exact_folds(folds: &[ExactLoopFoldEvidence]) -> Vec { + let recurrences = folds + .iter() + .cloned() + .map(ExactLoopRecurrenceEvidence::from) + .collect::>(); + solve_tactics_for_exact_recurrences(&recurrences) +} + +fn tactic_for_fold_recurrence( + recurrence: &ExactLoopRecurrenceEvidence, + operation: LoopFoldOperation, + term: &crate::LoopMemoryTerm, +) -> Option { + match (operation, term.kind) { + (LoopFoldOperation::Xor, LoopMemoryTermKind::InputRead) => Some(SolveTacticEvidence { + kind: SolveTacticKind::XorFoldPreimage, + status: SolveTacticStatus::Available, + reason: "exact symbolic input xor-fold can feed bounded preimage solving".to_string(), + recurrence: recurrence.clone(), + }), + (LoopFoldOperation::Add, LoopMemoryTermKind::InputRead) => Some(SolveTacticEvidence { + kind: SolveTacticKind::AddFoldPreimage, + status: SolveTacticStatus::Available, + reason: "exact symbolic input add-fold can feed bounded preimage solving".to_string(), + recurrence: recurrence.clone(), + }), + (_, LoopMemoryTermKind::TableRead) => Some(SolveTacticEvidence { + kind: SolveTacticKind::ConcreteTableFold, + status: SolveTacticStatus::EvidenceOnly, + reason: "exact concrete table fold is available as constraint evidence".to_string(), + recurrence: recurrence.clone(), + }), + (_, LoopMemoryTermKind::RuntimeBlobRead) => Some(SolveTacticEvidence { + kind: SolveTacticKind::RuntimeBlobFold, + status: SolveTacticStatus::EvidenceOnly, + reason: "exact runtime-blob fold is available as constraint evidence".to_string(), + recurrence: recurrence.clone(), + }), + (_, LoopMemoryTermKind::Unknown) => None, + } +} + +fn rotate_recurrence_reason(operation: LoopFoldOperation, term_kind: LoopMemoryTermKind) -> String { + let operation = match operation { + LoopFoldOperation::Add => "rotate-add", + LoopFoldOperation::Xor => "rotate-xor", + }; + match term_kind { + LoopMemoryTermKind::InputRead => { + format!( + "exact symbolic input {operation} recurrence can feed bounded preimage solving when initial state is concrete" + ) + } + LoopMemoryTermKind::TableRead => { + format!( + "exact concrete-table {operation} recurrence is available as constraint evidence" + ) + } + LoopMemoryTermKind::RuntimeBlobRead => { + format!("exact runtime-blob {operation} recurrence is available as constraint evidence") + } + LoopMemoryTermKind::Unknown => { + format!("exact {operation} recurrence has unknown memory provenance") + } + } +} + +pub fn verification_requirement_for_route_and_stats( + route: &crate::TargetQueryRoutePlan, + stats: &ExploreStats, + constraint_graph: Option<&FinalConstraintGraph>, +) -> VerificationRequirement { + let summary = evidence_summary_for_route_and_stats(route, stats, constraint_graph); + if summary.reasons.is_empty() { + return VerificationRequirement::NotRequired; + } + if matches!(route.target_plan, crate::TargetQueryPlan::Refuse { .. }) + || matches!(route.execution, TargetQueryExecutionRoute::Refuse { .. }) + { + return VerificationRequirement::Refused { + reasons: summary.reasons, + }; + } + if summary.requires_replay { + VerificationRequirement::Required { + reasons: summary.reasons, + } + } else { + VerificationRequirement::NotRequired + } +} + +pub(crate) fn completion_from_stats(stats: &ExploreStats) -> QueryCompletion { + if stats.timed_out || stats.max_states_exhausted { + QueryCompletion::BudgetExhausted + } else { + QueryCompletion::Complete + } +} + +pub(crate) fn push_unique_reason(reasons: &mut Vec, reason: impl Into) { + let reason = reason.into(); + if !reasons.iter().any(|existing| existing == &reason) { + reasons.push(reason); + } +} + +pub(crate) fn collect_route_residual_reasons( + route: &TargetQueryExecutionRoute, + reasons: &mut Vec, +) { + match route { + TargetQueryExecutionRoute::ContinuationSeeded { route, .. } => { + push_unique_reason(reasons, "continuation_seeded_unverified"); + collect_route_residual_reasons(route, reasons); + } + TargetQueryExecutionRoute::ResidualOnly { + reasons: route_reasons, + } => { + for reason in route_reasons { + push_unique_reason(reasons, reason.clone()); + } + } + TargetQueryExecutionRoute::Refuse { reason } => { + push_unique_reason(reasons, format!("refused: {reason}")); + } + TargetQueryExecutionRoute::ArtifactCondition { .. } + | TargetQueryExecutionRoute::ArtifactMemoryOnly + | TargetQueryExecutionRoute::DynamicTargetCompile { .. } + | TargetQueryExecutionRoute::VmTargetCompile { .. } => {} + } +} + +pub(crate) fn solve_residual_reasons( + route: &crate::TargetQueryRoutePlan, + stats: &ExploreStats, +) -> Vec { + let mut reasons = Vec::new(); + match &route.target_plan { + crate::TargetQueryPlan::Residual { + reasons: plan_reasons, + } => { + for reason in plan_reasons { + push_unique_reason(&mut reasons, reason.clone()); + } + } + crate::TargetQueryPlan::Refuse { reason } => { + push_unique_reason(&mut reasons, format!("target refused: {reason}")); + } + crate::TargetQueryPlan::Ready { .. } | crate::TargetQueryPlan::Fallback { .. } => {} + } + collect_route_residual_reasons(&route.execution, &mut reasons); + if stats.runtime_breakpoint_loop_summaries > 0 { + push_unique_reason(&mut reasons, "runtime_breakpoint_loop_summary_residual"); + } + if stats.runtime_loop_unknown_carried_state > 0 { + push_unique_reason(&mut reasons, "runtime_loop_unknown_carried_state"); + } + if stats.runtime_loop_budget_residuals > 0 { + push_unique_reason(&mut reasons, "runtime_loop_iteration_budget"); + } + if stats.runtime_loop_refusals > 0 { + push_unique_reason(&mut reasons, "runtime_loop_refused"); + } + if stats.runtime_missing_materialized_code > 0 { + push_unique_reason(&mut reasons, "missing_runtime_materialized_code"); + } + if stats.runtime_region_provenance_unknown > 0 { + push_unique_reason(&mut reasons, "runtime_region_provenance_unknown"); + } + reasons +} + +pub(crate) fn residual_summary_blocks_solution_extraction( + stats: &ExploreStats, + residual_reasons: &[String], +) -> bool { + stats.runtime_breakpoint_loop_summaries > 0 + && residual_reasons + .iter() + .any(|reason| reason == "runtime_breakpoint_loop_summary_residual") +} + +pub(crate) fn solve_verification( + target_reached_under_model: bool, + solution_present: bool, + residual_reasons: Vec, + model_validation: ModelValidation, +) -> SolveVerification { + let candidate_solution_verified = + solution_present && matches!(model_validation, ModelValidation::Verified); + SolveVerification { + target_reached_under_model, + candidate_solution_verified, + residual_reasons, + model_validation, + } +} + +pub(crate) fn classify_solve_status( + exact_unsat: bool, + selected_path_index: Option, + solution_present: bool, + completion: QueryCompletion, + verification: &SolveVerification, + constraint_graph: &FinalConstraintGraph, +) -> SolveStatus { + if exact_unsat { + return SolveStatus::Unsat; + } + let has_exact_final_constraints = constraint_graph.has_exact_constraints(); + let has_model_conditioned_final_constraints = + constraint_graph.model_conditioned_constraint_count() > 0; + if selected_path_index.is_some() + && !verification.residual_reasons.is_empty() + && !verification.candidate_solution_verified + { + if has_exact_final_constraints + && !matches!( + verification.model_validation, + ModelValidation::Failed { .. } + ) + { + return SolveStatus::Candidate; + } + return match verification.model_validation { + ModelValidation::Failed { .. } => SolveStatus::Unverified, + ModelValidation::NotRequired | ModelValidation::Unavailable { .. } => { + SolveStatus::ResidualReachable + } + ModelValidation::Verified => SolveStatus::Solved, + }; + } + match ( + selected_path_index, + solution_present, + completion, + &verification.model_validation, + ) { + (Some(_), true, _, ModelValidation::Unavailable { .. }) if has_exact_final_constraints => { + SolveStatus::Candidate + } + ( + Some(_), + true, + _, + ModelValidation::Failed { .. } | ModelValidation::Unavailable { .. }, + ) => SolveStatus::Unverified, + (Some(_), true, _, ModelValidation::NotRequired | ModelValidation::Verified) => { + if has_model_conditioned_final_constraints && !has_exact_final_constraints { + SolveStatus::Unverified + } else { + SolveStatus::Solved + } + } + (None, _, QueryCompletion::BudgetExhausted, _) => SolveStatus::BudgetExhausted, + (None, _, QueryCompletion::Complete, _) => SolveStatus::Unsat, + (Some(_), false, QueryCompletion::BudgetExhausted, _) => SolveStatus::BudgetExhausted, + (Some(_), false, _, _) => SolveStatus::Unknown, + } +} + +fn concrete_value_to_width(value: u64, bits: u32) -> u64 { + if bits >= 64 { + value + } else if bits == 0 { + 0 + } else { + value & ((1u64 << bits) - 1) + } +} + +fn little_endian_bytes_to_u64(bytes: &[u8]) -> Option { + if bytes.len() > 8 { + return None; + } + let mut value = 0u64; + for (index, byte) in bytes.iter().enumerate() { + value |= (*byte as u64) << (index * 8); + } + Some(value) +} + +fn solution_constraints_for_state<'ctx>( + state: &SymState<'ctx>, + solution: &SolvedPath, +) -> Vec<(crate::SymValue<'ctx>, u64)> { + let mut constraints = Vec::new(); + for (name, value) in &solution.inputs { + if let Some(symbolic) = state.symbolic_inputs().get(name) { + constraints.push(( + symbolic.clone(), + concrete_value_to_width(*value, symbolic.bits()), + )); + } + } + for (input_name, bytes) in &solution.input_buffers { + for input in state.symbolic_fd_inputs().values() { + if input.name != *input_name { + continue; + } + for (byte, value) in input.bytes.iter().zip(bytes.iter()) { + constraints.push((byte.clone(), *value as u64)); + } + } + } + for (region_name, bytes) in &solution.memory { + let Some(value) = little_endian_bytes_to_u64(bytes) else { + continue; + }; + if let Some(region) = state + .symbolic_memory() + .iter() + .find(|region| region.name == *region_name) + { + constraints.push(( + region.value.clone(), + concrete_value_to_width(value, region.value.bits()), + )); + } + } + constraints +} + +fn apply_solution_constraints<'ctx>( + state: &mut SymState<'ctx>, + solution: &SolvedPath, +) -> Result { + let constraints = solution_constraints_for_state(state, solution); + if constraints.is_empty() + && (!solution.inputs.is_empty() + || !solution.input_buffers.is_empty() + || !solution.memory.is_empty()) + { + return Err("candidate model did not match validation seed inputs".to_string()); + } + let count = constraints.len(); + for (value, concrete) in constraints { + state.constrain_eq(&value, concrete); + } + Ok(count) +} + +fn validate_solution_with_lifted_semantics<'ctx>( + explorer: &mut PathExplorer<'ctx>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + mut initial_state: SymState<'ctx>, + target_addr: u64, + solution: &SolvedPath, +) -> ModelValidation { + match apply_solution_constraints(&mut initial_state, solution) { + Ok(_) => {} + Err(reason) => return ModelValidation::Unavailable { reason }, + } + + let (found, replay_stats) = explorer.with_isolated_stats(|explorer| { + let previous_target_guided = explorer.target_guided_queries_enabled(); + explorer.set_target_guided_queries(true); + let found = explorer.with_residual_runtime_loop_summaries(false, |explorer| { + explorer.find_path_to_in_scope(func, scope, initial_state, target_addr) + }); + explorer.set_target_guided_queries(previous_target_guided); + found + }); + + match found { + Some(path) if path.final_pc() == target_addr => ModelValidation::Verified, + Some(path) => ModelValidation::Failed { + reason: format!( + "lifted replay stopped at 0x{:x} instead of 0x{:x}", + path.final_pc(), + target_addr + ), + }, + None if replay_stats.timed_out || replay_stats.max_states_exhausted => { + ModelValidation::Unavailable { + reason: "lifted replay validation exhausted budget".to_string(), + } + } + None => ModelValidation::Failed { + reason: "lifted replay did not reach target with candidate model".to_string(), + }, + } +} + +#[cfg(test)] +mod tests { + use super::{ + ModelValidation, SolveTacticKind, SolveTacticStatus, classify_solve_status, + evidence_summary_for_route_and_stats, residual_summary_blocks_solution_extraction, + solve_residual_reasons, solve_tactics_for_exact_folds, solve_tactics_for_exact_recurrences, + solve_verification, + }; + use crate::{ + ExactLoopFoldEvidence, ExactLoopRecurrenceEvidence, FinalConstraint, FinalConstraintGraph, + FinalConstraintPrecision, FinalConstraintSource, LoopFoldOperation, LoopMemoryTerm, + LoopMemoryTermKind, LoopRotateDirection, QueryCompletion, QueryGuidanceMode, + RecurrenceAggregateConstraint, SolveStatus, TargetQueryExecutionRoute, TargetQueryPlan, + TargetQueryRoutePlan, + }; + + fn exact_graph() -> FinalConstraintGraph { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 4, + accumulator: "RBX_2".to_string(), + bits: 64, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(4), + }, + }; + FinalConstraintGraph { + constraints: vec![FinalConstraint::RecurrenceEquals( + RecurrenceAggregateConstraint { + recurrence: fold.into(), + target: 0x41, + bits: 64, + source: FinalConstraintSource::TerminalCompareExact, + precision: FinalConstraintPrecision::Exact, + reasons: Vec::new(), + }, + )], + input_byte_constraints: Vec::new(), + input_length_constraints: Vec::new(), + refusals: Vec::new(), + } + } + + #[test] + fn residual_runtime_loop_summary_cannot_be_solved() { + let route = TargetQueryRoutePlan { + target_plan: TargetQueryPlan::Ready { + mode: QueryGuidanceMode::NarrowOnly, + }, + execution: TargetQueryExecutionRoute::DynamicTargetCompile { + reason: "large cfg".to_string(), + mode: QueryGuidanceMode::NarrowOnly, + }, + }; + let stats = crate::path::ExploreStats { + runtime_breakpoint_loop_summaries: 1, + ..crate::path::ExploreStats::default() + }; + let reasons = solve_residual_reasons(&route, &stats); + assert_eq!( + reasons, + vec!["runtime_breakpoint_loop_summary_residual".to_string()] + ); + assert!(residual_summary_blocks_solution_extraction( + &stats, &reasons + )); + let verification = solve_verification( + true, + true, + reasons, + ModelValidation::Unavailable { + reason: "residual/runtime evidence requires concrete replay validation".to_string(), + }, + ); + + assert_eq!( + classify_solve_status( + false, + Some(0), + true, + QueryCompletion::Complete, + &verification, + &FinalConstraintGraph::default(), + ), + SolveStatus::ResidualReachable + ); + assert!(!verification.candidate_solution_verified); + assert!(matches!( + verification.model_validation, + ModelValidation::Unavailable { .. } + )); + } + + #[test] + fn residual_route_cannot_be_solved() { + let route = TargetQueryRoutePlan { + target_plan: TargetQueryPlan::Residual { + reasons: vec!["LargeCfg".to_string()], + }, + execution: TargetQueryExecutionRoute::ResidualOnly { + reasons: vec!["continuation-seeded runtime execution".to_string()], + }, + }; + let reasons = solve_residual_reasons(&route, &crate::path::ExploreStats::default()); + let verification = solve_verification( + true, + true, + reasons, + ModelValidation::Unavailable { + reason: "residual/runtime evidence requires concrete replay validation".to_string(), + }, + ); + + assert_eq!( + classify_solve_status( + false, + Some(0), + true, + QueryCompletion::Complete, + &verification, + &FinalConstraintGraph::default(), + ), + SolveStatus::ResidualReachable + ); + } + + #[test] + fn verified_residual_route_can_be_solved() { + let verification = solve_verification( + true, + true, + vec!["continuation_seeded_unverified".to_string()], + ModelValidation::Verified, + ); + + assert_eq!( + classify_solve_status( + false, + Some(0), + true, + QueryCompletion::Complete, + &verification, + &exact_graph(), + ), + SolveStatus::Solved + ); + assert!(verification.candidate_solution_verified); + } + + #[test] + fn failed_residual_replay_is_unverified() { + let verification = solve_verification( + true, + true, + vec!["continuation_seeded_unverified".to_string()], + ModelValidation::Failed { + reason: "candidate rejected".to_string(), + }, + ); + + assert_eq!( + classify_solve_status( + false, + Some(0), + true, + QueryCompletion::Complete, + &verification, + &exact_graph(), + ), + SolveStatus::Unverified + ); + assert!(!verification.candidate_solution_verified); + } + + #[test] + fn exact_final_constraint_without_replay_proof_is_candidate() { + let verification = solve_verification( + true, + true, + Vec::new(), + ModelValidation::Unavailable { + reason: "lifted replay validation exhausted budget".to_string(), + }, + ); + + assert_eq!( + classify_solve_status( + false, + Some(0), + true, + QueryCompletion::Complete, + &verification, + &exact_graph(), + ), + SolveStatus::Candidate + ); + } + + #[test] + fn exact_input_fold_evidence_enables_preimage_tactic_without_downgrade() { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 4, + accumulator: "RBX_2".to_string(), + bits: 64, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(4), + }, + }; + let tactics = solve_tactics_for_exact_folds(std::slice::from_ref(&fold)); + assert_eq!(tactics.len(), 1); + assert_eq!(tactics[0].kind, SolveTacticKind::XorFoldPreimage); + assert_eq!(tactics[0].status, SolveTacticStatus::Available); + assert_eq!( + tactics[0].recurrence, + ExactLoopRecurrenceEvidence::from(fold.clone()) + ); + + let route = TargetQueryRoutePlan { + target_plan: TargetQueryPlan::Ready { + mode: QueryGuidanceMode::NarrowOnly, + }, + execution: TargetQueryExecutionRoute::DynamicTargetCompile { + reason: "test".to_string(), + mode: QueryGuidanceMode::NarrowOnly, + }, + }; + let stats = crate::path::ExploreStats { + runtime_loop_exact_recurrences: vec![ExactLoopRecurrenceEvidence::from(fold.clone())], + runtime_loop_exact_folds: vec![fold], + ..crate::path::ExploreStats::default() + }; + let evidence = evidence_summary_for_route_and_stats(&route, &stats, None); + assert_eq!(evidence.precision, crate::FactPrecision::Exact); + assert!(!evidence.requires_replay); + assert!(evidence.reasons.is_empty()); + assert_eq!(evidence.exact_loop_recurrences.len(), 1); + assert_eq!(evidence.exact_loop_folds.len(), 1); + assert_eq!(evidence.tactics.len(), 1); + } + + #[test] + fn exact_rotate_input_recurrence_is_reported_as_available_tactic() { + let recurrence = ExactLoopRecurrenceEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 4, + accumulator: "RBX_2".to_string(), + initial: "RBX_1".to_string(), + bits: 8, + kind: crate::ExactLoopRecurrenceKind::RotateMix { + direction: LoopRotateDirection::Left, + amount: 3, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(4), + }, + }, + }; + let tactics = solve_tactics_for_exact_recurrences(std::slice::from_ref(&recurrence)); + assert_eq!(tactics.len(), 1); + assert_eq!(tactics[0].kind, SolveTacticKind::RotateXorRecurrence); + assert_eq!(tactics[0].status, SolveTacticStatus::Available); + assert_eq!(tactics[0].recurrence, recurrence); + } +} diff --git a/crates/r2sym/tests/symex_integration.rs b/crates/r2sym/tests/symex_integration.rs index 74bf18e..8d719ae 100644 --- a/crates/r2sym/tests/symex_integration.rs +++ b/crates/r2sym/tests/symex_integration.rs @@ -3,7 +3,7 @@ //! These tests verify the symbolic execution engine works correctly //! with real SSA functions and Z3 constraint solving. -use r2il::{R2ILBlock, R2ILOp, SpaceId, Varnode}; +use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; use r2ssa::SsaArtifact; use r2sym::SymSolver; use r2sym::path::ExploreStrategy; @@ -11,8 +11,16 @@ use r2sym::sim::{ CallInfo, FunctionSummary, MemcmpSummary, MemsetSummary, PrintfSummaryBasic, PutsSummary, SummaryRegistry, }; -use r2sym::{ExploreConfig, PathExplorer, SymState, SymValue}; +use r2sym::spec::{AddressValue, ExplorationSpec, InputSpec, PredicateSpec}; +use r2sym::{ + BackwardConditionPrecision, BackwardMemoryCondition, BackwardMemoryRegion, ExploreConfig, + PathExplorer, QueryCompletion, ReachabilityStatus, RefinementStage, ReplayRegisterOverlay, + ReplayRegisterValue, ReplaySeed, SolveStatus, SymQueryConfig, SymState, SymValue, + SymbolicReachabilityStatus, compile_derived_summary_return_postcondition, + compile_function_semantics_with_scope, +}; use z3::Context; +use z3::ast::{BV, Bool}; // Helper functions for creating varnodes fn make_reg(offset: u64, size: u32) -> Varnode { @@ -33,11 +41,269 @@ fn make_const(val: u64, size: u32) -> Varnode { } } +fn region_for_anchor(artifact: &r2sym::SemanticArtifact, anchor: u64) -> &r2sym::SemanticRegion { + artifact + .native_body() + .expect("native artifact") + .regions + .values() + .find(|region| region.anchor == anchor) + .expect("semantic region") +} + +fn assert_argument_memory_term( + term: &BackwardMemoryCondition, + index: usize, + offset_lo: i64, + offset_hi: i64, + exact_offset: bool, + size: u32, +) { + assert!(matches!( + &term.region, + BackwardMemoryRegion::Argument { index: actual } if *actual == index + )); + assert_eq!(term.offset_lo, offset_lo); + assert_eq!(term.offset_hi, offset_hi); + assert_eq!(term.exact_offset, exact_offset); + assert_eq!(term.size, size); +} + +fn assert_region_memory_term( + term: &BackwardMemoryCondition, + kind: r2sym::MemoryRegionKind, + name: &str, + offset_lo: i64, + offset_hi: i64, + exact_offset: bool, + size: u32, +) { + match &term.region { + BackwardMemoryRegion::Region(region) => { + assert_eq!(region.kind, kind); + assert_eq!(region.name, name); + } + other => panic!("expected concrete region-backed term, got {other:?}"), + } + assert_eq!(term.offset_lo, offset_lo); + assert_eq!(term.offset_hi, offset_hi); + assert_eq!(term.exact_offset, exact_offset); + assert_eq!(term.size, size); +} + +fn make_x86_64_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("RAX", RAX, 8)); + arch.add_register(RegisterDef::new("EAX", RAX, 4)); + arch.add_register(RegisterDef::new("RCX", RCX, 8)); + arch.add_register(RegisterDef::new("EDI", RDI, 4)); + arch.add_register(RegisterDef::new("RDI", RDI, 8)); + arch.add_register(RegisterDef::new("RSI", RSI, 8)); + arch.add_register(RegisterDef::new("RDX", RDX, 8)); + arch +} + // Simulated x86-64 register offsets const RAX: u64 = 0; const RBX: u64 = 8; const RCX: u64 = 16; const RDI: u64 = 56; +const RSI: u64 = 64; +const RDX: u64 = 72; +const TMP0: u64 = 0x80; +const TMP1: u64 = 0x88; + +fn make_conditional_branch_blocks() -> Vec { + vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(0x1337, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 6, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 6, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ] +} + +fn make_conditional_branch_blocks_low32_arg() -> Vec { + vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 4), + b: make_const(0xdead, 4), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 6, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 6, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ] +} + +fn make_self_xor_guard_blocks() -> Vec { + vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(TMP0, 8), + a: make_reg(RDI, 8), + b: make_reg(RDI, 8), + }, + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(TMP0, 8), + b: make_const(0, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ] +} + +fn make_target_guided_budget_blocks() -> Vec { + vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(0x1337, 8), + }, + R2ILOp::CBranch { + target: make_const(0x2000, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1008, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1008, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x100c, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x100c, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x2000, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ] +} #[test] fn test_symbolic_execution_linear_block() { @@ -190,11 +456,13 @@ fn test_symbolic_execution_conditional_branch() { let config = ExploreConfig { max_states: 100, + max_completed_paths: None, max_depth: 50, timeout: None, strategy: ExploreStrategy::Dfs, prune_infeasible: true, merge_states: false, + ..ExploreConfig::default() }; let mut explorer = PathExplorer::with_config(&ctx, config); @@ -216,549 +484,4210 @@ fn test_symbolic_execution_conditional_branch() { } #[test] -fn test_find_paths_to_collects_multiple_matches() { +fn test_query_defaults_enable_subsumption() { + let config = SymQueryConfig::default(); + assert!(config.explore.subsumption_states); +} + +#[test] +fn test_query_target_guided_mode_enables_ranked_query_search() { + let ctx = Context::thread_local(); + let explorer = SymQueryConfig { + mode: r2sym::QueryMode::TargetGuided, + ..SymQueryConfig::default() + } + .make_explorer(&ctx); + assert!(explorer.target_guided_queries_enabled()); +} + +#[test] +fn test_query_can_reach_reports_reachable_and_unreachable() { + let blocks = make_conditional_branch_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + + let mut reachable_state = SymState::new(&ctx, 0x1000); + reachable_state.make_symbolic("reg:56_0", 64); + let mut explorer = SymQueryConfig { + mode: r2sym::QueryMode::TargetGuided, + ..SymQueryConfig::default() + } + .make_explorer(&ctx); + let reachable = explorer.can_reach(&func, reachable_state, 0x1010); + assert_eq!(reachable.status, ReachabilityStatus::Reachable); + assert!(!reachable.paths.is_empty()); + + let mut unreachable_state = SymState::new(&ctx, 0x1000); + unreachable_state.make_symbolic("reg:56_0", 64); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let unreachable = explorer.can_reach(&func, unreachable_state, 0x2000); + assert_eq!(unreachable.status, ReachabilityStatus::Unreachable); + assert!(unreachable.paths.is_empty()); +} + +#[test] +fn test_query_solve_for_target_returns_solution() { + let blocks = make_conditional_branch_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let solve = explorer.solve_for_target(&func, state, 0x1010); + assert_eq!(solve.status, SolveStatus::Solved); + assert_eq!( + solve + .compiled_precondition + .as_ref() + .map(|compiled| compiled.precision), + Some(BackwardConditionPrecision::Exact) + ); + assert!(solve.selected_path_index.is_some()); + assert!(solve.solution.is_some()); + assert!(solve.verification.candidate_solution_verified); + assert!(matches!( + solve.verification.model_validation, + r2sym::ModelValidation::Verified + )); + assert!(solve.witness.is_proven()); +} + +#[test] +fn test_query_path_conditions_at_collects_conditions() { + let blocks = make_conditional_branch_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let conditions = explorer.path_conditions_at(&func, state, 0x1010); + assert_eq!( + conditions + .compiled_precondition + .as_ref() + .map(|compiled| compiled.precision), + Some(BackwardConditionPrecision::Exact) + ); + assert!(!conditions.conditions.is_empty()); + assert!( + conditions + .conditions + .iter() + .all(|condition| condition.final_pc == 0x1010) + ); + assert!( + conditions + .conditions + .iter() + .all(|condition| condition.num_constraints > 0) + ); + assert!(conditions.conditions.iter().all(|condition| { + condition.condition == condition.path_condition.simplified + && condition.path_condition.num_constraints == condition.num_constraints + && condition.path_condition.terms.len() == condition.num_constraints + })); +} + +#[test] +fn test_query_compiled_precondition_shortcuts_exact_unsat() { + let blocks = make_conditional_branch_blocks(); + let arch = make_x86_64_arch(); + let func = + SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic_named("RDI_0", "rdi", 64); + let rdi = state.get_register_sized("RDI_0", 64); + state.constrain_ne(&rdi, 0x1337); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let solve = explorer.solve_for_target(&func, state, 0x1010); + assert_eq!(solve.status, SolveStatus::Unsat); + assert!(solve.matched_paths.is_empty()); + assert_eq!( + solve + .compiled_precondition + .as_ref() + .map(|compiled| compiled.precision), + Some(BackwardConditionPrecision::Exact) + ); +} + +#[test] +fn test_query_load_dependent_precondition_compiles_through_local_store() { let blocks = vec![ R2ILBlock { addr: 0x1000, size: 4, + switch_info: None, + op_metadata: Default::default(), ops: vec![ + R2ILOp::Store { + addr: make_const(0x2000, 8), + val: make_reg(RDI, 8), + space: SpaceId::Ram, + }, + R2ILOp::Load { + dst: make_reg(TMP0, 8), + addr: make_const(0x2000, 8), + space: SpaceId::Ram, + }, R2ILOp::IntEqual { - dst: make_reg(RCX, 1), - a: make_reg(RDI, 8), + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 8), b: make_const(0x1337, 8), }, R2ILOp::CBranch { target: make_const(0x1010, 8), - cond: make_reg(RCX, 1), + cond: make_reg(TMP1, 1), }, ], - switch_info: None, - op_metadata: Default::default(), }, R2ILBlock { addr: 0x1004, - size: 4, - ops: vec![R2ILOp::Branch { - target: make_const(0x1010, 8), - }], + size: 1, switch_info: None, op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], }, R2ILBlock { addr: 0x1010, - size: 4, - ops: vec![R2ILOp::Copy { - dst: make_reg(RAX, 8), - src: make_const(1, 8), - }], + size: 1, switch_info: None, op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(1, 8), + }], }, ]; - - let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); state.make_symbolic("reg:56_0", 64); - let mut explorer = PathExplorer::new(&ctx); - let paths = explorer.find_paths_to(&func, state, 0x1010); - assert!( - paths.len() >= 2, - "Expected multiple target-reaching paths, got {}", - paths.len() + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let solve = explorer.solve_for_target(&func, state, 0x1010); + assert_eq!(solve.status, SolveStatus::Solved); + assert_eq!( + solve + .compiled_precondition + .as_ref() + .map(|compiled| compiled.precision), + Some(BackwardConditionPrecision::Exact) ); + assert!(!solve.matched_paths.is_empty()); + assert!(solve.solution.is_some()); } #[test] -fn test_find_paths_to_unreachable_returns_empty() { - let blocks = vec![R2ILBlock { - addr: 0x1000, - size: 4, - ops: vec![R2ILOp::Copy { - dst: make_reg(RAX, 8), - src: make_const(1, 8), - }], - switch_info: None, - op_metadata: Default::default(), - }]; - - let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); +fn test_query_load_dependent_precondition_emits_region_backed_global_term() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 8), + addr: make_const(0x2000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 8), + b: make_const(0x41, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(1, 8), + }], + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); let ctx = Context::thread_local(); - let state = SymState::new(&ctx, 0x1000); - let mut explorer = PathExplorer::new(&ctx); - let paths = explorer.find_paths_to(&func, state, 0x2000); - assert!( - paths.is_empty(), - "Expected no paths for unreachable target, got {}", - paths.len() + + let mut state = SymState::new(&ctx, 0x1000); + let region = state.define_memory_region( + r2sym::MemoryRegionKind::Global, + "global_2000", + Some(0x2000), + Some(8), + ); + state.seed_region_bytes(region, 0, &[0x41, 0, 0, 0, 0, 0, 0, 0]); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let conditions = explorer.path_conditions_at(&func, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_region_memory_term( + &compiled.memory_terms[0], + r2sym::MemoryRegionKind::Global, + "global_2000", + 0, + 0, + true, + 8, ); } #[test] -fn test_find_paths_to_honors_limits() { +fn test_query_load_dependent_precondition_emits_region_backed_stack_term() { let blocks = vec![ R2ILBlock { addr: 0x1000, size: 4, + switch_info: None, + op_metadata: Default::default(), ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 8), + addr: make_const(0x7008, 8), + space: SpaceId::Ram, + }, R2ILOp::IntEqual { - dst: make_reg(RCX, 1), - a: make_reg(RDI, 8), - b: make_const(0, 8), + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 8), + b: make_const(0x41, 8), }, R2ILOp::CBranch { target: make_const(0x1010, 8), - cond: make_reg(RCX, 1), + cond: make_reg(TMP1, 1), }, ], + }, + R2ILBlock { + addr: 0x1004, + size: 1, switch_info: None, op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], }, R2ILBlock { addr: 0x1010, - size: 4, - ops: vec![R2ILOp::Copy { - dst: make_reg(RAX, 8), - src: make_const(1, 8), - }], + size: 1, switch_info: None, op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(1, 8), + }], }, ]; - - let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); - state.make_symbolic("reg:56_0", 64); + let region = state.define_memory_region( + r2sym::MemoryRegionKind::Stack, + "stack_window", + Some(0x7000), + Some(0x100), + ); + state.seed_region_bytes(region, 8, &[0x41, 0, 0, 0, 0, 0, 0, 0]); - let config = ExploreConfig { - max_states: 0, - max_depth: 50, - timeout: None, - strategy: ExploreStrategy::Dfs, - prune_infeasible: true, - merge_states: false, - }; - let mut explorer = PathExplorer::with_config(&ctx, config); - let paths = explorer.find_paths_to(&func, state, 0x1010); - assert!( - paths.is_empty(), - "Expected no matches when max_states=0, got {}", - paths.len() + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let conditions = explorer.path_conditions_at(&func, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_region_memory_term( + &compiled.memory_terms[0], + r2sym::MemoryRegionKind::Stack, + "stack_window", + 8, + 8, + true, + 8, ); } #[test] -fn test_symbolic_arithmetic_operations() { - // Test all arithmetic operations with symbolic values +fn test_query_load_dependent_precondition_emits_region_backed_replay_term() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 8), + addr: make_const(0x9004, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 8), + b: make_const(0x41, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(1, 8), + }], + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); let ctx = Context::thread_local(); - let _state = SymState::new(&ctx, 0x1000); - - // Create symbolic values - let x = SymValue::new_symbolic(&ctx, "x", 64); - let y = SymValue::new_symbolic(&ctx, "y", 64); + let mut state = SymState::new(&ctx, 0x1000); + let region = state.define_memory_region( + r2sym::MemoryRegionKind::Replay, + "replay_window", + Some(0x9000), + Some(0x100), + ); + state.seed_region_bytes(region, 4, &[0x41, 0, 0, 0, 0, 0, 0, 0]); - // Test addition - let sum = x.add(&ctx, &y); - assert!(sum.is_symbolic(), "Sum should be symbolic"); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let conditions = explorer.path_conditions_at(&func, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); - // Test subtraction - let diff = x.sub(&ctx, &y); - assert!(diff.is_symbolic(), "Diff should be symbolic"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_region_memory_term( + &compiled.memory_terms[0], + r2sym::MemoryRegionKind::Replay, + "replay_window", + 4, + 4, + true, + 8, + ); +} - // Test multiplication - let prod = x.mul(&ctx, &y); - assert!(prod.is_symbolic(), "Product should be symbolic"); +#[test] +fn test_query_load_dependent_precondition_emits_region_backed_heap_term() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 8), + addr: make_const(0xa004, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 8), + b: make_const(0x41, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(1, 8), + }], + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); - // Test concrete operations - let a = SymValue::concrete(10, 64); - let b = SymValue::concrete(3, 64); + let mut state = SymState::new(&ctx, 0x1000); + let region = state.define_memory_region( + r2sym::MemoryRegionKind::Heap, + "heap_window", + Some(0xa000), + Some(0x100), + ); + state.seed_region_bytes(region, 4, &[0x41, 0, 0, 0, 0, 0, 0, 0]); - let sum_concrete = a.add(&ctx, &b); - assert_eq!(sum_concrete.as_concrete(), Some(13)); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let conditions = explorer.path_conditions_at(&func, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); - let diff_concrete = a.sub(&ctx, &b); - assert_eq!(diff_concrete.as_concrete(), Some(7)); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_region_memory_term( + &compiled.memory_terms[0], + r2sym::MemoryRegionKind::Heap, + "heap_window", + 4, + 4, + true, + 8, + ); +} - let prod_concrete = a.mul(&ctx, &b); - assert_eq!(prod_concrete.as_concrete(), Some(30)); +#[test] +fn test_replay_seeded_query_solve_with_register_overlay() { + let blocks = make_conditional_branch_blocks(); + let arch = make_x86_64_arch(); + let func = + SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); - let div_concrete = a.udiv(&ctx, &b); - assert_eq!(div_concrete.as_concrete(), Some(3)); + let mut state = SymState::new(&ctx, 0x1000); + let seed = ReplaySeed { + checkpoint_id: Some(2), + entry_pc: Some(0x1000), + registers: vec![ReplayRegisterValue { + name: "rdi".to_string(), + value: 0, + }], + register_overlays: vec![ReplayRegisterOverlay { + name: "rdi".to_string(), + symbol: "rdi".to_string(), + }], + ..ReplaySeed::default() + }; + r2sym::seed_replay_state_for_arch(&mut state, Some(&func), Some(&arch), &seed); - let rem_concrete = a.urem(&ctx, &b); - assert_eq!(rem_concrete.as_concrete(), Some(1)); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let solve = explorer.solve_for_target(&func, state, 0x1010); + assert_eq!(solve.status, SolveStatus::Solved); + assert_eq!( + solve + .compiled_precondition + .as_ref() + .map(|compiled| compiled.precision), + Some(BackwardConditionPrecision::Exact) + ); + let solution = solve.solution.expect("solution"); + assert_eq!(solution.inputs.get("rdi").copied(), Some(0x1337)); } #[test] -fn test_symbolic_bitwise_operations() { +fn test_query_compiles_bounded_indexed_memory_range() { + let arch = make_x86_64_arch(); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntAdd { + dst: make_reg(TMP0, 8), + a: make_reg(RDI, 8), + b: make_reg(RCX, 8), + }, + R2ILOp::Load { + dst: make_reg(TMP1, 1), + addr: make_reg(TMP0, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(RDX, 1), + a: make_reg(TMP1, 1), + b: make_const(0x41, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RDX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("ssa"); let ctx = Context::thread_local(); - // Test concrete bitwise - let a = SymValue::concrete(0b1100, 8); - let b = SymValue::concrete(0b1010, 8); - - assert_eq!(a.and(&ctx, &b).as_concrete(), Some(0b1000)); - assert_eq!(a.or(&ctx, &b).as_concrete(), Some(0b1110)); - assert_eq!(a.xor(&ctx, &b).as_concrete(), Some(0b0110)); - - // Test shifts - let amt = SymValue::concrete(2, 8); - assert_eq!(a.shl(&ctx, &amt).as_concrete(), Some(0b110000)); - assert_eq!(a.lshr(&ctx, &amt).as_concrete(), Some(0b0011)); + let mut state = SymState::new(&ctx, 0x1000); + r2sym::seed_default_state_for_arch(&mut state, &func, Some(&arch)); + let rcx = state.get_register_sized("RCX_0", 64); + let is_zero = rcx.to_bv(&ctx).eq(BV::from_u64(0, 64)); + let is_one = rcx.to_bv(&ctx).eq(BV::from_u64(1, 64)); + state.add_constraint(Bool::or(&[&is_zero, &is_one])); - // Test symbolic bitwise - let x = SymValue::new_symbolic(&ctx, "x", 64); - let y = SymValue::new_symbolic(&ctx, "y", 64); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let conditions = explorer.path_conditions_at(&func, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); - assert!(x.and(&ctx, &y).is_symbolic()); - assert!(x.or(&ctx, &y).is_symbolic()); - assert!(x.xor(&ctx, &y).is_symbolic()); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + if matches!( + &compiled.memory_terms[0].region, + BackwardMemoryRegion::Argument { .. } + ) { + assert_eq!(compiled.memory_terms[0].offset_lo, 0); + assert_eq!(compiled.memory_terms[0].offset_hi, 1); + } else { + assert!(compiled.memory_terms[0].offset_hi >= compiled.memory_terms[0].offset_lo); + } + assert!(!compiled.memory_terms[0].exact_offset); + assert_eq!(compiled.memory_terms[0].size, 1); + assert!(compiled.backward_memory_candidate_enumerations > 0); + assert_eq!(compiled.backward_memory_residual_fallbacks, 0); } #[test] -fn test_symbolic_comparisons() { +fn test_replay_seeded_query_solve_with_register_alias_overlay() { + let blocks = make_conditional_branch_blocks_low32_arg(); + let arch = make_x86_64_arch(); + let func = + SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("Failed to build SSA function"); let ctx = Context::thread_local(); - let a = SymValue::concrete(10, 32); - let b = SymValue::concrete(20, 32); + let mut state = SymState::new(&ctx, 0x1000); + let seed = ReplaySeed { + checkpoint_id: Some(2), + entry_pc: Some(0x1000), + registers: vec![ReplayRegisterValue { + name: "rdi".to_string(), + value: 0, + }], + register_overlays: vec![ReplayRegisterOverlay { + name: "rdi".to_string(), + symbol: "rdi".to_string(), + }], + ..ReplaySeed::default() + }; + r2sym::seed_replay_state_for_arch(&mut state, Some(&func), Some(&arch), &seed); - // Equality - assert_eq!(a.eq(&ctx, &a).as_concrete(), Some(1)); - assert_eq!(a.eq(&ctx, &b).as_concrete(), Some(0)); + assert!(state.get_register("EDI_0").is_symbolic()); + assert!(!state.registers().contains_key("RDI")); - // Unsigned less than - assert_eq!(a.ult(&ctx, &b).as_concrete(), Some(1)); - assert_eq!(b.ult(&ctx, &a).as_concrete(), Some(0)); - - // Unsigned less than or equal - assert_eq!(a.ule(&ctx, &b).as_concrete(), Some(1)); - assert_eq!(a.ule(&ctx, &a).as_concrete(), Some(1)); - assert_eq!(b.ule(&ctx, &a).as_concrete(), Some(0)); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let solve = explorer.solve_for_target(&func, state, 0x1010); + assert_eq!(solve.status, SolveStatus::Solved); + let solution = solve.solution.expect("solution"); + assert_eq!(solution.inputs.get("rdi").copied(), Some(0xdead)); } #[test] -fn test_symbolic_memory_operations() { +fn test_query_target_guided_can_reach_finds_target_under_tight_budget() { + let blocks = make_target_guided_budget_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); let ctx = Context::thread_local(); let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); - // Write concrete value to concrete address - let addr = SymValue::concrete(0x2000, 64); - let value = SymValue::concrete(0xDEADBEEF, 32); - state.mem_write(&addr, &value, 4); - - // Read it back - let read_value = state.mem_read(&addr, 4); - assert_eq!(read_value.as_concrete(), Some(0xDEADBEEF)); - - // Write symbolic value - let sym_value = SymValue::new_symbolic(&ctx, "mem_data", 64); - let addr2 = SymValue::concrete(0x3000, 64); - state.mem_write(&addr2, &sym_value, 8); + let forward_config = SymQueryConfig { + explore: ExploreConfig { + max_states: 2, + ..ExploreConfig::default() + }, + mode: r2sym::QueryMode::ForwardOnly, + ..SymQueryConfig::default() + }; + let mut forward = forward_config.make_explorer(&ctx); + let forward_result = forward.can_reach(&func, state.fork(), 0x2000); + assert!( + matches!( + forward_result.status, + ReachabilityStatus::BudgetExhausted | ReachabilityStatus::Reachable + ), + "forward query should stay honest under tight budgets" + ); - // Read symbolic value - let read_sym = state.mem_read(&addr2, 8); - assert!(read_sym.is_symbolic()); + let guided_config = SymQueryConfig { + explore: ExploreConfig { + max_states: 2, + ..ExploreConfig::default() + }, + mode: r2sym::QueryMode::TargetGuided, + ..SymQueryConfig::default() + }; + let mut guided = guided_config.make_explorer(&ctx); + let guided_result = guided.can_reach(&func, state, 0x2000); + assert_eq!(guided_result.status, ReachabilityStatus::Reachable); + assert_eq!(guided_result.paths.len(), 1); } #[test] -fn test_state_forking() { +fn compile_function_semantics_prunes_self_xor_dead_branch() { + let arch = make_x86_64_arch(); + let func = SsaArtifact::for_symbolic(&make_self_xor_guard_blocks(), Some(&arch)) + .expect("symbolic ssa"); let ctx = Context::thread_local(); - let mut state = SymState::new(&ctx, 0x1000); - state.set_concrete("rax", 42, 64); - state.make_symbolic("rbx", 64); - - // Fork the state - let forked = state.fork(); + let artifact = compile_function_semantics_with_scope( + &ctx, + &func, + None, + Some(&arch), + &std::collections::HashMap::new(), + r2sym::SummaryProfile::Default, + ); + let region = region_for_anchor(&artifact, 0x1000); - // Original and fork should have same values - assert_eq!(forked.pc, state.pc); assert_eq!( - forked.get_register("rax").as_concrete(), - state.get_register("rax").as_concrete() + region.frontier, + std::collections::BTreeSet::from([0x1004, 0x1010]) ); - - // Modifications to one shouldn't affect the other - state.set_concrete("rax", 100, 64); - assert_eq!(state.get_register("rax").as_concrete(), Some(100)); - assert_eq!(forked.get_register("rax").as_concrete(), Some(42)); + assert!(region.control.iter().any(|fact| { + fact.value.target == 0x1010 + && fact.value.status == SymbolicReachabilityStatus::Reachable + && fact.value.branch_truth == Some(true) + })); + assert!(region.control.iter().any(|fact| { + fact.value.target == 0x1004 + && fact.value.status == SymbolicReachabilityStatus::Unreachable + && fact.value.branch_truth == Some(false) + })); + assert_eq!(artifact.diagnostics.branches_pruned, 1); } #[test] -fn test_constraint_solving() { - use r2sym::SymSolver; - - let ctx = Context::thread_local(); - - let mut state = SymState::new(&ctx, 0x1000); - state.make_symbolic("x", 32); - - let x = state.get_register("x"); - - // Add constraint: x < 100 - let hundred = SymValue::concrete(100, 32); - let cond = x.ult(&ctx, &hundred); - state.add_true_constraint(&cond); - - // Solve - let solver = SymSolver::new(&ctx); - assert!(solver.is_sat(&state), "Constraints should be satisfiable"); +fn compile_function_semantics_with_scope_prunes_helper_return_dead_branch() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Call { + target: make_const(0x2000, 8), + }], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntNotEqual { + dst: make_reg(TMP0, 1), + a: make_reg(RAX, 8), + b: make_const(0, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP0, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(RAX, 8), + a: make_reg(RDI, 8), + b: make_reg(RDI, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; - let model = solver.solve(&state); - assert!(model.is_some(), "Should get a model"); + let root = + SsaArtifact::for_decompile(&root_blocks, Some(&arch)).expect("root decompile function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_zero".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); - // The model should give us a value for x that is < 100 - let model = model.unwrap(); - if let Some(x_val) = model.eval(&x) { - assert!(x_val < 100, "x should be less than 100, got {}", x_val); - } + let ctx = Context::thread_local(); + let artifact = compile_function_semantics_with_scope( + &ctx, + &root, + Some(&scope), + Some(&arch), + &std::collections::HashMap::new(), + r2sym::SummaryProfile::Default, + ); + let region = region_for_anchor(&artifact, 0x1004); + assert_eq!( + region.frontier, + std::collections::BTreeSet::from([0x1008, 0x1010]) + ); + assert!(region.control.iter().any(|fact| { + fact.value.target == 0x1010 + && fact.value.status == SymbolicReachabilityStatus::Unreachable + && fact.value.branch_truth == Some(true) + })); + assert!(region.control.iter().any(|fact| { + fact.value.target == 0x1008 + && fact.value.status == SymbolicReachabilityStatus::Reachable + && fact.value.branch_truth == Some(false) + })); + assert_eq!(artifact.diagnostics.branches_pruned, 1); } #[test] -fn test_unsatisfiable_constraints() { - use r2sym::SymSolver; +fn compile_function_semantics_with_scope_marks_helper_scope_compiled() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Call { + target: make_const(0x2000, 8), + }], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntNotEqual { + dst: make_reg(TMP0, 1), + a: make_reg(RAX, 8), + b: make_const(0, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP0, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(RAX, 8), + a: make_reg(RDI, 8), + b: make_reg(RDI, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; - let ctx = Context::thread_local(); + let root = + SsaArtifact::for_decompile(&root_blocks, Some(&arch)).expect("root decompile function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_zero".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); - let mut state = SymState::new(&ctx, 0x1000); - state.make_symbolic("x", 32); + let ctx = Context::thread_local(); + let compiled = compile_function_semantics_with_scope( + &ctx, + &root, + Some(&scope), + Some(&arch), + &std::collections::HashMap::new(), + r2sym::SummaryProfile::Default, + ); - let x = state.get_register("x"); + let summary = &compiled + .native_body() + .expect("native artifact body") + .summary; - // Add contradictory constraints: x > 100 AND x < 50 - let hundred = SymValue::concrete(100, 32); - let fifty = SymValue::concrete(50, 32); + assert_eq!(compiled.stage, RefinementStage::Compiled); + assert_eq!(summary.closure_functions, 2); + assert_eq!(summary.helper_functions, 1); + assert!(summary.derived_summaries >= 1); + assert_eq!(compiled.diagnostics.branches_pruned, 1); +} - // x > 100 (equivalent to 100 < x, or NOT(x <= 100)) - let gt_100 = hundred.ult(&ctx, &x); - state.add_true_constraint(>_100); +#[test] +fn compile_function_semantics_with_scope_prunes_helper_return_spilled_dead_branch() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Call { + target: make_const(0x2000, 8), + }], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Store { + space: SpaceId::Ram, + addr: Varnode { + space: SpaceId::Ram, + offset: 0x4000, + size: 8, + meta: None, + }, + val: make_reg(RAX, 8), + }, + R2ILOp::Load { + dst: make_reg(TMP0, 8), + space: SpaceId::Ram, + addr: Varnode { + space: SpaceId::Ram, + offset: 0x4000, + size: 8, + meta: None, + }, + }, + R2ILOp::IntNotEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 8), + b: make_const(0, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(RAX, 8), + a: make_reg(RDI, 8), + b: make_reg(RDI, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; - // x < 50 - let lt_50 = x.ult(&ctx, &fifty); - state.add_true_constraint(<_50); + let root = + SsaArtifact::for_decompile(&root_blocks, Some(&arch)).expect("root decompile function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_zero".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); - // Should be unsatisfiable - let solver = SymSolver::new(&ctx); - assert!( - !solver.is_sat(&state), - "Contradictory constraints should be unsatisfiable" + let ctx = Context::thread_local(); + let artifact = compile_function_semantics_with_scope( + &ctx, + &root, + Some(&scope), + Some(&arch), + &std::collections::HashMap::new(), + r2sym::SummaryProfile::Default, + ); + let region = region_for_anchor(&artifact, 0x1004); + assert_eq!( + region.frontier, + std::collections::BTreeSet::from([0x1008, 0x1010]) ); + assert!(region.control.iter().any(|fact| { + fact.value.target == 0x1010 + && fact.value.status == SymbolicReachabilityStatus::Unreachable + && fact.value.branch_truth == Some(true) + })); + assert!(region.control.iter().any(|fact| { + fact.value.target == 0x1008 + && fact.value.status == SymbolicReachabilityStatus::Reachable + && fact.value.branch_truth == Some(false) + })); + assert_eq!(artifact.diagnostics.branches_pruned, 1); } #[test] -fn test_explore_config() { - let config = ExploreConfig::default(); - assert_eq!(config.max_states, 1000); - assert_eq!(config.max_depth, 100); - assert!(config.prune_infeasible); - assert!(!config.merge_states); - assert_eq!(config.strategy, ExploreStrategy::Dfs); +fn compile_function_semantics_with_scope_prunes_helper_return_dead_branch_via_eax_alias() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Call { + target: make_const(0x2000, 8), + }], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(TMP0, 1), + a: make_reg(RAX, 4), + b: make_const(0, 4), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP0, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(RAX, 8), + a: make_reg(RDI, 8), + b: make_reg(RDI, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_decompile(&root_blocks, Some(&arch)).expect("root decompile function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_zero".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let artifact = compile_function_semantics_with_scope( + &ctx, + &root, + Some(&scope), + Some(&arch), + &std::collections::HashMap::new(), + r2sym::SummaryProfile::Default, + ); + let region = region_for_anchor(&artifact, 0x1004); + assert_eq!( + region.frontier, + std::collections::BTreeSet::from([0x1008, 0x1010]) + ); + assert!(region.control.iter().any(|fact| { + fact.value.target == 0x1010 + && fact.value.status == SymbolicReachabilityStatus::Reachable + && fact.value.branch_truth == Some(true) + })); + assert!(region.control.iter().any(|fact| { + fact.value.target == 0x1008 + && fact.value.status == SymbolicReachabilityStatus::Unreachable + && fact.value.branch_truth == Some(false) + })); + assert_eq!(artifact.diagnostics.branches_pruned, 1); } #[test] -fn test_path_result_properties() { - use r2sym::PathResult; - +fn test_query_summarize_function_marks_budget_exhausted() { + let blocks = make_conditional_branch_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); let ctx = Context::thread_local(); let mut state = SymState::new(&ctx, 0x1000); - state.set_register("rax", SymValue::concrete(42, 64)); - state.make_symbolic("rbx", 64); - - let result = PathResult::new(state, true); + state.make_symbolic("reg:56_0", 64); - assert_eq!(result.final_pc(), 0x1000); - assert_eq!(result.num_constraints(), 0); - assert!(result.register_names().contains(&"rax".to_string())); - assert_eq!(result.get_concrete_register("rax"), Some(42)); - assert!(result.is_register_symbolic("rbx")); + let query_config = SymQueryConfig { + explore: ExploreConfig { + max_states: 0, + ..ExploreConfig::default() + }, + ..SymQueryConfig::default() + }; + let mut explorer = query_config.make_explorer(&ctx); + let summary = explorer.summarize_function(&func, state); + assert_eq!(summary.completion, QueryCompletion::BudgetExhausted); } #[test] -fn test_different_bitwidth_operations() { - // Test that operations with different bit widths work correctly +fn test_find_paths_to_collects_multiple_matches() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(0x1337, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1010, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); - // 8-bit and 64-bit values - let val8 = SymValue::concrete(5, 8); - let val64 = SymValue::concrete(10, 64); + let mut explorer = PathExplorer::new(&ctx); + let paths = explorer.find_paths_to(&func, state, 0x1010); + assert!( + paths.len() >= 2, + "Expected multiple target-reaching paths, got {}", + paths.len() + ); +} - // Should handle mismatch gracefully - let result = val8.add(&ctx, &val64); - assert_eq!(result.as_concrete(), Some(15)); - assert_eq!(result.bits(), 64); // Result uses larger width +#[test] +fn test_find_paths_to_unreachable_returns_empty() { + let blocks = vec![R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; - // Symbolic with different widths - let sym8 = SymValue::new_symbolic(&ctx, "x", 8); - let sym64 = SymValue::new_symbolic(&ctx, "y", 64); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let mut explorer = PathExplorer::new(&ctx); + let paths = explorer.find_paths_to(&func, state, 0x2000); + assert!( + paths.is_empty(), + "Expected no paths for unreachable target, got {}", + paths.len() + ); +} - let sym_result = sym8.add(&ctx, &sym64); - assert!(sym_result.is_symbolic()); - assert_eq!(sym_result.bits(), 64); +#[test] +fn test_find_paths_to_honors_limits() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(0, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let config = ExploreConfig { + max_states: 0, + max_completed_paths: None, + max_depth: 50, + timeout: None, + strategy: ExploreStrategy::Dfs, + prune_infeasible: true, + merge_states: false, + ..ExploreConfig::default() + }; + let mut explorer = PathExplorer::with_config(&ctx, config); + let paths = explorer.find_paths_to(&func, state, 0x1010); + assert!( + paths.is_empty(), + "Expected no matches when max_states=0, got {}", + paths.len() + ); } #[test] -fn memset_summary_writes_bounded_bytes() { +fn test_find_paths_to_with_same_pc_merge_still_reaches_target() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(0x1337, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1010, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); let ctx = Context::thread_local(); let mut state = SymState::new(&ctx, 0x1000); - let summary = MemsetSummary::new(4); - let call = CallInfo { + state.make_symbolic("reg:56_0", 64); + + let config = ExploreConfig { + max_states: 100, + max_completed_paths: None, + max_depth: 50, + timeout: None, + strategy: ExploreStrategy::Bfs, + prune_infeasible: true, + merge_states: true, + ..ExploreConfig::default() + }; + let mut explorer = PathExplorer::with_config(&ctx, config); + let paths = explorer.find_paths_to(&func, state, 0x1010); + + assert!( + !paths.is_empty(), + "Expected at least one feasible merged path to target" + ); + assert!(paths.iter().all(|path| path.final_pc() == 0x1010)); + assert!(paths.iter().all(|path| path.feasible)); +} + +#[test] +fn test_explore_honors_max_completed_paths() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(0x1337, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let config = ExploreConfig { + max_states: 100, + max_completed_paths: Some(1), + max_depth: 50, + timeout: None, + strategy: ExploreStrategy::Bfs, + prune_infeasible: true, + merge_states: false, + ..ExploreConfig::default() + }; + let mut explorer = PathExplorer::with_config(&ctx, config); + let paths = explorer.explore(&func, state); + + assert_eq!( + paths.len(), + 1, + "explore should stop after the configured cap" + ); +} + +#[test] +fn test_symbolic_arithmetic_operations() { + // Test all arithmetic operations with symbolic values + let ctx = Context::thread_local(); + + let _state = SymState::new(&ctx, 0x1000); + + // Create symbolic values + let x = SymValue::new_symbolic(&ctx, "x", 64); + let y = SymValue::new_symbolic(&ctx, "y", 64); + + // Test addition + let sum = x.add(&ctx, &y); + assert!(sum.is_symbolic(), "Sum should be symbolic"); + + // Test subtraction + let diff = x.sub(&ctx, &y); + assert!(diff.is_symbolic(), "Diff should be symbolic"); + + // Test multiplication + let prod = x.mul(&ctx, &y); + assert!(prod.is_symbolic(), "Product should be symbolic"); + + // Test concrete operations + let a = SymValue::concrete(10, 64); + let b = SymValue::concrete(3, 64); + + let sum_concrete = a.add(&ctx, &b); + assert_eq!(sum_concrete.as_concrete(), Some(13)); + + let diff_concrete = a.sub(&ctx, &b); + assert_eq!(diff_concrete.as_concrete(), Some(7)); + + let prod_concrete = a.mul(&ctx, &b); + assert_eq!(prod_concrete.as_concrete(), Some(30)); + + let div_concrete = a.udiv(&ctx, &b); + assert_eq!(div_concrete.as_concrete(), Some(3)); + + let rem_concrete = a.urem(&ctx, &b); + assert_eq!(rem_concrete.as_concrete(), Some(1)); +} + +#[test] +fn test_symbolic_bitwise_operations() { + let ctx = Context::thread_local(); + + // Test concrete bitwise + let a = SymValue::concrete(0b1100, 8); + let b = SymValue::concrete(0b1010, 8); + + assert_eq!(a.and(&ctx, &b).as_concrete(), Some(0b1000)); + assert_eq!(a.or(&ctx, &b).as_concrete(), Some(0b1110)); + assert_eq!(a.xor(&ctx, &b).as_concrete(), Some(0b0110)); + + // Test shifts + let amt = SymValue::concrete(2, 8); + assert_eq!(a.shl(&ctx, &amt).as_concrete(), Some(0b110000)); + assert_eq!(a.lshr(&ctx, &amt).as_concrete(), Some(0b0011)); + + // Test symbolic bitwise + let x = SymValue::new_symbolic(&ctx, "x", 64); + let y = SymValue::new_symbolic(&ctx, "y", 64); + + assert!(x.and(&ctx, &y).is_symbolic()); + assert!(x.or(&ctx, &y).is_symbolic()); + assert!(x.xor(&ctx, &y).is_symbolic()); +} + +#[test] +fn test_symbolic_comparisons() { + let ctx = Context::thread_local(); + + let a = SymValue::concrete(10, 32); + let b = SymValue::concrete(20, 32); + + // Equality + assert_eq!(a.eq(&ctx, &a).as_concrete(), Some(1)); + assert_eq!(a.eq(&ctx, &b).as_concrete(), Some(0)); + + // Unsigned less than + assert_eq!(a.ult(&ctx, &b).as_concrete(), Some(1)); + assert_eq!(b.ult(&ctx, &a).as_concrete(), Some(0)); + + // Unsigned less than or equal + assert_eq!(a.ule(&ctx, &b).as_concrete(), Some(1)); + assert_eq!(a.ule(&ctx, &a).as_concrete(), Some(1)); + assert_eq!(b.ule(&ctx, &a).as_concrete(), Some(0)); +} + +#[test] +fn test_symbolic_memory_operations() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + + // Write concrete value to concrete address + let addr = SymValue::concrete(0x2000, 64); + let value = SymValue::concrete(0xDEADBEEF, 32); + state.mem_write(&addr, &value, 4); + + // Read it back + let read_value = state.mem_read(&addr, 4); + assert_eq!(read_value.as_concrete(), Some(0xDEADBEEF)); + + // Write symbolic value + let sym_value = SymValue::new_symbolic(&ctx, "mem_data", 64); + let addr2 = SymValue::concrete(0x3000, 64); + state.mem_write(&addr2, &sym_value, 8); + + // Read symbolic value + let read_sym = state.mem_read(&addr2, 8); + assert!(read_sym.is_symbolic()); +} + +#[test] +fn test_state_forking() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.set_concrete("rax", 42, 64); + state.make_symbolic("rbx", 64); + + // Fork the state + let forked = state.fork(); + + // Original and fork should have same values + assert_eq!(forked.pc, state.pc); + assert_eq!( + forked.get_register("rax").as_concrete(), + state.get_register("rax").as_concrete() + ); + + // Modifications to one shouldn't affect the other + state.set_concrete("rax", 100, 64); + assert_eq!(state.get_register("rax").as_concrete(), Some(100)); + assert_eq!(forked.get_register("rax").as_concrete(), Some(42)); +} + +#[test] +fn test_constraint_solving() { + use r2sym::SymSolver; + + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + + let x = state.get_register("x"); + + // Add constraint: x < 100 + let hundred = SymValue::concrete(100, 32); + let cond = x.ult(&ctx, &hundred); + state.add_true_constraint(&cond); + + // Solve + let solver = SymSolver::new(&ctx); + assert!(solver.is_sat(&state), "Constraints should be satisfiable"); + + let model = solver.solve(&state); + assert!(model.is_some(), "Should get a model"); + + // The model should give us a value for x that is < 100 + let model = model.unwrap(); + if let Some(x_val) = model.eval(&x) { + assert!(x_val < 100, "x should be less than 100, got {}", x_val); + } +} + +#[test] +fn test_unsatisfiable_constraints() { + use r2sym::SymSolver; + + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + + let x = state.get_register("x"); + + // Add contradictory constraints: x > 100 AND x < 50 + let hundred = SymValue::concrete(100, 32); + let fifty = SymValue::concrete(50, 32); + + // x > 100 (equivalent to 100 < x, or NOT(x <= 100)) + let gt_100 = hundred.ult(&ctx, &x); + state.add_true_constraint(>_100); + + // x < 50 + let lt_50 = x.ult(&ctx, &fifty); + state.add_true_constraint(<_50); + + // Should be unsatisfiable + let solver = SymSolver::new(&ctx); + assert!( + !solver.is_sat(&state), + "Contradictory constraints should be unsatisfiable" + ); +} + +#[test] +fn test_explore_config() { + let config = ExploreConfig::default(); + assert_eq!(config.max_states, 1000); + assert_eq!(config.max_depth, 100); + assert!(config.prune_infeasible); + assert!(!config.merge_states); + assert_eq!(config.strategy, ExploreStrategy::Dfs); +} + +#[test] +fn test_path_result_properties() { + use r2sym::PathResult; + + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.set_register("rax", SymValue::concrete(42, 64)); + state.make_symbolic("rbx", 64); + + let result = PathResult::new(state, true); + + assert_eq!(result.final_pc(), 0x1000); + assert_eq!(result.num_constraints(), 0); + assert!(result.register_names().contains(&"rax".to_string())); + assert_eq!(result.get_concrete_register("rax"), Some(42)); + assert!(result.is_register_symbolic("rbx")); +} + +#[test] +fn test_different_bitwidth_operations() { + // Test that operations with different bit widths work correctly + let ctx = Context::thread_local(); + + // 8-bit and 64-bit values + let val8 = SymValue::concrete(5, 8); + let val64 = SymValue::concrete(10, 64); + + // Should handle mismatch gracefully + let result = val8.add(&ctx, &val64); + assert_eq!(result.as_concrete(), Some(15)); + assert_eq!(result.bits(), 64); // Result uses larger width + + // Symbolic with different widths + let sym8 = SymValue::new_symbolic(&ctx, "x", 8); + let sym64 = SymValue::new_symbolic(&ctx, "y", 64); + + let sym_result = sym8.add(&ctx, &sym64); + assert!(sym_result.is_symbolic()); + assert_eq!(sym_result.bits(), 64); +} + +#[test] +fn memset_summary_writes_bounded_bytes() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let summary = MemsetSummary::new(4); + let call = CallInfo { args: vec![ SymValue::concrete(0x2000, 64), SymValue::concrete(0x41, 64), SymValue::concrete(10, 64), ], - arg_bits: 64, - ret_bits: 64, - }; + arg_bits: 64, + ret_bits: 64, + }; + + let effect = summary.execute(&mut state, &call); + match effect { + r2sym::SummaryEffect::Return(Some(ret)) => { + assert_eq!(ret.as_concrete(), Some(0x2000)); + } + _ => panic!("memset summary should return destination pointer"), + } + + let bytes = state + .memory + .read_bytes(0x2000, 4) + .expect("memset should materialize concrete bytes"); + assert_eq!(bytes, vec![0x41, 0x41, 0x41, 0x41]); + assert!( + state.memory.read_bytes(0x2004, 1).is_none(), + "memset should be bounded by configured max" + ); +} + +#[test] +fn memcmp_summary_returns_trivalued_result() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let summary = MemcmpSummary::new(32); + let call = CallInfo { + args: vec![ + SymValue::concrete(0x3000, 64), + SymValue::concrete(0x4000, 64), + SymValue::concrete(8, 64), + ], + arg_bits: 64, + ret_bits: 64, + }; + + let ret = match summary.execute(&mut state, &call) { + r2sym::SummaryEffect::Return(Some(ret)) => ret, + _ => panic!("memcmp summary should return a value"), + }; + + let solver = SymSolver::new(&ctx); + for allowed in [u64::MAX, 0, 1] { + let mut candidate = state.fork(); + candidate.constrain_eq(&ret, allowed); + assert!( + solver.is_sat(&candidate), + "memcmp return should allow value {allowed:#x}" + ); + } + + let mut disallowed = state.fork(); + disallowed.constrain_eq(&ret, 2); + assert!( + !solver.is_sat(&disallowed), + "memcmp return must be constrained to -1/0/1" + ); +} + +#[test] +fn printf_summary_does_not_terminate_path() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let summary = PrintfSummaryBasic::new(32); + let call = CallInfo { + args: vec![SymValue::concrete(0x5000, 64)], + arg_bits: 64, + ret_bits: 64, + }; + + let effect = summary.execute(&mut state, &call); + assert!(matches!(effect, r2sym::SummaryEffect::Return(Some(_)))); + assert!( + state.active, + "printf summary should not terminate the state" + ); +} + +#[test] +fn puts_summary_does_not_terminate_path() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let summary = PutsSummary::new(32); + let call = CallInfo { + args: vec![SymValue::concrete(0x6000, 64)], + arg_bits: 64, + ret_bits: 64, + }; + + let effect = summary.execute(&mut state, &call); + assert!(matches!(effect, r2sym::SummaryEffect::Return(Some(_)))); + assert!(state.active, "puts summary should not terminate the state"); +} + +#[test] +fn registry_with_core_contains_new_summaries() { + let ctx = Context::thread_local(); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let mut explorer = PathExplorer::new(&ctx); + + assert!(registry.install_for_explorer(&mut explorer, 0x1000, "memcmp")); + assert!(registry.install_for_explorer(&mut explorer, 0x1001, "memset")); + assert!(registry.install_for_explorer(&mut explorer, 0x1002, "puts")); + assert!(registry.install_for_explorer(&mut explorer, 0x1003, "printf")); +} + +#[test] +fn run_spec_fd_read_flows_into_load_compare_and_solution() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Copy { + dst: make_reg(RDX, 8), + src: make_const(1, 8), + }, + R2ILOp::Call { + target: make_const(0x5000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x2000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(b'k' as u64, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0x1337, 8), + }], + }, + ]; + + let arch = make_x86_64_arch(); + let func = + SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + let mut initial_state = SymState::new(&ctx, 0x1000); + let mut explorer = PathExplorer::new(&ctx); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + assert!(registry.install_for_explorer(&mut explorer, 0x5000, "read")); + + let spec = ExplorationSpec { + find: vec![PredicateSpec::Address { + addr: AddressValue::Integer(0x1010), + }], + inputs: vec![InputSpec::Fd { + fd: 0, + len: 1, + name: Some("stdin0".to_string()), + alphabet: Some("k".to_string()), + }], + ..Default::default() + }; + spec.apply_to_state(&mut initial_state); + + let result = explorer + .run_spec(&func, initial_state, &spec) + .expect("spec exploration should succeed"); + assert_eq!(result.found_paths.len(), 1, "expected one matching path"); + + let solved = explorer + .solve_path(&result.found_paths[0]) + .expect("found path should solve"); + assert_eq!(solved.final_pc, 0x1010); + assert_eq!( + solved.input_buffers.get("stdin0"), + Some(&vec![b'k']), + "solver should recover the byte that drives the branch" + ); +} + +#[test] +fn symbolic_n_constraints_bounded_for_memset_memcmp() { + let ctx = Context::thread_local(); + + let mut state_memset = SymState::new(&ctx, 0x1000); + let memset = MemsetSummary::new(8); + let n_memset = SymValue::new_symbolic(&ctx, "sym_memset_n", 64); + let call_memset = CallInfo { + args: vec![ + SymValue::concrete(0x7000, 64), + SymValue::concrete(0x7f, 64), + n_memset, + ], + arg_bits: 64, + ret_bits: 64, + }; + let before_memset = state_memset.num_constraints(); + let _ = memset.execute(&mut state_memset, &call_memset); + assert!( + state_memset.num_constraints() > before_memset, + "symbolic memset length should add bounds constraints" + ); + + let mut state_memcmp = SymState::new(&ctx, 0x1000); + let memcmp = MemcmpSummary::new(8); + let n_memcmp = SymValue::new_symbolic(&ctx, "sym_memcmp_n", 64); + let call_memcmp = CallInfo { + args: vec![ + SymValue::concrete(0x7100, 64), + SymValue::concrete(0x7200, 64), + n_memcmp, + ], + arg_bits: 64, + ret_bits: 64, + }; + let before_memcmp = state_memcmp.num_constraints(); + let _ = memcmp.execute(&mut state_memcmp, &call_memcmp); + assert!( + state_memcmp.num_constraints() > before_memcmp, + "symbolic memcmp length should add bounds constraints" + ); +} + +#[test] +fn derived_helper_summary_solves_return_transform() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Call { + target: make_const(0x2000, 8), + }], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(TMP0, 1), + a: make_reg(RAX, 8), + b: make_const(0x6a, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP0, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(RAX, 8), + a: make_reg(RDI, 8), + b: make_const(0x55, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_xor".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + r2sym::seed_default_state_for_arch(&mut state, &root, Some(&arch)); + state.make_symbolic_named("RDI_0", "rdi", 64); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let solve = explorer.solve_for_target(&root, state, 0x1010); + assert_eq!(solve.status, SolveStatus::Solved); + let solution = solve.solution.expect("solution"); + assert_eq!(solution.inputs.get("rdi").copied(), Some(0x3f)); +} + +#[test] +fn derived_helper_summary_solves_transitive_return_chain() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Call { + target: make_const(0x2000, 8), + }], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(TMP0, 1), + a: make_reg(RAX, 8), + b: make_const(0x6a, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP0, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + }, + ]; + let helper1_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Call { + target: make_const(0x3000, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + let helper2_blocks = vec![R2ILBlock { + addr: 0x3000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(RAX, 8), + a: make_reg(RDI, 8), + b: make_const(0x55, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper1 = + SsaArtifact::for_symbolic(&helper1_blocks, Some(&arch)).expect("helper1 symbolic function"); + let helper2 = + SsaArtifact::for_symbolic(&helper2_blocks, Some(&arch)).expect("helper2 symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_wrapper".to_string()), + prepared: helper1, + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x3000), + name: Some("helper_xor".to_string()), + prepared: helper2, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + r2sym::seed_default_state_for_arch(&mut state, &root, Some(&arch)); + state.make_symbolic_named("RDI_0", "rdi", 64); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + let wrapper_return = derived + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .and_then(|summary| summary.cases.first()) + .and_then(|case| case.return_value.as_ref()) + .map(|value| value.to_bv(&ctx).to_string()) + .expect("wrapper return relation"); + assert!(wrapper_return.contains("helper_wrapper_arg0")); + assert!(wrapper_return.contains("0055")); + let diagnostics = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let solve = explorer.solve_for_target(&root, state, 0x1010); + assert_eq!(solve.status, SolveStatus::Solved); + assert_eq!( + solve + .compiled_precondition + .as_ref() + .map(|compiled| compiled.precision), + Some(BackwardConditionPrecision::Exact) + ); + let solution = solve.solution.expect("solution"); + assert_eq!(solution.inputs.get("rdi").copied(), Some(0x3f)); + assert_eq!(diagnostics.scc_count, 2); + assert_eq!(diagnostics.max_scc_size, 1); +} + +#[test] +fn derived_helper_summary_preserves_pointer_write_value() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x3000, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 1), + src: make_const(0x41, 1), + }, + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x3000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(0x41, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Store { + addr: make_reg(RDI, 8), + val: make_reg(RSI, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_store".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let mut explorer = PathExplorer::new(&ctx); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let paths = explorer.find_paths_to(&root, state, 0x1010); + assert_eq!(paths.len(), 1); +} + +#[test] +fn derived_helper_summary_compiles_backward_memory_terms() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x3000, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 1), + src: make_const(0x41, 1), + }, + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x3000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(0x41, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Store { + addr: make_reg(RDI, 8), + val: make_reg(RSI, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_store".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + let static_summary = derived + .interproc + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("static helper summary"); + assert!( + static_summary.memory_effects.iter().any(|effect| { + matches!( + effect.location.region, + r2ssa::SummaryMemoryRegion::Arg { index: 0 } + ) && effect + .location + .range + .is_some_and(|range| range.offset_lo == 0 && range.width.unwrap_or(0) >= 1) + }), + "expected static summary to record arg0 write, got {:?}", + static_summary.memory_effects + ); + let helper_summary = derived + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("derived helper summary"); + assert!( + helper_summary.cases.iter().any(|case| { + case.memory_writes + .iter() + .any(|write| write.arg_index == 0 && write.offset == 0 && write.size >= 1) + }), + "expected derived summary to record arg0 write, got {:?}", + helper_summary + .cases + .iter() + .flat_map(|case| case.memory_writes.iter().map(|write| ( + write.arg_index, + write.offset, + write.size + ))) + .collect::>() + ); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert!(!compiled.memory_terms.is_empty()); + assert_argument_memory_term(&compiled.memory_terms[0], 0, 0, 0, true, 1); +} + +#[test] +fn derived_helper_summary_compiles_backward_memory_terms_through_cast_pointer() { + let arch = make_x86_64_arch(); + let cast_ptr = make_reg(0x90, 8); + let loaded = make_reg(0x98, 1); + let cond = make_reg(0xa0, 1); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Cast { + dst: cast_ptr.clone(), + src: make_reg(RDI, 8), + }, + R2ILOp::Load { + dst: loaded.clone(), + addr: cast_ptr, + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: cond.clone(), + a: loaded, + b: make_const(0x41, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond, + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_argument_memory_term(&compiled.memory_terms[0], 0, 0, 0, true, 1); +} + +#[test] +fn derived_helper_summary_compiles_region_backed_global_memory_terms() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 1), + src: make_const(0x41, 1), + }, + R2ILOp::Call { + target: make_const(0x3000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x2000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(0x41, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x3000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Store { + addr: make_reg(RDI, 8), + val: make_reg(RSI, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x3000), + name: Some("helper_store".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.define_memory_region( + r2sym::MemoryRegionKind::Global, + "global_2000", + Some(0x2000), + Some(8), + ); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_region_memory_term( + &compiled.memory_terms[0], + r2sym::MemoryRegionKind::Global, + "global_2000", + 0, + 0, + true, + 1, + ); +} + +#[test] +fn derived_helper_summary_compiles_region_backed_stack_memory_terms() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x7008, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 1), + src: make_const(0x41, 1), + }, + R2ILOp::Call { + target: make_const(0x3000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x7008, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(0x41, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x3000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Store { + addr: make_reg(RDI, 8), + val: make_reg(RSI, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x3000), + name: Some("helper_store".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.define_memory_region( + r2sym::MemoryRegionKind::Stack, + "stack_window", + Some(0x7000), + Some(0x100), + ); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_region_memory_term( + &compiled.memory_terms[0], + r2sym::MemoryRegionKind::Stack, + "stack_window", + 8, + 8, + true, + 1, + ); +} + +#[test] +fn derived_helper_summary_compiles_region_backed_replay_memory_terms() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x9004, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 1), + src: make_const(0x41, 1), + }, + R2ILOp::Call { + target: make_const(0x3000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x9004, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(0x41, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x3000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Store { + addr: make_reg(RDI, 8), + val: make_reg(RSI, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x3000), + name: Some("helper_store".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.define_memory_region( + r2sym::MemoryRegionKind::Replay, + "replay_window", + Some(0x9000), + Some(0x100), + ); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_region_memory_term( + &compiled.memory_terms[0], + r2sym::MemoryRegionKind::Replay, + "replay_window", + 4, + 4, + true, + 1, + ); +} + +#[test] +fn derived_helper_summary_region_alias_falls_back_to_residual() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Call { + target: make_const(0x3000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x2000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(0x42, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x3000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Store { + addr: make_reg(RDI, 8), + val: make_const(0x41, 1), + space: SpaceId::Ram, + }, + R2ILOp::Store { + addr: make_reg(RSI, 8), + val: make_const(0x42, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x3000), + name: Some("helper_alias_store".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.define_memory_region( + r2sym::MemoryRegionKind::Global, + "global_2000", + Some(0x2000), + Some(8), + ); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!( + compiled.precision, + BackwardConditionPrecision::ResidualSearchRequired + ); + assert!(compiled.backward_memory_residual_fallbacks > 0); + assert!(!compiled.memory_terms.is_empty()); + assert_region_memory_term( + &compiled.memory_terms[0], + r2sym::MemoryRegionKind::Global, + "global_2000", + 0, + 0, + true, + 1, + ); +} + +#[test] +fn derived_helper_summary_coalesces_adjacent_byte_writes_into_slice() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x3000, 8), + }, + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 2), + addr: make_const(0x3000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 2), + b: make_const(0x4241, 2), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Store { + addr: make_reg(RDI, 8), + val: make_const(0x41, 1), + space: SpaceId::Ram, + }, + R2ILOp::IntAdd { + dst: make_reg(TMP0, 8), + a: make_reg(RDI, 8), + b: make_const(1, 8), + }, + R2ILOp::Store { + addr: make_reg(TMP0, 8), + val: make_const(0x42, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_store_pair".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + let helper_summary = derived + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("derived helper summary"); + assert!( + helper_summary.cases.iter().any(|case| { + case.memory_writes + .iter() + .any(|write| write.arg_index == 0 && write.offset == 0 && write.size == 2) + }), + "expected derived summary to coalesce adjacent byte writes into one 2-byte slice, got {:?}", + helper_summary + .cases + .iter() + .flat_map(|case| case.memory_writes.iter().map(|write| ( + write.arg_index, + write.offset, + write.size + ))) + .collect::>() + ); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_argument_memory_term(&compiled.memory_terms[0], 0, 0, 0, true, 2); +} + +#[test] +fn derived_helper_summary_compiles_backward_memory_slice_at_offset() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x3000, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 8), + src: make_const(0x4142, 8), + }, + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP1, 2), + addr: make_const(0x3002, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(TMP1, 2), + b: make_const(0x4142, 2), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntAdd { + dst: make_reg(TMP0, 8), + a: make_reg(RDI, 8), + b: make_const(2, 8), + }, + R2ILOp::Store { + addr: make_reg(TMP0, 8), + val: make_reg(RSI, 2), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_store_offset".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + let static_summary = derived + .interproc + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("static helper summary"); + assert!( + static_summary.memory_effects.iter().any(|effect| { + matches!( + effect.location.region, + r2ssa::SummaryMemoryRegion::Arg { index: 0 } + ) && effect + .location + .range + .is_some_and(|range| range.offset_lo == 2 && range.width.unwrap_or(0) >= 2) + }), + "expected static summary to record arg0+2 write, got {:?}", + static_summary.memory_effects + ); + let helper_summary = derived + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("derived helper summary"); + assert!( + helper_summary.cases.iter().any(|case| { + case.memory_writes + .iter() + .any(|write| write.arg_index == 0 && write.offset == 2 && write.size >= 2) + }), + "expected derived summary to record arg0+2 write" + ); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_argument_memory_term(&compiled.memory_terms[0], 0, 2, 2, true, 2); +} + +#[test] +fn derived_helper_summary_compiles_backward_memory_slice_via_ptradd() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x3000, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 8), + src: make_const(0x4142, 8), + }, + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP1, 2), + addr: make_const(0x3002, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(TMP1, 2), + b: make_const(0x4142, 2), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::PtrAdd { + dst: make_reg(TMP0, 8), + base: make_reg(RDI, 8), + index: make_const(1, 8), + element_size: 2, + }, + R2ILOp::Store { + addr: make_reg(TMP0, 8), + val: make_reg(RSI, 2), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_store_ptradd".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + let helper_summary = derived + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("derived helper summary"); + assert!( + helper_summary.cases.iter().any(|case| { + case.memory_writes + .iter() + .any(|write| write.arg_index == 0 && write.offset == 2 && write.size >= 2) + }), + "expected derived summary to record arg0+2 write" + ); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_argument_memory_term(&compiled.memory_terms[0], 0, 2, 2, true, 2); +} + +#[test] +fn derived_helper_summary_preserves_negative_pointer_write_value() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x3001, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 1), + src: make_const(0x41, 1), + }, + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x3000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(0x41, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntSub { + dst: make_reg(TMP0, 8), + a: make_reg(RDI, 8), + b: make_const(1, 8), + }, + R2ILOp::Store { + addr: make_reg(TMP0, 8), + val: make_reg(RSI, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; - let effect = summary.execute(&mut state, &call); - match effect { - r2sym::SummaryEffect::Return(Some(ret)) => { - assert_eq!(ret.as_concrete(), Some(0x2000)); - } - _ => panic!("memset summary should return destination pointer"), - } + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_store_prev".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); - let bytes = state - .memory - .read_bytes(0x2000, 4) - .expect("memset should materialize concrete bytes"); - assert_eq!(bytes, vec![0x41, 0x41, 0x41, 0x41]); + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + let static_summary = derived + .interproc + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("static helper summary"); assert!( - state.memory.read_bytes(0x2004, 1).is_none(), - "memset should be bounded by configured max" + static_summary.memory_effects.iter().any(|effect| { + matches!( + effect.location.region, + r2ssa::SummaryMemoryRegion::Arg { index: 0 } + ) && effect + .location + .range + .is_some_and(|range| range.offset_lo == -1 && range.width.unwrap_or(0) >= 1) + }), + "expected static summary to record arg0-1 write, got {:?}", + static_summary.memory_effects + ); + let helper_summary = derived + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("derived helper summary"); + assert!( + helper_summary.cases.iter().any(|case| { + case.memory_writes + .iter() + .any(|write| write.arg_index == 0 && write.offset == -1 && write.size >= 1) + }), + "expected derived summary to record arg0-1 write, got {:?}", + helper_summary + .cases + .iter() + .flat_map(|case| case.memory_writes.iter().map(|write| ( + write.arg_index, + write.offset, + write.size + ))) + .collect::>() + ); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), ); + + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_argument_memory_term(&compiled.memory_terms[0], 0, -1, -1, true, 1); + + let paths = explorer.find_paths_to(&root, SymState::new(&ctx, 0x1000), 0x1010); + assert_eq!(paths.len(), 1); } #[test] -fn memcmp_summary_returns_trivalued_result() { - let ctx = Context::thread_local(); - let mut state = SymState::new(&ctx, 0x1000); - let summary = MemcmpSummary::new(32); - let call = CallInfo { - args: vec![ - SymValue::concrete(0x3000, 64), - SymValue::concrete(0x4000, 64), - SymValue::concrete(8, 64), +fn derived_helper_summary_compiles_backward_memory_slice_via_ptrsub() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x3002, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 8), + src: make_const(0x4142, 8), + }, + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP1, 2), + addr: make_const(0x3000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(TMP1, 2), + b: make_const(0x4142, 2), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::PtrSub { + dst: make_reg(TMP0, 8), + base: make_reg(RDI, 8), + index: make_const(1, 8), + element_size: 2, + }, + R2ILOp::Store { + addr: make_reg(TMP0, 8), + val: make_reg(RSI, 2), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, ], - arg_bits: 64, - ret_bits: 64, - }; - - let ret = match summary.execute(&mut state, &call) { - r2sym::SummaryEffect::Return(Some(ret)) => ret, - _ => panic!("memcmp summary should return a value"), - }; - - let solver = SymSolver::new(&ctx); - for allowed in [u64::MAX, 0, 1] { - let mut candidate = state.fork(); - candidate.constrain_eq(&ret, allowed); - assert!( - solver.is_sat(&candidate), - "memcmp return should allow value {allowed:#x}" - ); - } + }]; - let mut disallowed = state.fork(); - disallowed.constrain_eq(&ret, 2); - assert!( - !solver.is_sat(&disallowed), - "memcmp return must be constrained to -1/0/1" - ); -} + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_store_ptrsub".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); -#[test] -fn printf_summary_does_not_terminate_path() { let ctx = Context::thread_local(); - let mut state = SymState::new(&ctx, 0x1000); - let summary = PrintfSummaryBasic::new(32); - let call = CallInfo { - args: vec![SymValue::concrete(0x5000, 64)], - arg_bits: 64, - ret_bits: 64, - }; - - let effect = summary.execute(&mut state, &call); - assert!(matches!(effect, r2sym::SummaryEffect::Return(Some(_)))); + let state = SymState::new(&ctx, 0x1000); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + let helper_summary = derived + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("derived helper summary"); assert!( - state.active, - "printf summary should not terminate the state" + helper_summary.cases.iter().any(|case| { + case.memory_writes + .iter() + .any(|write| write.arg_index == 0 && write.offset == -2 && write.size >= 2) + }), + "expected derived summary to record arg0-2 write" ); -} -#[test] -fn puts_summary_does_not_terminate_path() { - let ctx = Context::thread_local(); - let mut state = SymState::new(&ctx, 0x1000); - let summary = PutsSummary::new(32); - let call = CallInfo { - args: vec![SymValue::concrete(0x6000, 64)], - arg_bits: 64, - ret_bits: 64, - }; + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); - let effect = summary.execute(&mut state, &call); - assert!(matches!(effect, r2sym::SummaryEffect::Return(Some(_)))); - assert!(state.active, "puts summary should not terminate the state"); + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_argument_memory_term(&compiled.memory_terms[0], 0, -2, -2, true, 2); } #[test] -fn registry_with_core_contains_new_summaries() { +fn derived_helper_summary_compiles_backward_return_precondition() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Call { + target: make_const(0x2000, 8), + }], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(RAX, 8), + a: make_reg(RDI, 8), + b: make_const(0x55, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root, + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_xor".to_string()), + prepared: helper.clone(), + }, + ], + ) + .expect("scope"); + let ctx = Context::thread_local(); let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); - let mut explorer = PathExplorer::new(&ctx); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + let summary = derived + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("derived summary"); + let mut state = SymState::new(&ctx, 0x2000); + r2sym::seed_default_state_for_arch(&mut state, &helper, Some(&arch)); + state.make_symbolic_named("RDI_0", "rdi", 64); - assert!(registry.install_for_explorer(&mut explorer, 0x1000, "memcmp")); - assert!(registry.install_for_explorer(&mut explorer, 0x1001, "memset")); - assert!(registry.install_for_explorer(&mut explorer, 0x1002, "puts")); - assert!(registry.install_for_explorer(&mut explorer, 0x1003, "printf")); + let compiled = compile_derived_summary_return_postcondition( + &state, + summary, + &r2sym::CallConv::x86_64_sysv(), + |ret| { + ret.eq(&ctx, &SymValue::concrete(0x6a, 64)) + .to_bv(&ctx) + .eq(z3::ast::BV::from_u64(1, 1)) + }, + ) + .expect("compiled backward precondition"); + + assert_eq!( + compiled.summary.precision, + BackwardConditionPrecision::Exact + ); + let solver = SymSolver::new(&ctx); + assert_eq!( + solver.sat_with_constraint(&state, &compiled.predicate), + r2sym::SatResult::Sat + ); } #[test] -fn symbolic_n_constraints_bounded_for_memset_memcmp() { - let ctx = Context::thread_local(); +fn derived_helper_summary_reports_recursive_scc_diagnostics() { + let arch = make_x86_64_arch(); + let root_blocks = vec![R2ILBlock { + addr: 0x1000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + let helper_a_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Call { + target: make_const(0x3000, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + let helper_b_blocks = vec![R2ILBlock { + addr: 0x3000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; - let mut state_memset = SymState::new(&ctx, 0x1000); - let memset = MemsetSummary::new(8); - let n_memset = SymValue::new_symbolic(&ctx, "sym_memset_n", 64); - let call_memset = CallInfo { - args: vec![ - SymValue::concrete(0x7000, 64), - SymValue::concrete(0x7f, 64), - n_memset, + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper_a = SsaArtifact::for_symbolic(&helper_a_blocks, Some(&arch)) + .expect("helper_a symbolic function"); + let helper_b = SsaArtifact::for_symbolic(&helper_b_blocks, Some(&arch)) + .expect("helper_b symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root, + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_a".to_string()), + prepared: helper_a, + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x3000), + name: Some("helper_b".to_string()), + prepared: helper_b, + }, ], - arg_bits: 64, - ret_bits: 64, - }; - let before_memset = state_memset.num_constraints(); - let _ = memset.execute(&mut state_memset, &call_memset); - assert!( - state_memset.num_constraints() > before_memset, - "symbolic memset length should add bounds constraints" + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), ); - let mut state_memcmp = SymState::new(&ctx, 0x1000); - let memcmp = MemcmpSummary::new(8); - let n_memcmp = SymValue::new_symbolic(&ctx, "sym_memcmp_n", 64); - let call_memcmp = CallInfo { - args: vec![ - SymValue::concrete(0x7100, 64), - SymValue::concrete(0x7200, 64), - n_memcmp, - ], - arg_bits: 64, - ret_bits: 64, - }; - let before_memcmp = state_memcmp.num_constraints(); - let _ = memcmp.execute(&mut state_memcmp, &call_memcmp); + assert_eq!(derived.diagnostics.scc_count, 1); + assert_eq!(derived.diagnostics.max_scc_size, 2); + assert_eq!( + derived.diagnostics.scc_converged + derived.diagnostics.scc_budget_exhausted, + 1 + ); assert!( - state_memcmp.num_constraints() > before_memcmp, - "symbolic memcmp length should add bounds constraints" + derived + .summaries + .contains_key(&r2ssa::InterprocFunctionId(0x2000)) + ); + assert!( + derived + .summaries + .contains_key(&r2ssa::InterprocFunctionId(0x3000)) ); } diff --git a/crates/r2types/Cargo.toml b/crates/r2types/Cargo.toml index 5be5aee..a3b8279 100644 --- a/crates/r2types/Cargo.toml +++ b/crates/r2types/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true description = "Constraint-based type inference primitives for r2sleigh" [dependencies] +r2sym = { path = "../r2sym" } r2ssa = { path = "../r2ssa" } serde.workspace = true serde_json.workspace = true diff --git a/crates/r2types/src/context.rs b/crates/r2types/src/context.rs index 9b71f4d..f1f0a16 100644 --- a/crates/r2types/src/context.rs +++ b/crates/r2types/src/context.rs @@ -8,6 +8,7 @@ use crate::external::{ normalize_external_type_name, }; use crate::facts::{FunctionParamSpec, FunctionSignatureSpec, FunctionType, parse_type_like_spec}; +use crate::signature_infer::render_signature_type; #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ExternalRegisterParamSpec { @@ -39,6 +40,7 @@ pub struct ParsedExternalContext { // Legacy compatibility view derived from canonical stack_slots. pub external_stack_vars: HashMap, pub external_type_db: ExternalTypeDb, + pub assumptions: r2ssa::AssumptionSet, pub diagnostics: Vec, pub callconv: Option, pub noreturn: bool, @@ -206,6 +208,8 @@ pub struct ExternalContextJson { pub base_types: Vec, #[serde(default)] pub known_signatures: Vec, + #[serde(default)] + pub assumptions: Vec, } pub fn normalize_function_basename(name: &str) -> String { @@ -338,10 +342,79 @@ pub fn parse_external_context_json(json_str: &str, ptr_bits: u32) -> ParsedExter parsed.current_signature.clone(), &parsed.register_params, ); + parsed.assumptions = imported_assumptions_from_context( + &raw.assumptions, + &parsed.register_params, + &parsed.stack_slots, + &parsed.external_type_db, + ptr_bits, + ); parsed } +fn imported_assumptions_from_context( + explicit: &[r2ssa::AnalysisAssumption], + register_params: &[ExternalRegisterParamSpec], + stack_slots: &BTreeMap, + type_db: &ExternalTypeDb, + ptr_bits: u32, +) -> r2ssa::AssumptionSet { + let mut assumptions = r2ssa::AssumptionSet::new(explicit.to_vec()); + let mut maybe_push_type_hints = |subject: r2ssa::AssumptionSubject, ty: &CTypeLike| { + let ty_name = render_signature_type(ty, ptr_bits); + assumptions.push(r2ssa::AnalysisAssumption { + id: None, + subject: subject.clone(), + value: r2ssa::AssumptionValue::TypeHint { ty: ty_name }, + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::ImportedContext, + }); + if let CTypeLike::Enum(name) = ty + && let Some(external_enum) = type_db.enums.get(name) + { + assumptions.push(r2ssa::AnalysisAssumption { + id: None, + subject, + value: r2ssa::AssumptionValue::EnumDomain { + name: Some(external_enum.name.clone()), + values: external_enum.variants.keys().copied().collect(), + }, + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::ImportedContext, + }); + } + }; + for reg in register_params { + if let Some(ty) = reg.ty.as_ref() { + maybe_push_type_hints( + r2ssa::AssumptionSubject::Register { + name: reg.reg.clone(), + }, + ty, + ); + } + } + for (slot_key, slot) in stack_slots { + if let Some(ty) = slot.ty.as_ref() { + maybe_push_type_hints( + r2ssa::AssumptionSubject::StackSlot { + base: slot_key + .base + .legacy_name() + .unwrap_or_else(|| "stack".to_string()), + offset: slot_key.offset, + }, + ty, + ); + if let Some(index) = slot.param_index { + maybe_push_type_hints(r2ssa::AssumptionSubject::Parameter { index }, ty); + } + } + } + assumptions +} + fn parse_signature_json( signature: &ExternalSignatureJson, ptr_bits: u32, diff --git a/crates/r2types/src/facts.rs b/crates/r2types/src/facts.rs index 2872cb5..b64bd85 100644 --- a/crates/r2types/src/facts.rs +++ b/crates/r2types/src/facts.rs @@ -20,6 +20,7 @@ pub struct LocalFieldAccessFact { pub slot: usize, pub field_offset: u64, pub field_name: String, + pub field_type: Option, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -320,7 +321,12 @@ fn merge_local_field_accesses( .entry(access.slot) .or_default() .entry(access.field_offset) - .or_insert_with(|| access.field_name.clone()); + .or_insert_with(|| { + access + .field_type + .clone() + .unwrap_or_else(|| access.field_name.clone()) + }); } } @@ -483,11 +489,13 @@ mod tests { slot: 1, field_offset: 0, field_name: "first".to_string(), + field_type: None, }, LocalFieldAccessFact { slot: 1, field_offset: 8, field_name: "second".to_string(), + field_type: None, }, ], ..FunctionTypeFactInputs::default() @@ -521,6 +529,7 @@ mod tests { slot: 2, field_offset: 0, field_name: "local".to_string(), + field_type: None, }], ..FunctionTypeFactInputs::default() }) @@ -535,6 +544,28 @@ mod tests { ); } + #[test] + fn builder_prefers_local_field_access_type_when_present() { + let facts = FunctionTypeFacts::builder(FunctionTypeFactInputs { + local_field_accesses: vec![LocalFieldAccessFact { + slot: 3, + field_offset: 4, + field_name: "f_4".to_string(), + field_type: Some("int32_t".to_string()), + }], + ..FunctionTypeFactInputs::default() + }) + .build(); + + assert_eq!( + facts + .slot_field_profiles + .get(&3) + .and_then(|profile| profile.get(&4)), + Some(&"int32_t".to_string()) + ); + } + #[test] fn builder_merges_external_diagnostics_once() { let external = ExternalTypeDb { diff --git a/crates/r2types/src/function_facts.rs b/crates/r2types/src/function_facts.rs new file mode 100644 index 0000000..9c4e8e9 --- /dev/null +++ b/crates/r2types/src/function_facts.rs @@ -0,0 +1,374 @@ +use serde::{Deserialize, Serialize}; + +use crate::facts::FunctionTypeFacts; + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct AnalysisPlans { + #[serde(skip_serializing_if = "Option::is_none")] + pub artifact_build: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub query: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub type_plan: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub decompile: Option, +} + +impl AnalysisPlans { + pub fn from_semantics(semantics: Option<&r2sym::SemanticArtifact>) -> Self { + let Some(semantics) = semantics else { + return Self::default(); + }; + Self { + artifact_build: Some(semantics.build_plan()), + query: Some(semantics.query_plan()), + type_plan: Some(semantics.type_plan()), + decompile: Some(semantics.decompile_plan()), + } + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct InterprocSummaryView { + #[serde(skip_serializing_if = "Option::is_none")] + pub set: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub rollup: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub helpers: Vec, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct SummaryEffectRollup { + #[serde(skip_serializing_if = "Option::is_none")] + pub root_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub root_return_relation: Option, + #[serde(default)] + pub out_param_indices: Vec, + #[serde(default)] + pub pointer_param_indices: Vec, + pub helper_summary_count: usize, + pub has_unknown_calls: bool, + pub touches_unknown_memory: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SummaryHelperView { + pub function_id: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub arg_count_hint: Option, + pub return_relation: r2ssa::SummaryReturnRelation, + #[serde(default)] + pub out_param_indices: Vec, + #[serde(default)] + pub pointer_param_indices: Vec, + pub has_unknown_calls: bool, + pub touches_unknown_memory: bool, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct DecompileCapabilityView { + #[serde(skip_serializing_if = "Option::is_none")] + pub plan: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub slice_class: Option, + pub skipped_large_cfg: bool, + pub has_native_regions: bool, + pub actionable_region_count: usize, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub ambiguous_targets: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub residual_reasons: Vec, + pub assumption_conflicted: bool, + pub summary_conflicted: bool, +} + +impl InterprocSummaryView { + pub fn new(set: Option) -> Self { + let rollup = summary_rollup(set.as_ref()); + let helpers = helper_views(set.as_ref()); + Self { + set, + rollup, + helpers, + } + } + + pub fn as_set(&self) -> Option<&r2ssa::InterprocSummarySet> { + self.set.as_ref() + } + + pub fn root_summary(&self) -> Option<&r2ssa::FunctionSemanticSummary> { + let set = self.set.as_ref()?; + let root = set.root?; + set.summaries.get(&root) + } + + pub fn diagnostics(&self) -> Option<&r2ssa::InterprocSummaryDiagnostics> { + self.set.as_ref().map(|set| &set.diagnostics) + } + + pub fn helper_summary_for_name(&self, name: &str) -> Option<&r2ssa::FunctionSemanticSummary> { + let normalized = name.trim().to_ascii_lowercase(); + self.set.as_ref()?.summaries.values().find(|summary| { + summary + .name + .as_deref() + .is_some_and(|summary_name| summary_name.trim().to_ascii_lowercase() == normalized) + }) + } + + pub fn helper_view_for_name(&self, name: &str) -> Option<&SummaryHelperView> { + let normalized = name.trim().to_ascii_lowercase(); + self.helpers.iter().find(|summary| { + summary + .name + .as_deref() + .is_some_and(|summary_name| summary_name.trim().to_ascii_lowercase() == normalized) + }) + } + + pub fn out_param_indices(&self) -> &[usize] { + self.rollup + .as_ref() + .map(|rollup| rollup.out_param_indices.as_slice()) + .unwrap_or(&[]) + } + + pub fn pointer_param_indices(&self) -> &[usize] { + self.rollup + .as_ref() + .map(|rollup| rollup.pointer_param_indices.as_slice()) + .unwrap_or(&[]) + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct FunctionFacts { + pub types: FunctionTypeFacts, + pub semantics: Option, + pub assumptions: r2ssa::AssumptionSet, + pub plans: AnalysisPlans, + pub summary_view: InterprocSummaryView, + pub diagnostics: Vec, + pub assumption_usage: r2ssa::AssumptionUsageReport, +} + +impl FunctionFacts { + pub fn new(types: FunctionTypeFacts, semantics: Option) -> Self { + let plans = AnalysisPlans::from_semantics(semantics.as_ref()); + Self { + types, + semantics, + assumptions: r2ssa::AssumptionSet::default(), + plans, + summary_view: InterprocSummaryView::default(), + diagnostics: Vec::new(), + assumption_usage: r2ssa::AssumptionUsageReport::default(), + } + } + + pub fn with_assumptions(mut self, assumptions: r2ssa::AssumptionSet) -> Self { + self.assumptions = assumptions; + self + } + + pub fn with_summary_set(mut self, set: Option) -> Self { + self.summary_view = InterprocSummaryView::new(set); + self + } + + pub fn with_summary_view(mut self, summary_view: InterprocSummaryView) -> Self { + self.summary_view = summary_view; + self + } + + pub fn with_diagnostics(mut self, diagnostics: I) -> Self + where + I: IntoIterator, + { + self.diagnostics = diagnostics.into_iter().collect(); + self + } + + pub fn with_assumption_usage(mut self, usage: r2ssa::AssumptionUsageReport) -> Self { + self.assumption_usage = usage; + self + } + + pub fn set_semantics(&mut self, semantics: Option) { + self.semantics = semantics; + self.refresh_plans(); + } + + pub fn refresh_plans(&mut self) { + self.plans = AnalysisPlans::from_semantics(self.semantics.as_ref()); + } + + pub fn type_plan(&self) -> Option { + self.plans.type_plan.clone() + } + + pub fn decompile_plan(&self) -> Option { + self.plans.decompile.clone() + } + + pub fn query_plan(&self) -> Option { + self.plans.query.clone() + } + + pub fn artifact_build_plan(&self) -> Option { + self.plans.artifact_build.clone() + } + + pub fn interproc_summary_set(&self) -> Option<&r2ssa::InterprocSummarySet> { + self.summary_view.as_set() + } + + pub fn semantic_artifact(&self) -> Option<&r2sym::SemanticArtifact> { + self.semantics.as_ref() + } + + pub fn summary_rollup(&self) -> Option<&SummaryEffectRollup> { + self.summary_view.rollup.as_ref() + } + + pub fn has_assumption_conflicts(&self) -> bool { + !self.assumption_usage.conflicts.is_empty() + } + + pub fn has_applied_assumptions(&self) -> bool { + !self.assumption_usage.applied.is_empty() + } + + pub fn has_summary_conflicts(&self) -> bool { + self.summary_view + .diagnostics() + .is_some_and(|diagnostics| !diagnostics.converged) + } + + pub fn decompile_capability(&self) -> DecompileCapabilityView { + let mut capability = DecompileCapabilityView { + plan: self.decompile_plan(), + assumption_conflicted: self.has_assumption_conflicts(), + summary_conflicted: self.has_summary_conflicts(), + ..DecompileCapabilityView::default() + }; + let Some(semantics) = self.semantic_artifact() else { + return capability; + }; + capability.slice_class = semantics.slice_class(); + capability.skipped_large_cfg = semantics.diagnostics.skipped_large_cfg; + capability.has_native_regions = semantics + .native_body() + .is_some_and(|body| !body.regions.is_empty()); + capability.actionable_region_count = semantics.actionable_regions().len(); + capability.ambiguous_targets = semantics.ambiguous_targets(); + capability.residual_reasons = semantics.diagnostics.residual_reasons.clone(); + capability + } +} + +fn summary_rollup(set: Option<&r2ssa::InterprocSummarySet>) -> Option { + let set = set?; + let root_summary = set.root.and_then(|root| set.summaries.get(&root)); + let mut out_param_indices = root_summary + .map(|summary| { + summary + .arg_effects + .iter() + .filter_map(|(idx, effect)| (effect.write || effect.escape).then_some(*idx)) + .collect::>() + }) + .unwrap_or_default(); + out_param_indices.sort_unstable(); + out_param_indices.dedup(); + + let mut pointer_param_indices = root_summary + .map(|summary| { + let mut indices = summary + .arg_effects + .iter() + .filter_map(|(idx, effect)| { + (effect.read || effect.write || effect.escape || effect.free).then_some(*idx) + }) + .collect::>(); + for effect in &summary.memory_effects { + if let r2ssa::SummaryMemoryRegion::Arg { index } = effect.location.region { + indices.push(index); + } + } + indices + }) + .unwrap_or_default(); + pointer_param_indices.sort_unstable(); + pointer_param_indices.dedup(); + + Some(SummaryEffectRollup { + root_name: root_summary.and_then(|summary| summary.name.clone()), + root_return_relation: root_summary.map(|summary| summary.return_relation.clone()), + out_param_indices, + pointer_param_indices, + helper_summary_count: set + .summaries + .len() + .saturating_sub(usize::from(set.root.is_some())), + has_unknown_calls: root_summary.is_some_and(|summary| summary.has_unknown_calls), + touches_unknown_memory: root_summary.is_some_and(|summary| summary.touches_unknown_memory), + }) +} + +fn helper_views(set: Option<&r2ssa::InterprocSummarySet>) -> Vec { + let Some(set) = set else { + return Vec::new(); + }; + let mut helpers = set + .summaries + .iter() + .filter(|(id, _)| Some(**id) != set.root) + .map(|(id, summary)| { + let mut out_param_indices = summary + .arg_effects + .iter() + .filter_map(|(idx, effect)| (effect.write || effect.escape).then_some(*idx)) + .collect::>(); + out_param_indices.sort_unstable(); + out_param_indices.dedup(); + + let mut pointer_param_indices = summary + .arg_effects + .iter() + .filter_map(|(idx, effect)| { + (effect.read || effect.write || effect.escape || effect.free).then_some(*idx) + }) + .collect::>(); + for effect in &summary.memory_effects { + if let r2ssa::SummaryMemoryRegion::Arg { index } = effect.location.region { + pointer_param_indices.push(index); + } + } + pointer_param_indices.sort_unstable(); + pointer_param_indices.dedup(); + + SummaryHelperView { + function_id: id.0, + name: summary.name.clone(), + arg_count_hint: summary.arg_count_hint, + return_relation: summary.return_relation.clone(), + out_param_indices, + pointer_param_indices, + has_unknown_calls: summary.has_unknown_calls, + touches_unknown_memory: summary.touches_unknown_memory, + } + }) + .collect::>(); + helpers.sort_by(|left, right| { + left.name + .cmp(&right.name) + .then(left.function_id.cmp(&right.function_id)) + }); + helpers +} diff --git a/crates/r2types/src/lib.rs b/crates/r2types/src/lib.rs index 190c53f..40dafd5 100644 --- a/crates/r2types/src/lib.rs +++ b/crates/r2types/src/lib.rs @@ -3,11 +3,14 @@ pub mod context; pub mod convert; pub mod external; pub mod facts; +pub mod function_facts; pub mod inference; pub mod lattice; pub mod model; pub mod oracle; +pub mod prepare; pub mod signature; +pub mod signature_infer; pub mod solver; pub mod writeback; @@ -32,15 +35,39 @@ pub use facts::{ FunctionTypeFactsBuilder, InterprocFactDiagnostics, LocalFieldAccessFact, ResolvedFieldLayout, VisibleBinding, VisibleBindingKind, parse_type_like_spec, }; +pub use function_facts::{ + AnalysisPlans, DecompileCapabilityView, FunctionFacts, InterprocSummaryView, + SummaryEffectRollup, SummaryHelperView, +}; pub use inference::{CombinedTypeOracle, TypeInference}; pub use model::{Signedness, StructField, StructShape, Type, TypeArena, TypeId}; pub use oracle::{LayoutOracle, TypeOracle}; +pub use prepare::{ + ArgAliasMap, BaseRegList, SignatureTypeEvidenceContext, TypeHint, TypeHintRank, X86_ARG_REGS, + X86_FRAME_BASES, collect_pointer_arg_slots, collect_signature_type_evidence_context, + merge_type_hint, recover_vars_arch_profile, recover_vars_from_ssa, scalar_register_family_key, + size_to_type, ssa_var_block_key, ssa_var_key, +}; +pub use r2ssa::AssumptionUsageReport; pub use signature::{ResolvedSignature, SignatureRegistry}; +pub use signature_infer::{ + RecoveredSignatureParam, SignatureParamCandidate, SignatureTypeEvidence, + build_inferred_signature, collect_signature_type_evidence_for_var, collect_version0_input_regs, + compute_callconv_inference, compute_signature_confidence, + enrich_known_function_signatures_from_names, format_afs_signature, + infer_signature_from_prepared_ssa, infer_signature_return_type, + materialize_signature_type_like, merge_initial_signature_type_evidence, + merge_pointer_slot_evidence_into_signature_params, render_signature_type, + resolve_evidence_driven_signature_type, +}; pub use solver::{SolvedTypes, SolverConfig, SolverDiagnostics, TypeSolver}; pub use writeback::{ GlobalTypeLinkCandidate, InferredSignature, InferredSignatureParam, LocalStructArtifacts, RecoveredVariable, StructDeclCandidate, StructDeclSource, StructFieldCandidate, TypeWritebackAnalysis, TypeWritebackAnalysisInput, TypeWritebackDiagnostics, TypeWritebackPlan, - VarRenameCandidate, VarTypeCandidate, WritebackEvidence, WritebackSource, - build_type_writeback_analysis, + TypeWritebackSemanticInputs, VarRenameCandidate, VarTypeCandidate, WritebackEvidence, + WritebackSource, augment_local_struct_artifacts_with_semantics, + build_semantic_type_fallback_plan, build_type_writeback_analysis, + build_type_writeback_analysis_with_semantics, infer_local_struct_artifacts_from_ssa, + semantic_artifact_prefers_bounded_type_plan, }; diff --git a/crates/r2types/src/prepare.rs b/crates/r2types/src/prepare.rs new file mode 100644 index 0000000..572d9b1 --- /dev/null +++ b/crates/r2types/src/prepare.rs @@ -0,0 +1,1250 @@ +use std::collections::{BTreeSet, HashMap, HashSet}; + +use r2ssa::{SSABlock, SSAOp, SSAVar}; + +use crate::writeback::RecoveredVariable; + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct SignatureTypeEvidenceContext { + pub pointer_vars: HashSet, + pub scalar_proven_vars: HashSet, + pub scalar_likely_vars: HashSet, + pub bool_like_vars: HashSet, + pub width_bits: HashMap, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum TypeHintRank { + Integer = 1, + Float = 2, + Pointer = 3, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TypeHint { + pub rank: TypeHintRank, + pub ty: String, +} + +impl TypeHint { + pub fn pointer() -> Self { + Self { + rank: TypeHintRank::Pointer, + ty: "void *".to_string(), + } + } +} + +pub type ArgAliasMap = &'static [(&'static str, &'static [&'static str])]; +pub type BaseRegList = &'static [&'static str]; + +pub const X86_ARG_REGS: &[(&str, &[&str])] = &[ + ("rdi", &["rdi", "edi", "di", "dil"]), + ("rsi", &["rsi", "esi", "si", "sil"]), + ("rdx", &["rdx", "edx", "dx", "dl", "dh"]), + ("rcx", &["rcx", "ecx", "cx", "cl", "ch"]), + ("r8", &["r8", "r8d", "r8w", "r8b"]), + ("r9", &["r9", "r9d", "r9w", "r9b"]), +]; +const RISCV_ARG_REGS: &[(&str, &[&str])] = &[ + ("a0", &["a0", "x10"]), + ("a1", &["a1", "x11"]), + ("a2", &["a2", "x12"]), + ("a3", &["a3", "x13"]), + ("a4", &["a4", "x14"]), + ("a5", &["a5", "x15"]), + ("a6", &["a6", "x16"]), + ("a7", &["a7", "x17"]), +]; +const ARM64_ARG_REGS: &[(&str, &[&str])] = &[ + ("x0", &["x0", "w0"]), + ("x1", &["x1", "w1"]), + ("x2", &["x2", "w2"]), + ("x3", &["x3", "w3"]), + ("x4", &["x4", "w4"]), + ("x5", &["x5", "w5"]), + ("x6", &["x6", "w6"]), + ("x7", &["x7", "w7"]), +]; +const ARM32_ARG_REGS: &[(&str, &[&str])] = &[ + ("r0", &["r0"]), + ("r1", &["r1"]), + ("r2", &["r2"]), + ("r3", &["r3"]), +]; +const MIPS_ARG_REGS: &[(&str, &[&str])] = &[ + ("a0", &["a0", "$a0", "r4"]), + ("a1", &["a1", "$a1", "r5"]), + ("a2", &["a2", "$a2", "r6"]), + ("a3", &["a3", "$a3", "r7"]), +]; +const X86_STACK_BASES: &[&str] = &["rbp", "rsp", "ebp", "esp"]; +pub const X86_FRAME_BASES: &[&str] = &["rbp", "ebp"]; +const RISCV_STACK_BASES: &[&str] = &["sp", "s0", "fp", "x2", "x8"]; +const RISCV_FRAME_BASES: &[&str] = &["s0", "fp", "x8"]; +const ARM64_STACK_BASES: &[&str] = &["sp", "x29", "fp"]; +const ARM64_FRAME_BASES: &[&str] = &["x29", "fp"]; +const ARM32_STACK_BASES: &[&str] = &["sp", "r11", "fp"]; +const ARM32_FRAME_BASES: &[&str] = &["r11", "fp"]; +const MIPS_STACK_BASES: &[&str] = &["sp", "$sp", "fp", "$fp", "s8", "$s8"]; +const MIPS_FRAME_BASES: &[&str] = &["fp", "$fp", "s8", "$s8"]; +const GENERIC_STACK_BASES: &[&str] = &["sp", "fp", "bp", "s0", "x2", "x8", "rbp", "rsp"]; +const GENERIC_FRAME_BASES: &[&str] = &["fp", "bp", "s0", "x8", "rbp"]; + +pub fn recover_vars_arch_profile( + arch_name: Option<&str>, +) -> (ArgAliasMap, BaseRegList, BaseRegList) { + let Some(arch_name) = arch_name else { + return (&[], GENERIC_STACK_BASES, GENERIC_FRAME_BASES); + }; + + let arch_name = arch_name.to_ascii_lowercase(); + if arch_name.contains("x86") { + return (X86_ARG_REGS, X86_STACK_BASES, X86_FRAME_BASES); + } + if arch_name.contains("aarch64") || arch_name.contains("arm64") { + return (ARM64_ARG_REGS, ARM64_STACK_BASES, ARM64_FRAME_BASES); + } + if arch_name == "arm" || arch_name.starts_with("armv") { + return (ARM32_ARG_REGS, ARM32_STACK_BASES, ARM32_FRAME_BASES); + } + if arch_name.contains("riscv") || arch_name.starts_with("rv") { + return (RISCV_ARG_REGS, RISCV_STACK_BASES, RISCV_FRAME_BASES); + } + if arch_name.contains("mips") { + return (MIPS_ARG_REGS, MIPS_STACK_BASES, MIPS_FRAME_BASES); + } + + (&[], GENERIC_STACK_BASES, GENERIC_FRAME_BASES) +} + +pub fn size_to_type(size: u32) -> String { + match size { + 1 => "int8_t".to_string(), + 2 => "int16_t".to_string(), + 4 => "int32_t".to_string(), + 8 => "int64_t".to_string(), + _ => format!("byte[{size}]"), + } +} + +pub fn ssa_var_key(var: &SSAVar) -> String { + format!("{}_{}", var.name.to_ascii_lowercase(), var.version) +} + +pub fn ssa_var_block_key(block_addr: u64, var: &SSAVar) -> String { + format!("{}@{block_addr:x}", ssa_var_key(var)) +} + +pub fn scalar_register_family_key(name: &str) -> String { + let lower = name.to_ascii_lowercase(); + + if let Some(idx) = lower.strip_prefix('x').or_else(|| lower.strip_prefix('w')) + && !idx.is_empty() + && idx.chars().all(|ch| ch.is_ascii_digit()) + { + return format!("aarch64:gpr:{idx}"); + } + + match lower.as_str() { + "fp" => "aarch64:gpr:29".to_string(), + "lr" => "aarch64:gpr:30".to_string(), + "sp" | "wsp" => "aarch64:sp".to_string(), + "xzr" | "wzr" => "aarch64:zr".to_string(), + _ => lower, + } +} + +pub fn merge_type_hint(hints: &mut HashMap, key: String, incoming: TypeHint) { + match hints.get(&key) { + Some(current) if !incoming_hint_should_replace(current, &incoming) => {} + _ => { + hints.insert(key, incoming); + } + } +} + +pub fn collect_signature_type_evidence_context( + ssa_blocks: &[SSABlock], +) -> SignatureTypeEvidenceContext { + let pointer_vars = infer_pointer_var_keys_from_ssa(ssa_blocks); + let (scalar_proven_vars, scalar_likely_vars, bool_like_vars, mut width_bits) = + infer_scalar_var_evidence_from_ssa(ssa_blocks); + let register_versions = collect_register_version_keys(ssa_blocks); + normalize_register_family_width_hints(®ister_versions, &mut width_bits); + propagate_normalized_scalar_result_widths(ssa_blocks, &mut width_bits); + normalize_register_family_width_hints(®ister_versions, &mut width_bits); + SignatureTypeEvidenceContext { + pointer_vars, + scalar_proven_vars, + scalar_likely_vars, + bool_like_vars, + width_bits, + } +} + +pub fn collect_pointer_arg_slots(vars: &[RecoveredVariable]) -> BTreeSet { + vars.iter() + .filter(|var| var.kind == "r" && var.isarg && var.var_type.contains('*')) + .filter_map(|var| { + var.name + .strip_prefix("arg") + .and_then(|idx| idx.parse::().ok()) + }) + .collect() +} + +pub fn recover_vars_from_ssa( + ssa_blocks: &[SSABlock], + arch_name: Option<&str>, + metadata_reg_type_hints: &HashMap, + semantic_typing_enabled: bool, +) -> Vec { + let mut vars = Vec::new(); + let mut seen_slots: HashMap<(bool, i64), usize> = HashMap::new(); + let mut seen_arg_regs: HashSet = HashSet::new(); + let (arg_regs, stack_bases, frame_bases) = recover_vars_arch_profile(arch_name); + let signature_evidence = + semantic_typing_enabled.then(|| collect_signature_type_evidence_context(ssa_blocks)); + let (usage_reg_type_hints, pointer_var_keys) = if semantic_typing_enabled { + infer_usage_register_type_hints(ssa_blocks) + } else { + (HashMap::new(), HashSet::new()) + }; + let reg_type_hints = if semantic_typing_enabled { + merge_register_type_hints(metadata_reg_type_hints, &usage_reg_type_hints, arg_regs) + } else { + HashMap::new() + }; + + let mut stack_addr_temps: HashMap = HashMap::new(); + + for block in ssa_blocks { + for op in &block.ops { + match op { + SSAOp::IntAdd { dst, a, b } | SSAOp::IntSub { dst, a, b } => { + let a_name = a.name.to_lowercase(); + let b_name = b.name.to_lowercase(); + + let is_a_base = stack_bases.contains(&a_name.as_str()); + let is_b_const = b_name.starts_with("const:"); + + if is_a_base && is_b_const { + if let Some(raw_offset) = parse_const_value(&b.name) { + let offset = if matches!(op, SSAOp::IntSub { .. }) { + -(raw_offset as i64) + } else { + raw_offset as i64 + }; + let dst_key = ssa_var_block_key(block.addr, dst); + stack_addr_temps.insert(dst_key, (a_name.clone(), offset)); + } + } else if stack_bases.contains(&b_name.as_str()) + && a_name.starts_with("const:") + && let Some(raw_offset) = parse_const_value(&a.name) + { + let offset = raw_offset as i64; + let dst_key = ssa_var_block_key(block.addr, dst); + stack_addr_temps.insert(dst_key, (b_name.clone(), offset)); + } + } + SSAOp::Store { addr, val, .. } => { + let addr_key = ssa_var_block_key(block.addr, addr); + if let Some((base_reg, offset)) = stack_addr_temps.get(&addr_key) { + let type_override = if semantic_typing_enabled + && pointer_var_keys.contains(&ssa_var_key(val)) + { + Some("void *".to_string()) + } else { + None + }; + add_stack_var( + &mut vars, + &mut seen_slots, + base_reg, + frame_bases, + *offset, + val.size, + type_override, + ); + } + } + SSAOp::Load { dst, addr, .. } => { + let addr_key = ssa_var_block_key(block.addr, addr); + if let Some((base_reg, offset)) = stack_addr_temps.get(&addr_key) { + let type_override = if semantic_typing_enabled + && pointer_var_keys.contains(&ssa_var_key(dst)) + { + Some("void *".to_string()) + } else { + None + }; + add_stack_var( + &mut vars, + &mut seen_slots, + base_reg, + frame_bases, + *offset, + dst.size, + type_override, + ); + } + } + _ => {} + } + + for src in op.sources() { + let base_name = src.name.to_lowercase(); + if src.version == 0 { + for (i, (canonical, aliases)) in arg_regs.iter().enumerate() { + if aliases.contains(&base_name.as_str()) + && !seen_arg_regs.contains(*canonical) + { + seen_arg_regs.insert(canonical.to_string()); + let hinted_type = if semantic_typing_enabled { + recovered_arg_type_hint( + ®_type_hints, + canonical, + aliases, + src, + signature_evidence.as_ref(), + ) + } else { + None + }; + vars.push(RecoveredVariable { + name: format!("arg{i}"), + kind: "r".to_string(), + delta: 0, + var_type: hinted_type.unwrap_or_else(|| size_to_type(src.size)), + isarg: true, + reg: Some(canonical.to_string()), + }); + break; + } + } + } + } + } + } + + vars.sort_by_key(|v| v.delta); + vars +} + +fn incoming_hint_should_replace(current: &TypeHint, incoming: &TypeHint) -> bool { + incoming.rank > current.rank || (incoming.rank == current.rank && incoming.ty < current.ty) +} + +fn parse_const_value(name: &str) -> Option { + let val_str = name + .strip_prefix("const:") + .or_else(|| name.strip_prefix("CONST:"))?; + let val_str = val_str.split('_').next().unwrap_or(val_str); + if let Some(hex) = val_str + .strip_prefix("0x") + .or_else(|| val_str.strip_prefix("0X")) + { + return u64::from_str_radix(hex, 16).ok(); + } + if let Ok(v) = val_str.parse::() { + return Some(v); + } + u64::from_str_radix(val_str, 16).ok() +} + +fn ssa_var_is_const(var: &SSAVar) -> bool { + parse_const_value(&var.name).is_some() +} + +fn ssa_var_is_register_like(name: &str) -> bool { + let lower = name.to_ascii_lowercase(); + !(lower.starts_with("tmp:") + || lower.starts_with("const:") + || lower.starts_with("ram:") + || lower.starts_with("space")) +} + +fn strongest_hint_for_aliases( + hints: &HashMap, + canonical: &str, + aliases: &[&str], +) -> Option { + let mut best = hints.get(canonical).cloned(); + for alias in aliases { + if let Some(candidate) = hints.get(*alias).cloned() { + match &best { + Some(current) if !incoming_hint_should_replace(current, &candidate) => {} + _ => best = Some(candidate), + } + } + } + best +} + +fn width_hint_key_matches_family(key: &str, family: &str) -> bool { + let Some((name, _version)) = key.rsplit_once('_') else { + return false; + }; + scalar_register_family_key(name) == family +} + +fn width_hint_key_matches_family_version(key: &str, family: &str, version: u32) -> bool { + let Some((name, version_str)) = key.rsplit_once('_') else { + return false; + }; + version_str.parse::().ok() == Some(version) && scalar_register_family_key(name) == family +} + +fn recovered_arg_family_width_hint( + evidence: &SignatureTypeEvidenceContext, + src: &SSAVar, +) -> Option { + let family = scalar_register_family_key(&src.name); + evidence + .width_bits + .iter() + .filter(|(key, bits)| { + **bits > 0 && width_hint_key_matches_family_version(key, &family, src.version) + }) + .map(|(_, bits)| *bits) + .min() + .or_else(|| { + evidence + .width_bits + .iter() + .filter(|(key, bits)| **bits > 0 && width_hint_key_matches_family(key, &family)) + .map(|(_, bits)| *bits) + .min() + }) +} + +fn recovered_arg_type_hint( + reg_type_hints: &HashMap, + canonical: &str, + aliases: &[&str], + src: &SSAVar, + signature_evidence: Option<&SignatureTypeEvidenceContext>, +) -> Option { + let best_hint = strongest_hint_for_aliases(reg_type_hints, canonical, aliases); + if let Some(hint) = best_hint.as_ref() + && hint.rank == TypeHintRank::Pointer + { + return Some(hint.ty.clone()); + } + if let Some(evidence) = signature_evidence + && let Some(bits) = recovered_arg_family_width_hint(evidence, src) + { + return Some(size_to_type(bits.div_ceil(8))); + } + best_hint.map(|hint| hint.ty) +} + +fn merge_register_type_hints( + metadata_hints: &HashMap, + usage_hints: &HashMap, + arg_regs: ArgAliasMap, +) -> HashMap { + let mut merged = HashMap::new(); + + for (reg, hint) in metadata_hints { + merge_type_hint(&mut merged, reg.clone(), hint.clone()); + } + for (reg, hint) in usage_hints { + merge_type_hint(&mut merged, reg.clone(), hint.clone()); + } + + for (canonical, aliases) in arg_regs { + if let Some(best) = strongest_hint_for_aliases(&merged, canonical, aliases) { + merge_type_hint(&mut merged, (*canonical).to_string(), best.clone()); + for alias in *aliases { + merge_type_hint(&mut merged, alias.to_string(), best.clone()); + } + } + } + + merged +} + +fn add_stack_var( + vars: &mut Vec, + seen_slots: &mut HashMap<(bool, i64), usize>, + base_reg: &str, + frame_bases: &[&str], + offset: i64, + size: u32, + type_override: Option, +) { + let is_frame_base = frame_bases.contains(&base_reg); + let slot_key = (is_frame_base, offset); + if let Some(existing_idx) = seen_slots.get(&slot_key).copied() { + if let Some(override_ty) = type_override + && override_ty == "void *" + && let Some(existing) = vars.get_mut(existing_idx) + && existing.var_type != "void *" + { + existing.var_type = override_ty; + } + return; + } + + let is_arg = if is_frame_base { offset > 0 } else { false }; + let var_name = if is_arg && offset > 8 { + format!("arg_{:x}h", offset.unsigned_abs()) + } else { + format!("var_{:x}h", offset.unsigned_abs()) + }; + let kind = if is_frame_base { "b" } else { "s" }; + + vars.push(RecoveredVariable { + name: var_name, + kind: kind.to_string(), + delta: offset, + var_type: type_override.unwrap_or_else(|| size_to_type(size)), + isarg: is_arg && offset > 8, + reg: None, + }); + seen_slots.insert(slot_key, vars.len().saturating_sub(1)); +} + +fn collect_register_version_keys(ssa_blocks: &[SSABlock]) -> HashMap> { + let mut reg_versions: HashMap> = HashMap::new(); + for block in ssa_blocks { + for op in &block.ops { + let mut collect_var = |var: &SSAVar| { + if !ssa_var_is_register_like(&var.name) { + return; + } + let reg_name = scalar_register_family_key(&var.name); + reg_versions + .entry(reg_name) + .or_default() + .push(ssa_var_key(var)); + }; + if let Some(dst) = op.dst() { + collect_var(dst); + } + op.for_each_source(&mut collect_var); + } + } + for keys in reg_versions.values_mut() { + keys.sort(); + keys.dedup(); + } + reg_versions +} + +fn set_width_hint_prefer_narrower( + width_hints: &mut HashMap, + key: String, + bits: u32, +) -> bool { + if bits == 0 { + return false; + } + match width_hints.get(&key).copied() { + Some(current) if current == bits => false, + Some(current) if current > 0 && current <= bits => false, + _ => { + width_hints.insert(key, bits); + true + } + } +} + +fn normalize_register_family_width_hints( + register_versions: &HashMap>, + width_hints: &mut HashMap, +) -> bool { + let mut changed = false; + for reg_keys in register_versions.values() { + let max_bits = reg_keys + .iter() + .filter_map(|key| width_hints.get(key).copied()) + .max() + .unwrap_or(0); + let min_bits = reg_keys + .iter() + .filter_map(|key| { + let current = width_hints.get(key).copied().unwrap_or(0); + let natural = register_key_natural_bits(key).unwrap_or(0); + let candidate = match (current, natural) { + (0, 0) => 0, + (0, natural) => natural, + (current, 0) => current, + (current, natural) => current.min(natural), + }; + (candidate > 0).then_some(candidate) + }) + .min() + .unwrap_or(0); + let preferred_bits = if min_bits > 0 && max_bits == 64 && min_bits <= 32 { + min_bits + } else { + max_bits + }; + for key in reg_keys { + changed |= set_width_hint_prefer_narrower(width_hints, key.clone(), preferred_bits); + } + } + changed +} + +fn register_key_natural_bits(key: &str) -> Option { + let reg = key.split('_').next()?; + if let Some(idx) = reg.strip_prefix('w') + && !idx.is_empty() + && idx.chars().all(|ch| ch.is_ascii_digit()) + { + return Some(32); + } + if let Some(idx) = reg.strip_prefix('x') + && !idx.is_empty() + && idx.chars().all(|ch| ch.is_ascii_digit()) + { + return Some(64); + } + match reg { + "wsp" | "wzr" => Some(32), + "sp" | "fp" | "lr" | "xzr" => Some(64), + _ => None, + } +} + +fn propagate_normalized_scalar_result_widths( + ssa_blocks: &[SSABlock], + width_hints: &mut HashMap, +) -> bool { + let mut changed = false; + for block in ssa_blocks { + for op in &block.ops { + let (dst, source_bits) = match op { + SSAOp::Copy { dst, src } + | SSAOp::Cast { dst, src } + | SSAOp::New { dst, src } + | SSAOp::IntZExt { dst, src } + | SSAOp::IntSExt { dst, src } + | SSAOp::Subpiece { dst, src, .. } => { + let bits = width_hints.get(&ssa_var_key(src)).copied().unwrap_or(0); + (dst, bits) + } + SSAOp::IntAdd { dst, a, b } + | SSAOp::IntSub { dst, a, b } + | SSAOp::IntMult { dst, a, b } + | SSAOp::IntDiv { dst, a, b } + | SSAOp::IntSDiv { dst, a, b } + | SSAOp::IntRem { dst, a, b } + | SSAOp::IntSRem { dst, a, b } + | SSAOp::IntAnd { dst, a, b } + | SSAOp::IntOr { dst, a, b } + | SSAOp::IntXor { dst, a, b } + | SSAOp::IntLeft { dst, a, b } + | SSAOp::IntRight { dst, a, b } + | SSAOp::IntSRight { dst, a, b } => { + let bits = [a, b] + .into_iter() + .filter(|var| !ssa_var_is_const(var)) + .filter_map(|var| width_hints.get(&ssa_var_key(var)).copied()) + .filter(|bits| *bits > 0) + .max() + .unwrap_or(0); + (dst, bits) + } + _ => continue, + }; + changed |= set_width_hint_prefer_narrower(width_hints, ssa_var_key(dst), source_bits); + } + } + changed +} + +fn ssa_var_is_stack_base(var: &SSAVar) -> bool { + matches!( + var.name.to_ascii_lowercase().as_str(), + "rbp" | "rsp" | "ebp" | "esp" | "sp" | "fp" | "bp" | "s0" | "x2" | "x8" + ) +} + +fn infer_pointer_width_bytes(ssa_blocks: &[SSABlock]) -> u32 { + let mut width = 0u32; + for block in ssa_blocks { + for op in &block.ops { + if let Some(dst) = op.dst() + && ssa_var_is_stack_base(dst) + { + width = width.max(dst.size); + } + op.for_each_source(|src| { + if ssa_var_is_stack_base(src) { + width = width.max(src.size); + } + }); + } + } + if width == 0 { 8 } else { width } +} + +fn infer_index_like_var_keys(ssa_blocks: &[SSABlock]) -> HashSet { + let mut index_like: HashSet = HashSet::new(); + for block in ssa_blocks { + for op in &block.ops { + if let SSAOp::IntSExt { dst, src } | SSAOp::IntZExt { dst, src } = op + && src.size < dst.size + { + index_like.insert(ssa_var_key(dst)); + } + } + } + + let mut changed = true; + while changed { + changed = false; + for block in ssa_blocks { + for op in &block.ops { + match op { + SSAOp::Copy { dst, src } + | SSAOp::Cast { dst, src } + | SSAOp::New { dst, src } => { + if index_like.contains(&ssa_var_key(src)) { + changed |= index_like.insert(ssa_var_key(dst)); + } + } + SSAOp::IntMult { dst, a, b } => { + let a_key = ssa_var_key(a); + let b_key = ssa_var_key(b); + let a_is_scaled_const = ssa_var_is_const(a); + let b_is_scaled_const = ssa_var_is_const(b); + if (index_like.contains(&a_key) && ssa_var_is_const(b)) + || (index_like.contains(&b_key) && ssa_var_is_const(a)) + || (a_is_scaled_const && !b_is_scaled_const) + || (b_is_scaled_const && !a_is_scaled_const) + { + changed |= index_like.insert(ssa_var_key(dst)); + } + } + SSAOp::IntLeft { dst, a, b } => { + let shift_amount = parse_const_value(&b.name).unwrap_or(u64::MAX); + if (index_like.contains(&ssa_var_key(a)) && ssa_var_is_const(b)) + || shift_amount <= 6 + { + changed |= index_like.insert(ssa_var_key(dst)); + } + } + _ => {} + } + } + } + } + + index_like +} + +fn infer_pointer_var_keys_from_ssa(ssa_blocks: &[SSABlock]) -> HashSet { + let mut pointer_vars: HashSet = HashSet::new(); + let register_versions = collect_register_version_keys(ssa_blocks); + let index_like_vars = infer_index_like_var_keys(ssa_blocks); + let pointer_width = infer_pointer_width_bytes(ssa_blocks); + let mut stack_addr_slots: HashMap = HashMap::new(); + let mut pointer_stack_slots: HashSet = HashSet::new(); + + for block in ssa_blocks { + for op in &block.ops { + match op { + SSAOp::IntAdd { dst, a, b } | SSAOp::IntSub { dst, a, b } => { + let a_is_stack = ssa_var_is_stack_base(a); + let b_is_stack = ssa_var_is_stack_base(b); + let a_const = parse_const_value(&a.name); + let b_const = parse_const_value(&b.name); + + if a_is_stack && b_const.is_some() { + let raw = b_const.unwrap_or(0); + let offset = if matches!(op, SSAOp::IntSub { .. }) { + -(raw as i64) + } else { + raw as i64 + }; + stack_addr_slots.insert( + ssa_var_block_key(block.addr, dst), + format!("{}:{offset}", a.name.to_ascii_lowercase()), + ); + } else if matches!(op, SSAOp::IntAdd { .. }) && b_is_stack && a_const.is_some() + { + let raw = a_const.unwrap_or(0); + stack_addr_slots.insert( + ssa_var_block_key(block.addr, dst), + format!("{}:{}", b.name.to_ascii_lowercase(), raw as i64), + ); + } + } + SSAOp::Load { addr, .. } + | SSAOp::Store { addr, .. } + | SSAOp::LoadLinked { addr, .. } + | SSAOp::StoreConditional { addr, .. } + | SSAOp::LoadGuarded { addr, .. } + | SSAOp::StoreGuarded { addr, .. } + | SSAOp::AtomicCAS { addr, .. } => { + pointer_vars.insert(ssa_var_key(addr)); + } + _ => {} + } + } + } + + let mut changed = true; + while changed { + changed = false; + for block in ssa_blocks { + for op in &block.ops { + match op { + SSAOp::Phi { dst, sources } => { + let dst_key = ssa_var_key(dst); + let dst_is_pointer = pointer_vars.contains(&dst_key); + let any_source_pointer = sources + .iter() + .any(|src| pointer_vars.contains(&ssa_var_key(src))); + + if any_source_pointer { + changed |= pointer_vars.insert(dst_key.clone()); + } + if dst_is_pointer { + for src in sources { + changed |= pointer_vars.insert(ssa_var_key(src)); + } + } + } + SSAOp::Copy { dst, src } + | SSAOp::Cast { dst, src } + | SSAOp::New { dst, src } => { + let dst_key = ssa_var_key(dst); + let src_key = ssa_var_key(src); + if pointer_vars.contains(&dst_key) { + changed |= pointer_vars.insert(src_key.clone()); + } + if pointer_vars.contains(&src_key) { + changed |= pointer_vars.insert(dst_key); + } + } + SSAOp::IntAdd { dst, a, b } | SSAOp::IntSub { dst, a, b } => { + let dst_key = ssa_var_key(dst); + let a_key = ssa_var_key(a); + let b_key = ssa_var_key(b); + let a_is_const = ssa_var_is_const(a); + let b_is_const = ssa_var_is_const(b); + let a_index_like = index_like_vars.contains(&a_key); + let b_index_like = index_like_vars.contains(&b_key); + + if pointer_vars.contains(&dst_key) { + if a_is_const && !b_is_const { + changed |= pointer_vars.insert(b_key.clone()); + } else if b_is_const && !a_is_const { + changed |= pointer_vars.insert(a_key.clone()); + } else if a_index_like && !b_index_like { + changed |= pointer_vars.insert(b_key.clone()); + } else if b_index_like && !a_index_like { + changed |= pointer_vars.insert(a_key.clone()); + } else if a_index_like && b_index_like { + let a_is_tmp = a.name.starts_with("tmp:"); + let b_is_tmp = b.name.starts_with("tmp:"); + if a_is_tmp && !b_is_tmp { + changed |= pointer_vars.insert(b_key.clone()); + } else if b_is_tmp && !a_is_tmp { + changed |= pointer_vars.insert(a_key.clone()); + } + } + } + + if pointer_vars.contains(&a_key) && b_is_const { + changed |= pointer_vars.insert(dst_key.clone()); + } + if pointer_vars.contains(&b_key) && a_is_const { + changed |= pointer_vars.insert(dst_key.clone()); + } + if pointer_vars.contains(&a_key) && index_like_vars.contains(&b_key) { + changed |= pointer_vars.insert(dst_key.clone()); + } + if pointer_vars.contains(&b_key) && index_like_vars.contains(&a_key) { + changed |= pointer_vars.insert(dst_key.clone()); + } + } + SSAOp::PtrAdd { dst, base, .. } | SSAOp::PtrSub { dst, base, .. } => { + let dst_key = ssa_var_key(dst); + let base_key = ssa_var_key(base); + if pointer_vars.contains(&dst_key) { + changed |= pointer_vars.insert(base_key.clone()); + } + if pointer_vars.contains(&base_key) { + changed |= pointer_vars.insert(dst_key); + } + } + SSAOp::SegmentOp { dst, offset, .. } => { + let dst_key = ssa_var_key(dst); + let offset_key = ssa_var_key(offset); + if pointer_vars.contains(&dst_key) { + changed |= pointer_vars.insert(offset_key.clone()); + } + if pointer_vars.contains(&offset_key) { + changed |= pointer_vars.insert(dst_key); + } + } + SSAOp::Store { addr, val, .. } => { + if let Some(slot) = + stack_addr_slots.get(&ssa_var_block_key(block.addr, addr)) + { + let val_key = ssa_var_key(val); + if val.size >= pointer_width && pointer_vars.contains(&val_key) { + changed |= pointer_stack_slots.insert(slot.clone()); + } + if val.size >= pointer_width && pointer_stack_slots.contains(slot) { + changed |= pointer_vars.insert(val_key); + } + } + } + SSAOp::Load { dst, addr, .. } => { + if let Some(slot) = + stack_addr_slots.get(&ssa_var_block_key(block.addr, addr)) + { + let dst_key = ssa_var_key(dst); + if dst.size >= pointer_width && pointer_stack_slots.contains(slot) { + changed |= pointer_vars.insert(dst_key.clone()); + } + if dst.size >= pointer_width && pointer_vars.contains(&dst_key) { + changed |= pointer_stack_slots.insert(slot.clone()); + } + } + } + _ => {} + } + } + } + + for reg_keys in register_versions.values() { + if reg_keys.iter().any(|key| pointer_vars.contains(key)) { + for key in reg_keys { + changed |= pointer_vars.insert(key.clone()); + } + } + } + } + + pointer_vars +} + +fn merge_width_hint(width_hints: &mut HashMap, var: &SSAVar, bits: u32) { + let entry = width_hints.entry(ssa_var_key(var)).or_insert(0); + *entry = (*entry).max(bits.max(var.size.saturating_mul(8))); +} + +fn mark_scalar_var( + vars: &mut HashSet, + width_hints: &mut HashMap, + var: &SSAVar, +) { + vars.insert(ssa_var_key(var)); + merge_width_hint(width_hints, var, var.size.saturating_mul(8)); +} + +fn infer_scalar_var_evidence_from_ssa( + ssa_blocks: &[SSABlock], +) -> ( + HashSet, + HashSet, + HashSet, + HashMap, +) { + let register_versions = collect_register_version_keys(ssa_blocks); + let mut scalar_proven: HashSet = HashSet::new(); + let mut scalar_likely: HashSet = HashSet::new(); + let mut bool_like: HashSet = HashSet::new(); + let mut width_hints: HashMap = HashMap::new(); + + for block in ssa_blocks { + for op in &block.ops { + match op { + SSAOp::IntMult { a, b, .. } + | SSAOp::IntDiv { a, b, .. } + | SSAOp::IntSDiv { a, b, .. } + | SSAOp::IntRem { a, b, .. } + | SSAOp::IntSRem { a, b, .. } + | SSAOp::IntAnd { a, b, .. } + | SSAOp::IntOr { a, b, .. } + | SSAOp::IntXor { a, b, .. } + | SSAOp::IntLeft { a, b, .. } + | SSAOp::IntRight { a, b, .. } + | SSAOp::IntSRight { a, b, .. } + | SSAOp::IntCarry { a, b, .. } + | SSAOp::IntSCarry { a, b, .. } + | SSAOp::IntSBorrow { a, b, .. } => { + mark_scalar_var(&mut scalar_proven, &mut width_hints, a); + mark_scalar_var(&mut scalar_proven, &mut width_hints, b); + } + SSAOp::IntNegate { src, .. } + | SSAOp::IntNot { src, .. } + | SSAOp::PopCount { src, .. } + | SSAOp::Lzcount { src, .. } => { + mark_scalar_var(&mut scalar_proven, &mut width_hints, src); + } + SSAOp::PtrAdd { index, .. } | SSAOp::PtrSub { index, .. } => { + mark_scalar_var(&mut scalar_proven, &mut width_hints, index); + } + SSAOp::IntAdd { a, b, .. } | SSAOp::IntSub { a, b, .. } => { + mark_scalar_var(&mut scalar_likely, &mut width_hints, a); + mark_scalar_var(&mut scalar_likely, &mut width_hints, b); + } + SSAOp::BoolNot { dst, src } => { + mark_scalar_var(&mut scalar_likely, &mut width_hints, src); + bool_like.insert(ssa_var_key(src)); + bool_like.insert(ssa_var_key(dst)); + merge_width_hint(&mut width_hints, dst, 1); + } + SSAOp::CBranch { cond, .. } + | SSAOp::LoadGuarded { guard: cond, .. } + | SSAOp::StoreGuarded { guard: cond, .. } => { + mark_scalar_var(&mut scalar_likely, &mut width_hints, cond); + bool_like.insert(ssa_var_key(cond)); + merge_width_hint(&mut width_hints, cond, 1); + } + SSAOp::FloatNeg { src, .. } + | SSAOp::FloatAbs { src, .. } + | SSAOp::FloatSqrt { src, .. } + | SSAOp::Cast { src, .. } => { + mark_scalar_var(&mut scalar_likely, &mut width_hints, src); + } + SSAOp::IntEqual { dst, a, b } + | SSAOp::IntNotEqual { dst, a, b } + | SSAOp::IntLess { dst, a, b } + | SSAOp::IntSLess { dst, a, b } + | SSAOp::IntLessEqual { dst, a, b } + | SSAOp::IntSLessEqual { dst, a, b } + | SSAOp::FloatEqual { dst, a, b } + | SSAOp::FloatNotEqual { dst, a, b } + | SSAOp::FloatLess { dst, a, b } + | SSAOp::FloatLessEqual { dst, a, b } => { + mark_scalar_var(&mut scalar_likely, &mut width_hints, a); + mark_scalar_var(&mut scalar_likely, &mut width_hints, b); + bool_like.insert(ssa_var_key(dst)); + merge_width_hint(&mut width_hints, dst, 1); + } + SSAOp::BoolAnd { dst, a, b } + | SSAOp::BoolOr { dst, a, b } + | SSAOp::BoolXor { dst, a, b } => { + mark_scalar_var(&mut scalar_likely, &mut width_hints, a); + mark_scalar_var(&mut scalar_likely, &mut width_hints, b); + bool_like.insert(ssa_var_key(a)); + bool_like.insert(ssa_var_key(b)); + bool_like.insert(ssa_var_key(dst)); + merge_width_hint(&mut width_hints, dst, 1); + } + SSAOp::IntZExt { dst, src } | SSAOp::IntSExt { dst, src } => { + mark_scalar_var(&mut scalar_likely, &mut width_hints, src); + mark_scalar_var(&mut scalar_likely, &mut width_hints, dst); + merge_width_hint(&mut width_hints, dst, dst.size.saturating_mul(8)); + } + SSAOp::Subpiece { dst, src, .. } => { + mark_scalar_var(&mut scalar_likely, &mut width_hints, src); + mark_scalar_var(&mut scalar_likely, &mut width_hints, dst); + merge_width_hint(&mut width_hints, dst, dst.size.saturating_mul(8)); + } + _ => {} + } + } + } + + let mut changed = true; + while changed { + changed = false; + for block in ssa_blocks { + for op in &block.ops { + match op { + SSAOp::IntAdd { dst, a, b } + | SSAOp::IntSub { dst, a, b } + | SSAOp::IntMult { dst, a, b } + | SSAOp::IntDiv { dst, a, b } + | SSAOp::IntSDiv { dst, a, b } + | SSAOp::IntRem { dst, a, b } + | SSAOp::IntSRem { dst, a, b } + | SSAOp::IntAnd { dst, a, b } + | SSAOp::IntOr { dst, a, b } + | SSAOp::IntXor { dst, a, b } + | SSAOp::IntLeft { dst, a, b } + | SSAOp::IntRight { dst, a, b } + | SSAOp::IntSRight { dst, a, b } => { + let dst_key = ssa_var_key(dst); + let a_key = ssa_var_key(a); + let b_key = ssa_var_key(b); + let any_proven = + scalar_proven.contains(&a_key) || scalar_proven.contains(&b_key); + let any_likely = scalar_likely.contains(&a_key) + || scalar_likely.contains(&b_key) + || any_proven; + if any_proven { + changed |= scalar_proven.insert(dst_key.clone()); + } + if any_likely { + changed |= scalar_likely.insert(dst_key.clone()); + } + let operand_bits = [a, b] + .into_iter() + .filter(|var| !ssa_var_is_const(var)) + .filter_map(|var| width_hints.get(&ssa_var_key(var)).copied()) + .filter(|bits| *bits > 0) + .max() + .unwrap_or(0); + if operand_bits > 0 { + let entry = width_hints.entry(dst_key).or_insert(0); + if operand_bits > *entry { + *entry = operand_bits; + changed = true; + } + } + } + SSAOp::Phi { dst, sources } => { + let dst_key = ssa_var_key(dst); + let any_proven = sources + .iter() + .any(|src| scalar_proven.contains(&ssa_var_key(src))); + let any_likely = sources + .iter() + .any(|src| scalar_likely.contains(&ssa_var_key(src))); + let any_bool = sources + .iter() + .any(|src| bool_like.contains(&ssa_var_key(src))); + if any_proven { + changed |= scalar_proven.insert(dst_key.clone()); + } + if any_likely || any_proven { + changed |= scalar_likely.insert(dst_key.clone()); + } + if any_bool { + changed |= bool_like.insert(dst_key.clone()); + } + let max_bits = sources + .iter() + .filter_map(|src| width_hints.get(&ssa_var_key(src)).copied()) + .max() + .unwrap_or(0); + if max_bits > 0 { + let entry = width_hints.entry(dst_key).or_insert(0); + if max_bits > *entry { + *entry = max_bits; + changed = true; + } + } + } + SSAOp::Copy { dst, src } + | SSAOp::Cast { dst, src } + | SSAOp::New { dst, src } => { + let dst_key = ssa_var_key(dst); + let src_key = ssa_var_key(src); + if scalar_proven.contains(&src_key) { + changed |= scalar_proven.insert(dst_key.clone()); + } + if scalar_likely.contains(&src_key) || scalar_proven.contains(&src_key) { + changed |= scalar_likely.insert(dst_key.clone()); + } + if bool_like.contains(&src_key) { + changed |= bool_like.insert(dst_key.clone()); + } + let bits = width_hints.get(&src_key).copied().unwrap_or(0); + if bits > 0 { + let entry = width_hints.entry(dst_key).or_insert(0); + if bits > *entry { + *entry = bits; + changed = true; + } + } + } + SSAOp::IntZExt { dst, src } + | SSAOp::IntSExt { dst, src } + | SSAOp::Subpiece { dst, src, .. } => { + let dst_key = ssa_var_key(dst); + let src_key = ssa_var_key(src); + if scalar_likely.contains(&src_key) || scalar_proven.contains(&src_key) { + changed |= scalar_likely.insert(dst_key.clone()); + } + if bool_like.contains(&src_key) { + changed |= bool_like.insert(dst_key.clone()); + } + let entry = width_hints.entry(dst_key).or_insert(0); + let bits = dst.size.saturating_mul(8); + if bits > *entry { + *entry = bits; + changed = true; + } + } + _ => {} + } + } + } + + for reg_keys in register_versions.values() { + let any_proven = reg_keys.iter().any(|key| scalar_proven.contains(key)); + let any_likely = reg_keys.iter().any(|key| scalar_likely.contains(key)); + let any_bool = reg_keys.iter().any(|key| bool_like.contains(key)); + let max_bits = reg_keys + .iter() + .filter_map(|key| width_hints.get(key).copied()) + .max() + .unwrap_or(0); + let min_bits = reg_keys + .iter() + .filter_map(|key| width_hints.get(key).copied()) + .filter(|bits| *bits > 0) + .min() + .unwrap_or(0); + let preferred_bits = if min_bits > 0 && max_bits == 64 && min_bits <= 32 { + min_bits + } else { + max_bits + }; + for key in reg_keys { + if any_proven { + changed |= scalar_proven.insert(key.clone()); + } + if any_likely || any_proven { + changed |= scalar_likely.insert(key.clone()); + } + if any_bool { + changed |= bool_like.insert(key.clone()); + } + if preferred_bits > 0 { + let entry = width_hints.entry(key.clone()).or_insert(0); + if preferred_bits > *entry { + *entry = preferred_bits; + changed = true; + } + } + } + } + } + + (scalar_proven, scalar_likely, bool_like, width_hints) +} + +fn infer_usage_register_type_hints( + ssa_blocks: &[SSABlock], +) -> (HashMap, HashSet) { + let pointer_vars = infer_pointer_var_keys_from_ssa(ssa_blocks); + let mut hints = HashMap::new(); + + for block in ssa_blocks { + for op in &block.ops { + let mut maybe_add = |var: &SSAVar| { + let key = ssa_var_key(var); + if !pointer_vars.contains(&key) || !ssa_var_is_register_like(&var.name) { + return; + } + merge_type_hint( + &mut hints, + var.name.to_ascii_lowercase(), + TypeHint::pointer(), + ); + }; + if let Some(dst) = op.dst() { + maybe_add(dst); + } + op.for_each_source(&mut maybe_add); + } + } + + (hints, pointer_vars) +} diff --git a/crates/r2types/src/signature_infer.rs b/crates/r2types/src/signature_infer.rs new file mode 100644 index 0000000..efd0ab1 --- /dev/null +++ b/crates/r2types/src/signature_infer.rs @@ -0,0 +1,1014 @@ +use std::collections::{BTreeSet, HashMap, HashSet}; + +use r2ssa::{SSABlock, SSAFunction, SSAOp, SSAVar, SsaArtifact}; + +use crate::convert::CTypeLike; +use crate::facts::{FunctionType, FunctionTypeFacts}; +use crate::inference::TypeInference; +use crate::model::Signedness; +use crate::prepare::{SignatureTypeEvidenceContext, scalar_register_family_key}; +use crate::signature::SignatureRegistry; +use crate::writeback::{InferredSignature, InferredSignatureParam}; + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct SignatureTypeEvidence { + pub pointer_proven: u8, + pub pointer_likely: u8, + pub scalar_proven: u8, + pub scalar_likely: u8, + pub bool_like: u8, + pub width_bits: u32, +} + +impl SignatureTypeEvidence { + pub fn pointer_score(&self) -> u16 { + (self.pointer_proven as u16) * 4 + (self.pointer_likely as u16) * 2 + } + + pub fn scalar_score(&self) -> u16 { + (self.scalar_proven as u16) * 4 + + (self.scalar_likely as u16) * 2 + + (self.bool_like as u16) * 3 + } + + pub fn has_pointer_signal(&self) -> bool { + self.pointer_proven > 0 || self.pointer_likely > 0 + } + + pub fn has_scalar_signal(&self) -> bool { + self.scalar_proven > 0 || self.scalar_likely > 0 || self.bool_like > 0 + } + + pub fn has_conflict(&self) -> bool { + self.has_pointer_signal() && self.has_scalar_signal() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SignatureParamCandidate { + pub name: String, + pub ty: CTypeLike, + pub arg_index: usize, + pub size_bytes: u32, + pub evidence: SignatureTypeEvidence, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RecoveredSignatureParam { + pub name: String, + pub ssa_var: SSAVar, + pub initial_ty: CTypeLike, +} + +pub fn merge_pointer_slot_evidence_into_signature_params( + inferred_params: &mut [SignatureParamCandidate], + pointer_arg_slots: &BTreeSet, +) { + if pointer_arg_slots.is_empty() { + return; + } + + let param_count = inferred_params.len(); + for (fallback_idx, param) in inferred_params.iter_mut().enumerate() { + let explicit_slot = if param.arg_index == usize::MAX { + None + } else { + Some(param.arg_index) + }; + let slot = explicit_slot.unwrap_or(fallback_idx); + let fallback_slot_match = pointer_arg_slots.contains(&fallback_idx) + && (explicit_slot.is_none() || param_count == 1); + if pointer_arg_slots.contains(&slot) || fallback_slot_match { + param.evidence.pointer_proven = param.evidence.pointer_proven.max(1); + } + } +} + +pub fn infer_signature_from_prepared_ssa( + function_name: &str, + arch_name: &str, + ptr_bits: u32, + prepared: &SsaArtifact, + pattern_ssa_blocks: &[SSABlock], + recovered_params: &[RecoveredSignatureParam], + pointer_arg_slots: &BTreeSet, +) -> InferredSignature { + let evidence_ctx = crate::prepare::collect_signature_type_evidence_context(pattern_ssa_blocks); + let mut type_inference = TypeInference::new(ptr_bits); + type_inference.set_prepared_ssa(prepared); + type_inference.infer_function(prepared); + + let mut canonical_params = recovered_params + .iter() + .map(|param| { + let mut evidence = collect_signature_type_evidence_for_var( + &evidence_ctx, + ¶m.ssa_var, + ¶m.initial_ty, + ); + if matches!(param.initial_ty, CTypeLike::Void | CTypeLike::Unknown) { + merge_initial_signature_type_evidence( + &type_inference.type_from_size(param.ssa_var.size), + &mut evidence, + ); + } + let ty = resolve_evidence_driven_signature_type( + param.initial_ty.clone(), + param.ssa_var.size, + ptr_bits, + &evidence, + ); + let arg_index = param + .name + .strip_prefix("arg") + .and_then(|n| n.parse::().ok()) + .unwrap_or(usize::MAX); + SignatureParamCandidate { + name: param.name.clone(), + ty, + arg_index, + size_bytes: param.ssa_var.size, + evidence, + } + }) + .collect::>(); + + merge_pointer_slot_evidence_into_signature_params(&mut canonical_params, pointer_arg_slots); + + for param in &mut canonical_params { + param.ty = resolve_evidence_driven_signature_type( + param.ty.clone(), + param.size_bytes, + ptr_bits, + ¶m.evidence, + ); + } + + let (ret_type, ret_evidence) = + infer_signature_return_type(prepared, &type_inference, ptr_bits, &evidence_ctx); + let input_counts = collect_version0_input_regs(prepared); + build_inferred_signature( + function_name, + arch_name, + ptr_bits, + &canonical_params, + &ret_type, + &ret_evidence, + &input_counts, + ) +} + +pub fn merge_initial_signature_type_evidence( + initial_ty: &CTypeLike, + evidence: &mut SignatureTypeEvidence, +) { + match initial_ty { + CTypeLike::Pointer(_) => evidence.pointer_likely = evidence.pointer_likely.max(1), + CTypeLike::Bool => evidence.bool_like = evidence.bool_like.max(1), + CTypeLike::Int { bits, .. } => { + evidence.scalar_likely = evidence.scalar_likely.max(1); + if !(evidence.has_scalar_signal() + && !evidence.has_pointer_signal() + && evidence.width_bits > 0 + && evidence.width_bits < *bits) + { + evidence.width_bits = evidence.width_bits.max(*bits); + } + } + CTypeLike::Float(bits) => { + evidence.scalar_proven = evidence.scalar_proven.max(1); + evidence.width_bits = evidence.width_bits.max(*bits); + } + _ => {} + } +} + +fn fallback_scalar_type_like( + var_size_bytes: u32, + evidence: &SignatureTypeEvidence, + ptr_bits: u32, +) -> CTypeLike { + if evidence.bool_like > 0 + && evidence.pointer_score() == 0 + && evidence.scalar_proven == 0 + && evidence.scalar_likely <= 1 + { + return CTypeLike::Bool; + } + + let carrier_bits = var_size_bytes.saturating_mul(8); + let width_bits = if evidence.has_scalar_signal() + && !evidence.has_pointer_signal() + && evidence.width_bits > 0 + { + evidence.width_bits + } else { + evidence.width_bits.max(carrier_bits) + }; + let width_bits = match width_bits { + 0 => { + if ptr_bits >= 64 { + 64 + } else { + 32 + } + } + 1 => 8, + 2..=8 => 8, + 9..=16 => 16, + 17..=32 => 32, + _ => 64, + }; + + CTypeLike::Int { + bits: width_bits, + signedness: Signedness::Signed, + } +} + +fn is_unmaterialized_aggregate_name(name: &str) -> bool { + let lower = name.trim().to_ascii_lowercase(); + lower.is_empty() || lower == "anon" || lower.starts_with("anon_") +} + +pub fn materialize_signature_type_like(ty: CTypeLike, ptr_bits: u32) -> CTypeLike { + match ty { + CTypeLike::Pointer(inner) => { + if matches!(*inner, CTypeLike::Unknown | CTypeLike::Void) + || matches!( + inner.as_ref(), + CTypeLike::Struct(name) + | CTypeLike::Union(name) + | CTypeLike::Enum(name) + if is_unmaterialized_aggregate_name(name) + ) + { + return CTypeLike::Pointer(Box::new(CTypeLike::Void)); + } + CTypeLike::Pointer(Box::new(materialize_signature_type_like(*inner, ptr_bits))) + } + CTypeLike::Array(inner, len) => { + if matches!(*inner, CTypeLike::Unknown | CTypeLike::Void) { + return CTypeLike::Array( + Box::new(CTypeLike::Int { + bits: 8, + signedness: Signedness::Unsigned, + }), + len, + ); + } + CTypeLike::Array( + Box::new(materialize_signature_type_like(*inner, ptr_bits)), + len, + ) + } + CTypeLike::Unknown => { + fallback_scalar_type_like((ptr_bits / 8).max(1), &Default::default(), ptr_bits) + } + CTypeLike::Struct(name) if is_unmaterialized_aggregate_name(&name) => { + fallback_scalar_type_like((ptr_bits / 8).max(1), &Default::default(), ptr_bits) + } + CTypeLike::Union(name) if is_unmaterialized_aggregate_name(&name) => { + fallback_scalar_type_like((ptr_bits / 8).max(1), &Default::default(), ptr_bits) + } + CTypeLike::Enum(name) if is_unmaterialized_aggregate_name(&name) => { + fallback_scalar_type_like((ptr_bits / 8).max(1), &Default::default(), ptr_bits) + } + other => other, + } +} + +pub fn render_signature_type(ty: &CTypeLike, ptr_bits: u32) -> String { + match materialize_signature_type_like(ty.clone(), ptr_bits) { + CTypeLike::Void => "void".to_string(), + CTypeLike::Bool => "bool".to_string(), + CTypeLike::Int { + bits: 8, + signedness: Signedness::Signed, + } => "int8_t".to_string(), + CTypeLike::Int { + bits: 16, + signedness: Signedness::Signed, + } => "int16_t".to_string(), + CTypeLike::Int { + bits: 32, + signedness: Signedness::Signed, + } => "int32_t".to_string(), + CTypeLike::Int { + bits: 64, + signedness: Signedness::Signed, + } => "int64_t".to_string(), + CTypeLike::Int { + bits, + signedness: Signedness::Signed | Signedness::Unknown, + } => format!("int{bits}_t"), + CTypeLike::Int { + bits: 8, + signedness: Signedness::Unsigned, + } => "uint8_t".to_string(), + CTypeLike::Int { + bits: 16, + signedness: Signedness::Unsigned, + } => "uint16_t".to_string(), + CTypeLike::Int { + bits: 32, + signedness: Signedness::Unsigned, + } => "uint32_t".to_string(), + CTypeLike::Int { + bits: 64, + signedness: Signedness::Unsigned, + } => "uint64_t".to_string(), + CTypeLike::Int { + bits, + signedness: Signedness::Unsigned, + } => format!("uint{bits}_t"), + CTypeLike::Float(32) => "float".to_string(), + CTypeLike::Float(64) => "double".to_string(), + CTypeLike::Float(bits) => format!("float{bits}"), + CTypeLike::Pointer(inner) => format!("{}*", render_signature_type(&inner, ptr_bits)), + CTypeLike::Array(inner, Some(size)) => { + format!("{}[{}]", render_signature_type(&inner, ptr_bits), size) + } + CTypeLike::Array(inner, None) => format!("{}[]", render_signature_type(&inner, ptr_bits)), + CTypeLike::Struct(name) => format!("struct {name}"), + CTypeLike::Union(name) => format!("union {name}"), + CTypeLike::Enum(name) => format!("enum {name}"), + CTypeLike::Function => "void (*)()".to_string(), + CTypeLike::Unknown => "int64_t".to_string(), + } +} + +fn sanitize_signature_type_like( + mut ty: CTypeLike, + var_size_bytes: u32, + ptr_bits: u32, +) -> CTypeLike { + if matches!(ty, CTypeLike::Void | CTypeLike::Unknown) { + ty = match var_size_bytes { + 1 => CTypeLike::Int { + bits: 8, + signedness: Signedness::Signed, + }, + 2 => CTypeLike::Int { + bits: 16, + signedness: Signedness::Signed, + }, + 4 => CTypeLike::Int { + bits: 32, + signedness: Signedness::Signed, + }, + 8 => CTypeLike::Int { + bits: 64, + signedness: Signedness::Signed, + }, + _ => CTypeLike::Unknown, + }; + } + + if matches!(ty, CTypeLike::Void | CTypeLike::Unknown) { + ty = CTypeLike::Int { + bits: if ptr_bits >= 64 { 64 } else { 32 }, + signedness: Signedness::Signed, + }; + } + + ty +} + +pub fn resolve_evidence_driven_signature_type( + initial_ty: CTypeLike, + var_size_bytes: u32, + ptr_bits: u32, + evidence: &SignatureTypeEvidence, +) -> CTypeLike { + if matches!(initial_ty, CTypeLike::Float(_)) { + return initial_ty; + } + + let pointer_score = evidence.pointer_score(); + let scalar_score = evidence.scalar_score(); + let initial_is_pointer = matches!(initial_ty, CTypeLike::Pointer(_)); + let initial_is_scalar = matches!(initial_ty, CTypeLike::Bool | CTypeLike::Int { .. }); + let preferred_scalar = fallback_scalar_type_like(var_size_bytes, evidence, ptr_bits); + let scalar_width_narrows = match (&initial_ty, &preferred_scalar) { + ( + CTypeLike::Int { + bits: initial_bits, .. + }, + CTypeLike::Int { + bits: preferred_bits, + .. + }, + ) => preferred_bits < initial_bits, + (CTypeLike::Int { .. }, CTypeLike::Bool) => true, + _ => false, + }; + + if initial_is_pointer && pointer_score.saturating_add(1) >= scalar_score { + return initial_ty; + } + if initial_is_scalar && scalar_score.saturating_add(1) >= pointer_score { + if scalar_width_narrows + && evidence.has_scalar_signal() + && !evidence.has_pointer_signal() + && !evidence.has_conflict() + { + return preferred_scalar; + } + return initial_ty; + } + + match initial_ty { + CTypeLike::Struct(_) | CTypeLike::Union(_) | CTypeLike::Enum(_) => { + if pointer_score > scalar_score.saturating_add(1) { + return CTypeLike::Pointer(Box::new(CTypeLike::Void)); + } + if scalar_score > pointer_score.saturating_add(2) { + return fallback_scalar_type_like(var_size_bytes, evidence, ptr_bits); + } + return initial_ty; + } + _ => {} + } + + if pointer_score > scalar_score.saturating_add(1) { + return CTypeLike::Pointer(Box::new(CTypeLike::Void)); + } + if scalar_score > pointer_score || matches!(initial_ty, CTypeLike::Void | CTypeLike::Unknown) { + return preferred_scalar; + } + + sanitize_signature_type_like(initial_ty, var_size_bytes, ptr_bits) +} + +pub fn collect_signature_type_evidence_for_var( + evidence_ctx: &SignatureTypeEvidenceContext, + var: &SSAVar, + initial_ty: &CTypeLike, +) -> SignatureTypeEvidence { + let key = format!("{}_{}", var.name.to_ascii_lowercase(), var.version); + let family = scalar_register_family_key(&var.name); + let mut evidence = SignatureTypeEvidence::default(); + if evidence_ctx.pointer_vars.contains(&key) { + evidence.pointer_proven = 1; + } + if evidence_ctx.scalar_proven_vars.contains(&key) { + evidence.scalar_proven = 1; + } + if evidence_ctx.scalar_likely_vars.contains(&key) { + evidence.scalar_likely = 1; + } + if evidence_ctx.bool_like_vars.contains(&key) { + evidence.bool_like = 1; + } + if let Some(bits) = evidence_ctx.width_bits.get(&key) { + evidence.width_bits = *bits; + } + if evidence.pointer_proven == 0 + && signal_present_for_register_family(&evidence_ctx.pointer_vars, &family, var.version) + { + evidence.pointer_proven = 1; + } + if evidence.scalar_proven == 0 + && signal_present_for_register_family( + &evidence_ctx.scalar_proven_vars, + &family, + var.version, + ) + { + evidence.scalar_proven = 1; + } + if evidence.scalar_likely == 0 + && signal_present_for_register_family( + &evidence_ctx.scalar_likely_vars, + &family, + var.version, + ) + { + evidence.scalar_likely = 1; + } + if evidence.bool_like == 0 + && signal_present_for_register_family(&evidence_ctx.bool_like_vars, &family, var.version) + { + evidence.bool_like = 1; + } + if evidence.width_bits == 0 + && let Some(bits) = + width_hint_for_register_family(&evidence_ctx.width_bits, &family, var.version) + { + evidence.width_bits = bits; + } + merge_initial_signature_type_evidence(initial_ty, &mut evidence); + evidence +} + +fn signal_present_for_register_family(keys: &HashSet, family: &str, version: u32) -> bool { + keys.iter() + .any(|key| key_matches_register_family_version(key, family, version)) +} + +fn width_hint_for_register_family( + hints: &HashMap, + family: &str, + version: u32, +) -> Option { + hints + .iter() + .filter(|(key, _)| key_matches_register_family_version(key, family, version)) + .map(|(_, bits)| *bits) + .filter(|bits| *bits > 0) + .min() +} + +fn key_matches_register_family_version(key: &str, family: &str, version: u32) -> bool { + let Some((name, version_str)) = key.rsplit_once('_') else { + return false; + }; + version_str.parse::().ok() == Some(version) && scalar_register_family_key(name) == family +} + +pub fn infer_signature_return_type( + func: &SSAFunction, + type_inference: &TypeInference, + ptr_bits: u32, + evidence_ctx: &SignatureTypeEvidenceContext, +) -> (CTypeLike, SignatureTypeEvidence) { + let mut candidates = Vec::new(); + let mut candidate_evidence = Vec::new(); + + for block in func.blocks() { + for op in &block.ops { + let SSAOp::Return { target } = op else { + continue; + }; + + let target_name = target.name.to_ascii_lowercase(); + if target_name.starts_with("xmm0") || target_name.starts_with("st0") { + let bits = if target.size.saturating_mul(8) <= 32 { + 32 + } else { + 64 + }; + let ty = CTypeLike::Float(bits); + let mut evidence = SignatureTypeEvidence::default(); + merge_initial_signature_type_evidence(&ty, &mut evidence); + evidence.width_bits = bits; + candidates.push(ty); + candidate_evidence.push(evidence); + continue; + } + + let initial_ty = type_inference.get_type(target); + let evidence = + collect_signature_type_evidence_for_var(evidence_ctx, target, &initial_ty); + let ty = resolve_evidence_driven_signature_type( + initial_ty, + target.size, + ptr_bits, + &evidence, + ); + candidates.push(ty); + candidate_evidence.push(evidence); + } + } + + if candidates.is_empty() { + return (CTypeLike::Void, SignatureTypeEvidence::default()); + } + + let mut meaningful: Vec = candidates + .iter() + .filter(|ty| !matches!(ty, CTypeLike::Unknown)) + .cloned() + .collect(); + if meaningful.is_empty() { + let fallback_evidence = candidate_evidence.into_iter().next().unwrap_or_default(); + return ( + fallback_scalar_type_like((ptr_bits / 8).max(1), &fallback_evidence, ptr_bits), + fallback_evidence, + ); + } + if meaningful.iter().all(|ty| ty == &meaningful[0]) { + return ( + meaningful.remove(0), + candidate_evidence.into_iter().next().unwrap_or_default(), + ); + } + if let Some(float_ty) = meaningful + .iter() + .find(|ty| matches!(ty, CTypeLike::Float(_))) + .cloned() + { + let evidence = candidate_evidence + .into_iter() + .find(|e| e.width_bits >= 32) + .unwrap_or_default(); + return (float_ty, evidence); + } + let evidence = candidate_evidence.into_iter().next().unwrap_or_default(); + (meaningful.remove(0), evidence) +} + +fn canonical_x86_64_arg_reg(name: &str) -> Option<&'static str> { + match name.to_ascii_lowercase().as_str() { + "rdi" | "edi" | "di" | "dil" => Some("rdi"), + "rsi" | "esi" | "si" | "sil" => Some("rsi"), + "rdx" | "edx" | "dx" | "dl" | "dh" => Some("rdx"), + "rcx" | "ecx" | "cx" | "cl" | "ch" => Some("rcx"), + "r8" | "r8d" | "r8w" | "r8b" => Some("r8"), + "r9" | "r9d" | "r9w" | "r9b" => Some("r9"), + _ => None, + } +} + +pub fn collect_version0_input_regs(func: &SSAFunction) -> HashMap { + let mut counts = HashMap::new(); + for block in func.blocks() { + for op in &block.ops { + for src in op.sources() { + if src.version != 0 { + continue; + } + if src.name.starts_with("tmp:") || src.name.starts_with("const:") { + continue; + } + let key = src.name.to_ascii_lowercase(); + *counts.entry(key).or_insert(0) += 1; + } + } + } + counts +} + +fn infer_callconv_x86_64_from_counts(counts: &HashMap) -> (&'static str, u8) { + let mut canonical = std::collections::BTreeMap::new(); + for (reg, count) in counts { + if let Some(name) = canonical_x86_64_arg_reg(reg) { + *canonical.entry(name).or_insert(0u32) += *count; + } + } + + let rdi = *canonical.get("rdi").unwrap_or(&0); + let rsi = *canonical.get("rsi").unwrap_or(&0); + let rcx = *canonical.get("rcx").unwrap_or(&0); + let rdx = *canonical.get("rdx").unwrap_or(&0); + let r8 = *canonical.get("r8").unwrap_or(&0); + let r9 = *canonical.get("r9").unwrap_or(&0); + + let sysv_primary = rdi + rsi; + let sysv_total = rdi + rsi + rdx + rcx + r8 + r9; + let ms_total = rcx + rdx + r8 + r9; + let ms_regs_used = [rcx, rdx, r8, r9].iter().filter(|&&v| v > 0).count(); + let ms_dominant = sysv_primary == 0 + && rcx > 0 + && ms_regs_used >= 2 + && ms_total >= 3 + && ms_total >= (rdi + rsi + rdx + 1); + + if ms_dominant { + let confidence = if ms_total >= 3 { 90 } else { 76 }; + ("ms", confidence) + } else { + let confidence = if sysv_primary > 0 { + 92 + } else if sysv_total > 0 { + 76 + } else { + 60 + }; + ("amd64", confidence) + } +} + +pub fn compute_callconv_inference( + arch_name: &str, + input_counts: &HashMap, +) -> (String, u8) { + match arch_name { + "x86-64" => { + let (callconv, confidence) = infer_callconv_x86_64_from_counts(input_counts); + (callconv.to_string(), confidence) + } + "x86" => ("cdecl".to_string(), 64), + _ => (String::new(), 0), + } +} + +fn is_informative_type(ty: &CTypeLike) -> bool { + !matches!(ty, CTypeLike::Void | CTypeLike::Unknown) +} + +pub fn compute_signature_confidence( + params: &[SignatureParamCandidate], + ret_type: &CTypeLike, + ret_evidence: &SignatureTypeEvidence, +) -> u8 { + let mut confidence: i32 = 48; + if !params.is_empty() { + confidence += 8; + } + + for param in params { + let evidence = ¶m.evidence; + if evidence.pointer_proven > 0 || evidence.scalar_proven > 0 { + confidence += 6; + } else if evidence.bool_like > 0 + || evidence.pointer_likely > 0 + || evidence.scalar_likely > 0 + { + confidence += 3; + } else if is_informative_type(¶m.ty) { + confidence += 2; + } else { + confidence -= 2; + } + + if evidence.has_conflict() { + confidence -= 4; + } + } + + if is_informative_type(ret_type) { + confidence += 4; + if ret_evidence.pointer_proven > 0 + || ret_evidence.scalar_proven > 0 + || ret_evidence.bool_like > 0 + { + confidence += 2; + } + } else if ret_evidence.has_pointer_signal() || ret_evidence.has_scalar_signal() { + confidence += 2; + } + + if ret_evidence.has_conflict() { + confidence -= 3; + } + + confidence.clamp(0, 100) as u8 +} + +fn sanitize_c_identifier(name: &str) -> Option { + let mut out = String::with_capacity(name.len()); + for (idx, ch) in name.chars().enumerate() { + let mapped = if ch.is_ascii_alphanumeric() || ch == '_' { + ch + } else { + '_' + }; + if idx == 0 && mapped.is_ascii_digit() { + out.push('_'); + } + out.push(mapped); + } + let trimmed = out.trim_matches('_').to_string(); + if trimmed.is_empty() { + None + } else { + Some(trimmed) + } +} + +fn uniquify_name(base: String, used: &mut HashSet) -> String { + if used.insert(base.clone()) { + return base; + } + let mut index = 1usize; + loop { + let candidate = format!("{base}_{index}"); + if used.insert(candidate.clone()) { + return candidate; + } + index += 1; + } +} + +fn normalize_inferred_param_name( + raw_name: &str, + fallback_idx: usize, + used: &mut HashSet, +) -> String { + let fallback = format!("arg{}", fallback_idx); + let clean = sanitize_c_identifier(raw_name).unwrap_or_else(|| fallback.clone()); + let clean = if clean.is_empty() { fallback } else { clean }; + uniquify_name(clean, used) +} + +pub fn format_afs_signature( + function_name: &str, + ret_type: &str, + params: &[InferredSignatureParam], +) -> String { + let params_str = if params.is_empty() { + "void".to_string() + } else { + params + .iter() + .map(|p| format!("{} {}", p.param_type, p.name)) + .collect::>() + .join(", ") + }; + format!("{ret_type} {function_name} ({params_str})") +} + +pub fn build_inferred_signature( + function_name: &str, + arch_name: &str, + ptr_bits: u32, + params: &[SignatureParamCandidate], + ret_type: &CTypeLike, + ret_evidence: &SignatureTypeEvidence, + input_counts: &HashMap, +) -> InferredSignature { + let mut ordered = params.to_vec(); + ordered.sort_by(|a, b| { + a.arg_index + .cmp(&b.arg_index) + .then_with(|| a.name.cmp(&b.name)) + }); + let mut used_param_names = HashSet::new(); + let json_params = ordered + .iter() + .enumerate() + .map(|(idx, p)| { + let fallback_idx = if p.arg_index == usize::MAX { + idx + } else { + p.arg_index + }; + InferredSignatureParam { + name: normalize_inferred_param_name(&p.name, fallback_idx, &mut used_param_names), + param_type: render_signature_type(&p.ty, ptr_bits), + } + }) + .collect::>(); + let rendered_ret = render_signature_type(ret_type, ptr_bits); + let (callconv, callconv_confidence) = compute_callconv_inference(arch_name, input_counts); + let confidence = compute_signature_confidence(&ordered, ret_type, ret_evidence); + InferredSignature { + function_name: function_name.to_string(), + signature: format_afs_signature(function_name, &rendered_ret, &json_params), + ret_type: rendered_ret, + params: json_params, + callconv, + arch: arch_name.to_string(), + confidence, + callconv_confidence, + } +} + +fn insert_known_signature_aliases( + known: &mut HashMap, + name: &str, + sig: &FunctionType, +) { + if name.is_empty() { + return; + } + known.insert(name.to_string(), sig.clone()); + + for prefix in ["sym.imp.", "sym.", "imp.", "dbg.", "fcn."] { + if let Some(stripped) = name.strip_prefix(prefix) + && !stripped.is_empty() + { + known.insert(stripped.to_string(), sig.clone()); + } + } +} + +fn normalize_signature_registry_name(name: &str) -> Option<&'static str> { + let normalized_owned = name.trim().to_ascii_lowercase(); + let mut normalized = normalized_owned.as_str(); + + for prefix in ["sym.imp.", "sym.", "imp.", "reloc.", "dbg."] { + while let Some(rest) = normalized.strip_prefix(prefix) { + normalized = rest; + } + } + + while let Some(rest) = normalized.strip_suffix("@plt") { + normalized = rest; + } + while let Some(rest) = normalized.strip_suffix(".plt") { + normalized = rest; + } + if let Some((base, _)) = normalized.split_once('@') { + normalized = base; + } + + if let Some(rest) = normalized.strip_prefix("__isoc99_") { + normalized = rest; + } + if let Some(rest) = normalized.strip_prefix("__gi_") { + normalized = rest; + } + + match normalized { + "strlen" | "__strlen_chk" => Some("strlen"), + "strcmp" => Some("strcmp"), + "memcmp" => Some("memcmp"), + "memcpy" | "__memcpy_chk" => Some("memcpy"), + "memset" => Some("memset"), + "malloc" | "__libc_malloc" | "__gi___libc_malloc" => Some("malloc"), + "free" => Some("free"), + "puts" => Some("puts"), + "printf" | "__printf_chk" => Some("printf"), + "exit" | "_exit" => Some("exit"), + _ => { + if normalized.starts_with("strlen") { + Some("strlen") + } else if normalized.starts_with("strcmp") { + Some("strcmp") + } else if normalized.starts_with("memcmp") { + Some("memcmp") + } else if normalized.starts_with("memcpy") { + Some("memcpy") + } else if normalized.starts_with("memset") { + Some("memset") + } else if normalized.starts_with("printf") || normalized == "__printf_chk" { + Some("printf") + } else if normalized.starts_with("puts") { + Some("puts") + } else if normalized == "malloc" || normalized.ends_with("malloc") { + Some("malloc") + } else if normalized == "free" || normalized.ends_with("free") { + Some("free") + } else if normalized.starts_with("exit") { + Some("exit") + } else { + None + } + } + } +} + +pub fn enrich_known_function_signatures_from_names( + type_facts: &mut FunctionTypeFacts, + function_names: &HashMap, + ptr_bits: u32, +) { + let registry = SignatureRegistry::from_embedded_json(); + + for name in function_names.values() { + let mut candidates = vec![name.clone()]; + if let Some(sim_name) = normalize_signature_registry_name(name) + && sim_name != name + && !candidates.iter().any(|candidate| candidate == sim_name) + { + candidates.push(sim_name.to_string()); + } + + let resolved = candidates.into_iter().find_map(|candidate| { + let mut arena = crate::TypeArena::default(); + registry + .resolve(&candidate, &mut arena, ptr_bits) + .map(|sig| { + ( + candidate, + FunctionType { + return_type: crate::to_c_type_like(&arena, sig.ret), + params: sig + .params + .into_iter() + .map(|param| crate::to_c_type_like(&arena, param)) + .collect(), + variadic: sig.variadic, + }, + ) + }) + }); + + let Some((resolved_name, sig)) = resolved else { + continue; + }; + + insert_known_signature_aliases(&mut type_facts.known_function_signatures, name, &sig); + if resolved_name != *name { + insert_known_signature_aliases( + &mut type_facts.known_function_signatures, + &resolved_name, + &sig, + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn enrich_known_function_signatures_moves_registry_alias_policy_upstream() { + let mut facts = FunctionTypeFacts::default(); + let names = HashMap::from([(0u64, "sym.imp.__printf_chk".to_string())]); + + enrich_known_function_signatures_from_names(&mut facts, &names, 64); + + assert!( + facts + .known_function_signatures + .contains_key("sym.imp.__printf_chk") + ); + assert!(facts.known_function_signatures.contains_key("__printf_chk")); + assert!(facts.known_function_signatures.contains_key("printf")); + } +} diff --git a/crates/r2types/src/writeback.rs b/crates/r2types/src/writeback.rs index bfee199..0fdb13a 100644 --- a/crates/r2types/src/writeback.rs +++ b/crates/r2types/src/writeback.rs @@ -1,4 +1,5 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::hash::{Hash, Hasher}; use r2ssa::{ FunctionSemanticSummary, InterprocSummarySet, SSABlock, SSAOp, SSAVar, SummaryArgEffect, @@ -18,9 +19,11 @@ use crate::facts::{ CalleeArgEffect, CalleeFact, CalleeMemoryEffect, CalleeMemoryEffectKind, CalleeMemoryLocation, CalleeMemoryRange, CalleeMemoryRegion, CalleeReturnRelation, FunctionParamSpec, FunctionSignatureSpec, FunctionTypeFactInputs, FunctionTypeFacts, InterprocFactDiagnostics, - VisibleBinding, VisibleBindingKind, parse_type_like_spec, + LocalFieldAccessFact, VisibleBinding, VisibleBindingKind, parse_type_like_spec, }; +use crate::function_facts::{FunctionFacts, InterprocSummaryView}; use crate::model::Signedness; +use crate::prepare::recover_vars_arch_profile; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WritebackSource { @@ -103,13 +106,15 @@ pub struct InferredSignature { pub callconv_confidence: u8, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct RecoveredVariable { pub name: String, pub kind: String, pub delta: i64, + #[serde(rename = "type")] pub var_type: String, pub isarg: bool, + #[serde(skip_serializing_if = "Option::is_none")] pub reg: Option, } @@ -188,6 +193,7 @@ pub struct TypeWritebackPlan { #[derive(Debug, Clone, PartialEq, Eq)] pub struct TypeWritebackAnalysis { pub signature: InferredSignature, + pub function_facts: FunctionFacts, pub type_facts: FunctionTypeFacts, pub plan: TypeWritebackPlan, } @@ -204,12 +210,62 @@ pub struct TypeWritebackAnalysisInput<'a> { pub diagnostics: TypeWritebackDiagnostics, } +pub struct TypeWritebackSemanticInputs<'a> { + pub artifact: &'a r2sym::SemanticArtifact, + pub local_field_accesses: &'a [LocalFieldAccessFact], +} + #[derive(Debug, Clone, Default)] struct SignatureContextMaps { param_types: HashMap, param_names: HashMap, } +#[derive(Debug, Clone, Default, PartialEq, Eq)] +struct SemanticTypeProjection { + pointer_param_indices: BTreeSet, + out_param_indices: BTreeSet, + slot_field_profiles: BTreeMap>, +} + +impl SemanticTypeProjection { + fn from_inputs( + summary_view: &InterprocSummaryView, + semantic_artifact: Option<&r2sym::SemanticArtifact>, + ptr_bits: u32, + ) -> Self { + let mut projection = Self::default(); + if let Some(summary) = summary_view.root_summary() { + for idx in 0..=summary.arg_effects.keys().copied().max().unwrap_or(0) { + if summary_suggests_pointer_param(summary, idx) { + projection.pointer_param_indices.insert(idx); + } + } + for (idx, effect) in &summary.arg_effects { + if effect.write || effect.escape { + projection.out_param_indices.insert(*idx); + } + } + } + projection.slot_field_profiles = + collect_semantic_slot_profiles(semantic_artifact, ptr_bits); + projection + } + + fn corroborates_param_type_hint(&self, index: usize, hint: &CTypeLike) -> bool { + if !matches!(hint, CTypeLike::Pointer(_)) { + return false; + } + self.pointer_param_indices.contains(&index) + || self.out_param_indices.contains(&index) + || self.slot_field_profiles.contains_key(&index) + } + + fn corroborates_stack_slot_type_hint(&self, slot: usize, hint: &CTypeLike) -> bool { + matches!(hint, CTypeLike::Pointer(_)) && self.slot_field_profiles.contains_key(&slot) + } +} + struct VarTypeCandidateContext<'a> { current_context_maps: &'a SignatureContextMaps, merged_signature: Option<&'a FunctionSignatureSpec>, @@ -426,22 +482,90 @@ fn maybe_upgrade_param_to_pointer( } } -fn apply_interproc_summary_to_signature( +fn upgrade_param_indices_to_pointer( + indices: impl IntoIterator, merged_signature: &mut Option, inferred_signature: &mut InferredSignature, - summary_set: Option<&InterprocSummarySet>, ptr_bits: u32, ) { - let Some(summary_set) = summary_set else { - return; - }; - let Some(root) = summary_set.root else { + let pointer_ty = CTypeLike::Pointer(Box::new(CTypeLike::Void)); + + if merged_signature.is_none() { + *merged_signature = inferred_signature_to_spec(inferred_signature, ptr_bits); + } + + let Some(signature) = merged_signature.as_mut() else { return; }; - let Some(summary) = summary_set.summaries.get(&root) else { + + for idx in indices { + let merged_param = signature.params.get_mut(idx); + let inferred_param = inferred_signature.params.get_mut(idx); + + let merged_is_generic = merged_param.as_ref().is_some_and(|param| { + param.ty.as_ref().is_none_or(|ty| { + is_generic_signature_type(Some(ty)) + || matches!( + ty, + CTypeLike::Int { + bits, + signedness: Signedness::Signed + | Signedness::Unsigned + | Signedness::Unknown, + } if *bits == ptr_bits + ) + }) + }); + + let inferred_is_generic = inferred_param.as_ref().is_some_and(|param| { + is_generic_type_string(¶m.param_type) + || matches!( + parse_type_like_spec(¶m.param_type, ptr_bits), + Some(CTypeLike::Int { + bits, + signedness: Signedness::Signed + | Signedness::Unsigned + | Signedness::Unknown, + }) if bits == ptr_bits + ) + }); + + if merged_is_generic && let Some(param) = merged_param { + param.ty = Some(pointer_ty.clone()); + } + if inferred_is_generic && let Some(param) = inferred_param { + param.param_type = render_signature_type(&pointer_ty, ptr_bits); + } + } +} + +fn apply_interproc_summary_to_signature( + merged_signature: &mut Option, + inferred_signature: &mut InferredSignature, + summary_view: &InterprocSummaryView, + semantic_projection: Option<&SemanticTypeProjection>, + ptr_bits: u32, +) { + let Some(summary) = summary_view.root_summary() else { + if let Some(projection) = semantic_projection { + upgrade_param_indices_to_pointer( + projection.pointer_param_indices.iter().copied(), + merged_signature, + inferred_signature, + ptr_bits, + ); + } return; }; maybe_upgrade_param_to_pointer(summary, merged_signature, inferred_signature, ptr_bits); + if let Some(projection) = semantic_projection { + upgrade_param_indices_to_pointer( + projection.pointer_param_indices.iter().copied(), + merged_signature, + inferred_signature, + ptr_bits, + ); + } let Some(ret_ty) = infer_interproc_return_type( summary, merged_signature.as_ref(), @@ -491,8 +615,249 @@ fn apply_interproc_summary_to_signature( } } -pub fn build_type_writeback_analysis( +fn assumption_type_hint( + assumption: &r2ssa::AnalysisAssumption, + ptr_bits: u32, +) -> Option { + let r2ssa::AssumptionValue::TypeHint { ty } = &assumption.value else { + return None; + }; + parse_type_like_spec(ty, ptr_bits) +} + +fn type_hint_conflicts(existing: &CTypeLike, hint: &CTypeLike, ptr_bits: u32) -> bool { + render_signature_type(existing, ptr_bits) != render_signature_type(hint, ptr_bits) +} + +fn apply_type_hint_to_signature_param( + merged_signature: &mut Option, + inferred_signature: &mut InferredSignature, + index: usize, + hint: &CTypeLike, + ptr_bits: u32, +) -> Result { + if merged_signature.is_none() { + *merged_signature = inferred_signature_to_spec(inferred_signature, ptr_bits); + } + + let mut applied = false; + if let Some(signature) = merged_signature.as_mut() { + let Some(param) = signature.params.get_mut(index) else { + return Ok(false); + }; + match param.ty.as_ref() { + None => { + param.ty = Some(hint.clone()); + applied = true; + } + Some(existing) if is_generic_signature_type(Some(existing)) => { + param.ty = Some(hint.clone()); + applied = true; + } + Some(existing) if !type_hint_conflicts(existing, hint, ptr_bits) => { + applied = true; + } + Some(existing) => { + return Err(format!( + "parameter {} already has incompatible type {}", + index, + render_signature_type(existing, ptr_bits) + )); + } + } + } + + if let Some(param) = inferred_signature.params.get_mut(index) + && is_generic_type_string(¶m.param_type) + { + param.param_type = render_signature_type(hint, ptr_bits); + applied = true; + } + + Ok(applied) +} + +fn assumption_stack_base(base: &str) -> Option { + match base.trim().to_ascii_lowercase().as_str() { + "bp" | "rbp" | "ebp" | "frame" | "fp" => Some(ExternalStackBase::FramePointer), + "sp" | "rsp" | "esp" | "stack" => Some(ExternalStackBase::StackPointer), + "" => None, + other => Some(ExternalStackBase::Named(other.to_string())), + } +} + +fn apply_type_hint_assumptions_to_context( + parsed_context: &mut ParsedExternalContext, + inferred_signature: &mut InferredSignature, + ptr_bits: u32, + semantic_projection: Option<&SemanticTypeProjection>, +) -> r2ssa::AssumptionUsageReport { + let mut usage = r2ssa::AssumptionUsageReport::default(); + let assumptions = parsed_context.assumptions.items.clone(); + for assumption in &assumptions { + let Some(hint) = assumption_type_hint(assumption, ptr_bits) else { + continue; + }; + match &assumption.subject { + r2ssa::AssumptionSubject::Parameter { index } => { + let corroborated = semantic_projection.is_some_and(|projection| { + projection.corroborates_param_type_hint(*index, &hint) + }); + if !corroborated { + usage.mark_ignored(assumption); + continue; + } + match apply_type_hint_to_signature_param( + &mut parsed_context.merged_signature, + inferred_signature, + *index, + &hint, + ptr_bits, + ) { + Ok(true) => usage.mark_applied(assumption), + Ok(false) => usage.mark_ignored(assumption), + Err(reason) => usage.mark_conflict(assumption, reason), + } + } + r2ssa::AssumptionSubject::Register { name } => { + let Some((idx, reg_param)) = parsed_context + .register_params + .iter_mut() + .enumerate() + .find(|(_, param)| { + param.reg.eq_ignore_ascii_case(name) + || param.name.eq_ignore_ascii_case(name) + }) + else { + usage.mark_ignored(assumption); + continue; + }; + let corroborated = semantic_projection + .is_some_and(|projection| projection.corroborates_param_type_hint(idx, &hint)); + if !corroborated { + usage.mark_ignored(assumption); + continue; + } + + let reg_applied = match reg_param.ty.as_ref() { + None => { + reg_param.ty = Some(hint.clone()); + true + } + Some(existing) if is_generic_signature_type(Some(existing)) => { + reg_param.ty = Some(hint.clone()); + true + } + Some(existing) if !type_hint_conflicts(existing, &hint, ptr_bits) => true, + Some(existing) => { + usage.mark_conflict( + assumption, + format!( + "register {} already has incompatible type {}", + name, + render_signature_type(existing, ptr_bits) + ), + ); + continue; + } + }; + match apply_type_hint_to_signature_param( + &mut parsed_context.merged_signature, + inferred_signature, + idx, + &hint, + ptr_bits, + ) { + Ok(true) => usage.mark_applied(assumption), + Ok(false) if reg_applied => usage.mark_applied(assumption), + Ok(false) => usage.mark_ignored(assumption), + Err(reason) => usage.mark_conflict(assumption, reason), + } + } + r2ssa::AssumptionSubject::StackSlot { base, offset } => { + let Some(base) = assumption_stack_base(base) else { + usage.mark_ignored(assumption); + continue; + }; + let key = StackSlotKey { + base, + offset: *offset, + }; + let corroborated = semantic_projection.is_some_and(|projection| { + parsed_context + .stack_slots + .get(&key) + .and_then(|slot| slot.param_index) + .is_some_and(|slot| { + projection.corroborates_stack_slot_type_hint(slot, &hint) + }) + }); + if !corroborated { + usage.mark_ignored(assumption); + continue; + } + let Some(slot) = parsed_context.stack_slots.get_mut(&key) else { + usage.mark_ignored(assumption); + continue; + }; + let mut applied = match slot.ty.as_ref() { + None => { + slot.ty = Some(hint.clone()); + true + } + Some(existing) if is_generic_signature_type(Some(existing)) => { + slot.ty = Some(hint.clone()); + true + } + Some(existing) if !type_hint_conflicts(existing, &hint, ptr_bits) => true, + Some(existing) => { + usage.mark_conflict( + assumption, + format!( + "stack slot {}@{} already has incompatible type {}", + match &key.base { + ExternalStackBase::FramePointer => "bp", + ExternalStackBase::StackPointer => "sp", + ExternalStackBase::Named(name) => name.as_str(), + }, + key.offset, + render_signature_type(existing, ptr_bits) + ), + ); + continue; + } + }; + + if let Some(index) = slot.param_index { + match apply_type_hint_to_signature_param( + &mut parsed_context.merged_signature, + inferred_signature, + index, + &hint, + ptr_bits, + ) { + Ok(result) => applied |= result, + Err(reason) => { + usage.mark_conflict(assumption, reason); + continue; + } + } + } + if applied { + usage.mark_applied(assumption); + } else { + usage.mark_ignored(assumption); + } + } + _ => {} + } + } + usage +} + +fn build_type_writeback_analysis_inner( mut input: TypeWritebackAnalysisInput<'_>, + semantic_inputs: Option>, ) -> TypeWritebackAnalysis { if input.parsed_context.stack_slots.is_empty() && !input.parsed_context.external_stack_vars.is_empty() @@ -500,6 +865,20 @@ pub fn build_type_writeback_analysis( input.parsed_context.stack_slots = stack_slots_from_legacy_external_stack_vars(&input.parsed_context.external_stack_vars); } + let summary_view = InterprocSummaryView::new(input.interproc_summary_set.clone()); + + let semantic_projection = SemanticTypeProjection::from_inputs( + &summary_view, + semantic_inputs.as_ref().map(|semantic| semantic.artifact), + input.ptr_bits, + ); + + let type_assumption_usage = apply_type_hint_assumptions_to_context( + &mut input.parsed_context, + &mut input.inferred_signature, + input.ptr_bits, + Some(&semantic_projection), + ); let mut merged_signature = merge_local_signature_into_merged_signature( input.parsed_context.merged_signature.clone(), @@ -522,18 +901,40 @@ pub fn build_type_writeback_analysis( apply_interproc_summary_to_signature( &mut merged_signature, &mut input.inferred_signature, - input.interproc_summary_set.as_ref(), + &summary_view, + Some(&semantic_projection), input.ptr_bits, ); let mut diagnostics = input.diagnostics; diagnostics.solver_warnings = input.parsed_context.diagnostics.clone(); + if summary_view + .diagnostics() + .is_some_and(|diagnostics| !diagnostics.converged) + { + diagnostics.warnings.push( + "interprocedural summary did not converge; downgraded summary-driven type hints" + .to_string(), + ); + } let external_structs = collect_external_struct_candidates_from_db( &input.parsed_context.external_type_db, input.ptr_bits, ); let mut local_structs = input.local_structs; + augment_local_struct_artifacts_with_projection( + &mut local_structs, + &semantic_projection, + input.ptr_bits, + ); + if let Some(semantic) = semantic_inputs.as_ref() { + augment_local_struct_artifacts_with_local_field_accesses( + &mut local_structs, + semantic.local_field_accesses, + input.ptr_bits, + ); + } align_local_structs_with_external( &mut local_structs.struct_decls, &mut local_structs.slot_type_overrides, @@ -601,7 +1002,7 @@ pub fn build_type_writeback_analysis( &var_rename_candidates, input.ptr_bits, ); - let type_facts = FunctionTypeFacts::builder(FunctionTypeFactInputs { + let mut type_facts = FunctionTypeFacts::builder(FunctionTypeFactInputs { merged_signature: merged_signature.clone(), known_function_signatures: input.parsed_context.known_function_signatures.clone(), register_params: input.parsed_context.register_params.clone(), @@ -618,52 +1019,1062 @@ pub fn build_type_writeback_analysis( .map(|(id, summary)| (id.0, summary_to_callee_fact(summary))) .collect() }) - .unwrap_or_default(), - external_stack_vars: if input.parsed_context.stack_slots.is_empty() { - input.parsed_context.external_stack_vars.clone() - } else { - HashMap::new() - }, - external_type_db: type_db, - slot_type_overrides: local_structs.slot_type_overrides.clone(), - slot_field_profiles: local_structs.slot_field_profiles.clone(), - interproc_diagnostics: input - .interproc_summary_set - .as_ref() - .map(|summary_set| InterprocFactDiagnostics { - iterations: summary_set.diagnostics.iterations, - max_iterations: summary_set.diagnostics.max_iterations, - converged: summary_set.diagnostics.converged, - scope_size: summary_set.diagnostics.scope_size, - scc_count: summary_set.diagnostics.scc_count, - max_scc_size: summary_set.diagnostics.max_scc_size, + .unwrap_or_default(), + external_stack_vars: if input.parsed_context.stack_slots.is_empty() { + input.parsed_context.external_stack_vars.clone() + } else { + HashMap::new() + }, + external_type_db: type_db, + slot_type_overrides: local_structs.slot_type_overrides.clone(), + slot_field_profiles: local_structs.slot_field_profiles.clone(), + local_field_accesses: semantic_inputs + .as_ref() + .map(|semantic| semantic.local_field_accesses.to_vec()) + .unwrap_or_default(), + interproc_diagnostics: input + .interproc_summary_set + .as_ref() + .map(|summary_set| InterprocFactDiagnostics { + iterations: summary_set.diagnostics.iterations, + max_iterations: summary_set.diagnostics.max_iterations, + converged: summary_set.diagnostics.converged, + scope_size: summary_set.diagnostics.scope_size, + scc_count: summary_set.diagnostics.scc_count, + max_scc_size: summary_set.diagnostics.max_scc_size, + }) + .unwrap_or_default(), + diagnostics: diagnostics.solver_warnings.clone(), + }) + .build(); + if let Some(semantic) = semantic_inputs.as_ref() + && semantic.artifact.diagnostics.branches_pruned > 0 + { + type_facts.diagnostics.push(format!( + "symbolic pruned {} branch arm(s)", + semantic.artifact.diagnostics.branches_pruned + )); + } + let global_type_links = score_global_type_links( + input.ssa_blocks, + &struct_decls, + &var_type_candidates, + input.ptr_bits, + ); + + let plan = TypeWritebackPlan { + signature: input.inferred_signature.clone(), + var_type_candidates, + var_rename_candidates, + struct_decls: struct_decls.clone(), + global_type_links, + diagnostics: diagnostics.clone(), + }; + + TypeWritebackAnalysis { + signature: input.inferred_signature, + function_facts: FunctionFacts::new( + type_facts.clone(), + semantic_inputs + .as_ref() + .map(|semantic| semantic.artifact.clone()), + ) + .with_assumptions(input.parsed_context.assumptions.clone()) + .with_summary_view(summary_view) + .with_diagnostics(type_facts.diagnostics.clone()) + .with_assumption_usage(type_assumption_usage), + type_facts, + plan, + } +} + +pub fn build_type_writeback_analysis( + input: TypeWritebackAnalysisInput<'_>, +) -> TypeWritebackAnalysis { + build_type_writeback_analysis_inner(input, None) +} + +pub fn build_type_writeback_analysis_with_semantics( + input: TypeWritebackAnalysisInput<'_>, + semantic_inputs: TypeWritebackSemanticInputs<'_>, +) -> TypeWritebackAnalysis { + build_type_writeback_analysis_inner(input, Some(semantic_inputs)) +} + +fn collect_semantic_slot_profiles( + artifact: Option<&r2sym::SemanticArtifact>, + ptr_bits: u32, +) -> BTreeMap> { + fn reliable_post_memory_terms( + region: &r2sym::SemanticRegion, + ) -> impl Iterator { + region + .post + .iter() + .filter(|predicate| predicate.evidence.is_reliable()) + .filter_map(|predicate| predicate.value.compiled.as_ref()) + .filter(|compiled| compiled.evidence().is_reliable()) + .flat_map(|compiled| compiled.memory_terms.iter()) + } + + fn has_reliable_preconditions(region: &r2sym::SemanticRegion) -> bool { + region + .pre + .iter() + .any(|predicate| predicate.evidence.is_reliable()) + } + + fn has_decisive_target_support(region: &r2sym::SemanticRegion) -> bool { + region.actionable_reachable_target().is_some() || { + let actionable_targets = region + .targets + .iter() + .filter(|fact| fact.evidence.allows_narrowing()) + .filter(|fact| { + matches!( + fact.value.status, + r2sym::SymbolicReachabilityStatus::Reachable + ) + }) + .map(|fact| fact.value.target) + .collect::>(); + actionable_targets.len() == 1 + } + } + + fn supports_conservative_type_projection(region: &r2sym::SemanticRegion) -> bool { + let decisive_target = has_decisive_target_support(region); + let has_post_support = region.post.iter().any(|predicate| { + predicate.evidence.allows_narrowing() + && predicate + .value + .compiled + .as_ref() + .is_some_and(|compiled| compiled.evidence().is_reliable()) + }); + if !decisive_target && !has_post_support { + return false; + } + if has_reliable_preconditions(region) && !decisive_target { + return false; + } + true + } + + let Some(artifact) = artifact else { + return BTreeMap::new(); + }; + if artifact.vm_summary_only_type_plan() { + return BTreeMap::new(); + } + let Some(native) = artifact.native_body() else { + return BTreeMap::new(); + }; + + let mut projected_profiles = BTreeMap::>::new(); + for region in native.regions.values() { + if !supports_conservative_type_projection(region) { + continue; + } + for term in region + .memory + .iter() + .filter(|memory| memory.evidence.is_reliable()) + .map(|memory| &memory.value.term) + { + let Some((slot, offset, field_type)) = backward_memory_term_slot_field(term, ptr_bits) + else { + continue; + }; + projected_profiles + .entry(slot) + .or_default() + .entry(offset) + .or_insert(field_type); + } + for term in reliable_post_memory_terms(region) { + let Some((slot, offset, field_type)) = backward_memory_term_slot_field(term, ptr_bits) + else { + continue; + }; + projected_profiles + .entry(slot) + .or_default() + .entry(offset) + .or_insert(field_type); + } + } + + if projected_profiles.is_empty() { + for region in native.regions.values() { + if !supports_conservative_type_projection(region) { + continue; + } + let Some(target) = region.actionable_reachable_target() else { + continue; + }; + for compiled in region + .control + .iter() + .filter(|fact| fact.evidence.allows_narrowing()) + .filter(|fact| fact.value.target == target) + .filter_map(|fact| fact.value.compiled.as_ref()) + { + if !compiled.evidence().is_reliable() { + continue; + } + for term in &compiled.memory_terms { + let Some((slot, offset, field_type)) = + backward_memory_term_slot_field(term, ptr_bits) + else { + continue; + }; + projected_profiles + .entry(slot) + .or_default() + .entry(offset) + .or_insert(field_type); + } + } + } + } + + projected_profiles +} + +fn semantic_stage_label(artifact: &r2sym::SemanticArtifact) -> &'static str { + match (artifact.execution, artifact.stage, artifact.granularity) { + (r2sym::ExecutionModel::Vm, _, _) => "vm_summary", + (_, r2sym::RefinementStage::Raw, _) => "raw", + (_, r2sym::RefinementStage::Compiled, r2sym::ArtifactGranularity::Regioned) => { + "island_compiled" + } + (_, r2sym::RefinementStage::Compiled, _) => "compiled", + (_, r2sym::RefinementStage::Residual, _) => "residual", + } +} + +fn semantic_slice_class_label(slice_class: r2sym::SliceClass) -> &'static str { + match slice_class { + r2sym::SliceClass::Wrapper => "wrapper", + r2sym::SliceClass::Worker => "worker", + r2sym::SliceClass::RecursiveGroup => "recursive_group", + r2sym::SliceClass::InterpreterSwitch => "interpreter_switch", + r2sym::SliceClass::InterpreterIndirect => "interpreter_indirect", + r2sym::SliceClass::GenericLarge => "generic_large", + } +} + +fn semantic_residual_reason_label(reason: r2sym::ResidualReason) -> &'static str { + match reason { + r2sym::ResidualReason::MissingArch => "missing_arch", + r2sym::ResidualReason::LargeCfg => "large_cfg", + r2sym::ResidualReason::SummaryBudgetExhausted => "summary_budget_exhausted", + r2sym::ResidualReason::SccBudgetExhausted => "scc_budget_exhausted", + r2sym::ResidualReason::InterpreterRequiresStepSummary => { + "interpreter_requires_step_summary" + } + } +} + +fn semantic_fallback_warning(artifact: &r2sym::SemanticArtifact) -> String { + let slice_class = artifact + .slice_class() + .map(semantic_slice_class_label) + .unwrap_or("unknown"); + let mode = semantic_stage_label(artifact); + let mut warning = format!("semantic fallback: {slice_class} slice in {mode} mode"); + if !artifact.diagnostics.residual_reasons.is_empty() { + warning.push_str(" ("); + warning.push_str( + &artifact + .diagnostics + .residual_reasons + .iter() + .map(|reason| semantic_residual_reason_label(*reason)) + .collect::>() + .join(", "), + ); + warning.push(')'); + } + if let Some(native) = artifact.native_body() + && !native.regions.is_empty() + { + warning.push_str(&format!( + "; regions={}, actionable_conditions={}, exact_conditions={}", + native.regions.len(), + native.actionable_control_count(), + native.exact_control_count(), + )); + } + warning +} + +pub fn semantic_artifact_prefers_bounded_type_plan(artifact: &r2sym::SemanticArtifact) -> bool { + if !artifact.type_plan().allows_native_augmentation() { + return true; + } + matches!( + artifact.stage, + r2sym::RefinementStage::Residual | r2sym::RefinementStage::Compiled + ) && artifact.diagnostics.skipped_large_cfg + && matches!(artifact.slice_class(), Some(r2sym::SliceClass::Worker)) + && artifact + .native_body() + .is_some_and(|body| !body.regions.is_empty()) + && (artifact.actionable_control_count() > 0 + || artifact + .actionable_regions() + .into_iter() + .any(|region| !region.actionable_memory_terms().is_empty())) +} + +pub fn build_semantic_type_fallback_plan( + function_name: &str, + arch_name: &str, + ptr_bits: u32, + artifact: &r2sym::SemanticArtifact, +) -> TypeWritebackPlan { + let mut warnings = vec![semantic_fallback_warning(artifact)]; + if !artifact.type_plan().allows_native_augmentation() { + warnings.push("type analysis not ready from semantic capability".to_string()); + } + let mut local_structs = LocalStructArtifacts::default(); + augment_local_struct_artifacts_with_semantics(&mut local_structs, artifact, ptr_bits); + let mut signature = InferredSignature { + function_name: function_name.to_string(), + signature: format!("void {}(void)", function_name), + ret_type: "void".to_string(), + params: Vec::new(), + callconv: "unknown".to_string(), + arch: arch_name.to_string(), + confidence: 0, + callconv_confidence: 0, + }; + if let Some(merged_signature) = merge_slot_type_overrides_into_signature( + inferred_signature_to_spec(&signature, ptr_bits), + &local_structs.slot_type_overrides, + ptr_bits, + ) { + apply_signature_context_overrides(&mut signature, Some(&merged_signature), ptr_bits); + } + if !local_structs.struct_decls.is_empty() { + warnings.push(format!( + "semantic regions projected {} struct candidate(s)", + local_structs.struct_decls.len() + )); + } + + TypeWritebackPlan { + signature, + var_type_candidates: Vec::new(), + var_rename_candidates: Vec::new(), + struct_decls: local_structs.struct_decls, + global_type_links: Vec::new(), + diagnostics: TypeWritebackDiagnostics { + conflicts: Vec::new(), + warnings, + solver_warnings: Vec::new(), + }, + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct LocalAddrExpr { + slot: usize, + offset: i64, + confidence: u8, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +struct InferredLocalFieldEvidence { + reads: u32, + writes: u32, + widths: BTreeMap, + type_votes: BTreeMap, +} + +type LocalFieldEvidenceMap = HashMap>; + +fn collect_pointer_arg_slot_map(arch_name: Option<&str>, ptr_bits: u32) -> HashMap { + let (arg_regs, _, _) = recover_vars_arch_profile(arch_name); + let arch_name = arch_name.unwrap_or_default().to_ascii_lowercase(); + let is_arm64 = arch_name.contains("aarch64") || arch_name.contains("arm64"); + let is_x86_64 = arch_name.contains("x86-64") + || arch_name.contains("x86_64") + || arch_name.contains("amd64") + || arch_name.contains("x64"); + let is_riscv64 = arch_name.contains("riscv64") || arch_name.contains("rv64"); + + let mut out = HashMap::new(); + for (idx, (canonical, aliases)) in arg_regs.iter().enumerate() { + let include_alias = |alias: &str| -> bool { + if ptr_bits <= 32 { + return true; + } + let alias = alias.to_ascii_lowercase(); + if is_arm64 { + return alias.starts_with('x'); + } + if is_x86_64 { + return alias.starts_with('r'); + } + if is_riscv64 { + return alias.starts_with('x') || alias.starts_with('a'); + } + alias == (*canonical).to_ascii_lowercase() + }; + + if include_alias(canonical) { + out.insert((*canonical).to_string(), idx); + } + for alias in *aliases { + if include_alias(alias) { + out.insert((*alias).to_string(), idx); + } + } + } + out +} + +fn parse_ssa_const_offset(name: &str, ptr_bits: u32) -> Option { + let val_str = name + .strip_prefix("const:") + .or_else(|| name.strip_prefix("CONST:"))?; + let val_str = val_str.split('_').next().unwrap_or(val_str); + + let raw = if let Some(hex) = val_str + .strip_prefix("0x") + .or_else(|| val_str.strip_prefix("0X")) + { + u64::from_str_radix(hex, 16).ok()? + } else if let Some(dec) = val_str + .strip_prefix("0d") + .or_else(|| val_str.strip_prefix("0D")) + { + dec.parse::().ok()? + } else { + u64::from_str_radix(val_str, 16).ok()? + }; + Some(signed_offset_from_const(raw, ptr_bits)) +} + +pub fn infer_local_struct_artifacts_from_ssa( + ssa_blocks: &[SSABlock], + arch_name: Option<&str>, + ptr_bits: u32, + diagnostics: &mut TypeWritebackDiagnostics, +) -> LocalStructArtifacts { + let pointer_arg_slot_map = collect_pointer_arg_slot_map(arch_name, ptr_bits); + let (_, stack_bases, frame_bases) = recover_vars_arch_profile(arch_name); + let mut addr_exprs: HashMap = HashMap::new(); + let mut stack_addr_offsets: HashMap = HashMap::new(); + let mut stack_slot_values: HashMap<(u64, i64), LocalAddrExpr> = HashMap::new(); + let mut slot_field_evidence: LocalFieldEvidenceMap = HashMap::new(); + let offset_bound = 0x4000i64; + let block_ops: HashMap> = ssa_blocks + .iter() + .map(|block| { + let ops = block + .ops + .iter() + .filter_map(|op| { + op.dst() + .map(|dst| (ssa_var_block_key(block.addr, dst), op.clone())) + }) + .collect::>(); + (block.addr, ops) + }) + .collect(); + + fn is_scaled_index_like( + block_addr: u64, + var: &SSAVar, + ops_by_block: &HashMap>, + addr_exprs: &HashMap, + depth: u32, + ) -> bool { + if depth > 8 || var.is_const() { + return false; + } + let key = ssa_var_block_key(block_addr, var); + if addr_exprs.contains_key(&key) { + return false; + } + let Some(op) = ops_by_block.get(&block_addr).and_then(|ops| ops.get(&key)) else { + return true; + }; + match op { + SSAOp::Copy { src, .. } + | SSAOp::Cast { src, .. } + | SSAOp::New { src, .. } + | SSAOp::IntZExt { src, .. } + | SSAOp::IntSExt { src, .. } + | SSAOp::Trunc { src, .. } + | SSAOp::Subpiece { src, .. } => { + is_scaled_index_like(block_addr, src, ops_by_block, addr_exprs, depth + 1) + } + SSAOp::IntMult { a, b, .. } => { + (parse_ssa_const_offset(&a.name, 64).is_some() + && is_scaled_index_like(block_addr, b, ops_by_block, addr_exprs, depth + 1)) + || (parse_ssa_const_offset(&b.name, 64).is_some() + && is_scaled_index_like(block_addr, a, ops_by_block, addr_exprs, depth + 1)) + } + SSAOp::IntLeft { a, b, .. } => { + parse_ssa_const_offset(&b.name, 64).is_some() + && is_scaled_index_like(block_addr, a, ops_by_block, addr_exprs, depth + 1) + } + SSAOp::IntSub { a, b, .. } => { + parse_ssa_const_offset(&a.name, 64) == Some(0) + && is_scaled_index_like(block_addr, b, ops_by_block, addr_exprs, depth + 1) + } + SSAOp::Load { .. } | SSAOp::Phi { .. } => true, + _ => false, + } + } + + for block in ssa_blocks { + for op in &block.ops { + op.for_each_source(&mut |src: &SSAVar| { + if src.version != 0 { + return; + } + let key = src.name.to_ascii_lowercase(); + if let Some(slot) = pointer_arg_slot_map.get(key.as_str()).copied() { + addr_exprs + .entry(ssa_var_block_key(block.addr, src)) + .or_insert(LocalAddrExpr { + slot, + offset: 0, + confidence: 92, + }); + } + }); + } + } + + let is_stack_base = |name: &str| stack_bases.contains(&name) || frame_bases.contains(&name); + + for _ in 0..6 { + let mut changed = false; + for block in ssa_blocks { + for op in &block.ops { + let addr_of = |var: &SSAVar, map: &HashMap| { + if var.version == 0 { + let key = var.name.to_ascii_lowercase(); + if let Some(slot) = pointer_arg_slot_map.get(key.as_str()).copied() { + return Some(LocalAddrExpr { + slot, + offset: 0, + confidence: 92, + }); + } + } + map.get(&ssa_var_block_key(block.addr, var)).copied() + }; + let stack_slot_of = |var: &SSAVar, stack_map: &HashMap| { + stack_map.get(&ssa_var_block_key(block.addr, var)).copied() + }; + let set_expr = + |dst: &SSAVar, + expr: LocalAddrExpr, + map: &mut HashMap| { + let key = ssa_var_block_key(block.addr, dst); + match map.get(&key).copied() { + Some(prev) if prev.confidence >= expr.confidence => false, + _ => { + map.insert(key, expr); + true + } + } + }; + let set_stack_slot = |dst: &SSAVar, offset: i64, map: &mut HashMap| { + let key = ssa_var_block_key(block.addr, dst); + match map.get(&key).copied() { + Some(prev) if prev == offset => false, + _ => { + map.insert(key, offset); + true + } + } + }; + + match op { + SSAOp::Copy { dst, src } + | SSAOp::Cast { dst, src } + | SSAOp::New { dst, src } + | SSAOp::IntZExt { dst, src } + | SSAOp::IntSExt { dst, src } => { + if let Some(mut expr) = addr_of(src, &addr_exprs) { + expr.confidence = expr.confidence.saturating_sub(2); + changed |= set_expr(dst, expr, &mut addr_exprs); + } + if let Some(offset) = stack_slot_of(src, &stack_addr_offsets) { + changed |= set_stack_slot(dst, offset, &mut stack_addr_offsets); + } + } + SSAOp::Phi { dst, sources } => { + let mut selected = None; + let mut selected_slot = None; + for src in sources { + let Some(expr) = addr_of(src, &addr_exprs) else { + selected = None; + break; + }; + selected = match selected { + None => Some(expr), + Some(prev) + if prev.slot == expr.slot && prev.offset == expr.offset => + { + Some(LocalAddrExpr { + slot: prev.slot, + offset: prev.offset, + confidence: prev.confidence.max(expr.confidence), + }) + } + _ => None, + }; + let Some(slot) = stack_slot_of(src, &stack_addr_offsets) else { + selected_slot = None; + break; + }; + selected_slot = match selected_slot { + None => Some(slot), + Some(prev) if prev == slot => Some(prev), + _ => None, + }; + if selected.is_none() { + break; + } + } + if let Some(mut expr) = selected { + expr.confidence = expr.confidence.saturating_sub(3); + changed |= set_expr(dst, expr, &mut addr_exprs); + } + if let Some(slot) = selected_slot { + changed |= set_stack_slot(dst, slot, &mut stack_addr_offsets); + } + } + SSAOp::IntAdd { dst, a, b } => { + if let Some(off) = parse_ssa_const_offset(&b.name, ptr_bits) { + let a_lower = a.name.to_ascii_lowercase(); + if is_stack_base(a_lower.as_str()) { + changed |= set_stack_slot(dst, off, &mut stack_addr_offsets); + } + } + if let Some(off) = parse_ssa_const_offset(&a.name, ptr_bits) { + let b_lower = b.name.to_ascii_lowercase(); + if is_stack_base(b_lower.as_str()) { + changed |= set_stack_slot(dst, off, &mut stack_addr_offsets); + } + } + if let Some(base) = addr_of(a, &addr_exprs) + && let Some(delta) = parse_ssa_const_offset(&b.name, ptr_bits) + { + let off = base.offset.saturating_add(delta); + if (-offset_bound..=offset_bound).contains(&off) { + changed |= set_expr( + dst, + LocalAddrExpr { + slot: base.slot, + offset: off, + confidence: base.confidence.saturating_sub(1), + }, + &mut addr_exprs, + ); + } + } else if let Some(base) = addr_of(a, &addr_exprs) + && is_scaled_index_like(block.addr, b, &block_ops, &addr_exprs, 0) + { + changed |= set_expr( + dst, + LocalAddrExpr { + slot: base.slot, + offset: base.offset, + confidence: base.confidence.saturating_sub(4), + }, + &mut addr_exprs, + ); + } else if let Some(base) = addr_of(b, &addr_exprs) + && let Some(delta) = parse_ssa_const_offset(&a.name, ptr_bits) + { + let off = base.offset.saturating_add(delta); + if (-offset_bound..=offset_bound).contains(&off) { + changed |= set_expr( + dst, + LocalAddrExpr { + slot: base.slot, + offset: off, + confidence: base.confidence.saturating_sub(1), + }, + &mut addr_exprs, + ); + } + } else if let Some(base) = addr_of(b, &addr_exprs) + && is_scaled_index_like(block.addr, a, &block_ops, &addr_exprs, 0) + { + changed |= set_expr( + dst, + LocalAddrExpr { + slot: base.slot, + offset: base.offset, + confidence: base.confidence.saturating_sub(4), + }, + &mut addr_exprs, + ); + } + } + SSAOp::IntSub { dst, a, b } => { + if let Some(delta) = parse_ssa_const_offset(&b.name, ptr_bits) { + let a_lower = a.name.to_ascii_lowercase(); + if is_stack_base(a_lower.as_str()) { + changed |= set_stack_slot( + dst, + delta.saturating_neg(), + &mut stack_addr_offsets, + ); + } + } + if let Some(base) = addr_of(a, &addr_exprs) + && let Some(delta) = parse_ssa_const_offset(&b.name, ptr_bits) + { + let off = base.offset.saturating_sub(delta); + if (-offset_bound..=offset_bound).contains(&off) { + changed |= set_expr( + dst, + LocalAddrExpr { + slot: base.slot, + offset: off, + confidence: base.confidence.saturating_sub(1), + }, + &mut addr_exprs, + ); + } + } else if let Some(base) = addr_of(a, &addr_exprs) + && is_scaled_index_like(block.addr, b, &block_ops, &addr_exprs, 0) + { + changed |= set_expr( + dst, + LocalAddrExpr { + slot: base.slot, + offset: base.offset, + confidence: base.confidence.saturating_sub(4), + }, + &mut addr_exprs, + ); + } + } + SSAOp::Store { addr, val, .. } => { + if let Some(offset) = stack_slot_of(addr, &stack_addr_offsets) + && let Some(mut expr) = addr_of(val, &addr_exprs) + { + expr.confidence = expr.confidence.saturating_sub(2); + let key = (block.addr, offset); + match stack_slot_values.get(&key).copied() { + Some(prev) if prev.confidence >= expr.confidence => {} + _ => { + stack_slot_values.insert(key, expr); + changed = true; + } + } + } + } + SSAOp::Load { dst, addr, .. } => { + if let Some(offset) = stack_slot_of(addr, &stack_addr_offsets) + && let Some(mut expr) = + stack_slot_values.get(&(block.addr, offset)).copied() + { + expr.confidence = expr.confidence.saturating_sub(3); + changed |= set_expr(dst, expr, &mut addr_exprs); + } + } + _ => {} + } + } + } + if !changed { + break; + } + } + + for block in ssa_blocks { + for op in &block.ops { + let resolve_addr = |addr: &SSAVar| -> Option { + if addr.version == 0 { + let key = addr.name.to_ascii_lowercase(); + if let Some(slot) = pointer_arg_slot_map.get(key.as_str()).copied() { + return Some(LocalAddrExpr { + slot, + offset: 0, + confidence: 92, + }); + } + } + addr_exprs + .get(&ssa_var_block_key(block.addr, addr)) + .copied() + }; + match op { + SSAOp::Load { dst, addr, .. } => { + if let Some(expr) = resolve_addr(addr) + && (0..=offset_bound).contains(&expr.offset) + { + let entry = slot_field_evidence + .entry(expr.slot) + .or_default() + .entry(expr.offset as u64) + .or_default(); + entry.reads = entry.reads.saturating_add(1); + *entry.widths.entry(dst.size).or_insert(0) += 1; + *entry.type_votes.entry(size_to_type(dst.size)).or_insert(0) += 1; + } + } + SSAOp::Store { addr, val, .. } => { + if let Some(expr) = resolve_addr(addr) + && (0..=offset_bound).contains(&expr.offset) + { + let entry = slot_field_evidence + .entry(expr.slot) + .or_default() + .entry(expr.offset as u64) + .or_default(); + entry.writes = entry.writes.saturating_add(1); + *entry.widths.entry(val.size).or_insert(0) += 1; + *entry.type_votes.entry(size_to_type(val.size)).or_insert(0) += 1; + } + } + _ => {} + } + } + } + + let mut struct_decls = Vec::new(); + let mut slot_type_overrides = HashMap::new(); + let mut slot_field_profiles = HashMap::new(); + let mut slots: Vec = slot_field_evidence.keys().copied().collect(); + slots.sort_unstable(); + + for slot in slots { + let Some(fields_map) = slot_field_evidence.get(&slot) else { + continue; + }; + if fields_map.is_empty() { + continue; + } + let mut shape = String::new(); + let mut fields = Vec::new(); + let mut normalized_fields = BTreeMap::new(); + let mut confidence_acc = 0u32; + for (offset, evidence) in fields_map { + let total_votes: u32 = evidence.type_votes.values().copied().sum(); + let Some((field_type, field_votes)) = evidence + .type_votes + .iter() + .max_by_key(|(_, count)| **count) + .map(|(ty, count)| (ty.clone(), *count)) + else { + continue; + }; + if evidence.type_votes.len() > 1 { + diagnostics.conflicts.push(format!( + "slot {slot} field +0x{offset:x} conflicting type votes {:?}", + evidence.type_votes + )); + } + let strength = ((field_votes.saturating_mul(100)) / total_votes.max(1)) as u8; + let rw_bonus = if evidence.reads > 0 && evidence.writes > 0 { + 10 + } else { + 0 + }; + let field_conf = 70u8.saturating_add(strength / 3).saturating_add(rw_bonus); + confidence_acc = confidence_acc.saturating_add(field_conf as u32); + shape.push_str(&format!("{offset:x}:{field_type};")); + normalized_fields.insert(*offset, field_type.clone()); + fields.push(StructFieldCandidate { + name: format!("f_{offset:x}"), + offset: *offset, + field_type, + confidence: field_conf, + }); + } + if fields.is_empty() { + continue; + } + let avg_conf = (confidence_acc / fields.len() as u32).clamp(1, 100) as u8; + let allow_single_field = fields.len() == 1 && avg_conf >= 94; + if fields.len() < 2 && !allow_single_field { + continue; + } + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + shape.hash(&mut hasher); + let struct_name = format!("sla_struct_{:016x}", hasher.finish()); + let Some(decl) = build_struct_decl(&struct_name, &fields, ptr_bits) else { + continue; + }; + struct_decls.push(StructDeclCandidate { + name: struct_name.clone(), + decl, + confidence: avg_conf.max(84), + source: StructDeclSource::LocalInferred, + fields, + }); + slot_field_profiles.insert(slot, normalized_fields); + slot_type_overrides.insert(slot, format!("struct {struct_name} *")); + } + + LocalStructArtifacts { + struct_decls, + slot_type_overrides, + slot_field_profiles, + } +} + +pub fn augment_local_struct_artifacts_with_semantics( + local_structs: &mut LocalStructArtifacts, + artifact: &r2sym::SemanticArtifact, + ptr_bits: u32, +) { + let projection = SemanticTypeProjection::from_inputs( + &InterprocSummaryView::default(), + Some(artifact), + ptr_bits, + ); + augment_local_struct_artifacts_with_projection(local_structs, &projection, ptr_bits); +} + +fn augment_local_struct_artifacts_with_projection( + local_structs: &mut LocalStructArtifacts, + projection: &SemanticTypeProjection, + ptr_bits: u32, +) { + for (slot, projected) in &projection.slot_field_profiles { + let profile = local_structs.slot_field_profiles.entry(*slot).or_default(); + for (offset, field_type) in projected { + profile.entry(*offset).or_insert(field_type.clone()); + } + if profile.is_empty() || local_structs.slot_type_overrides.contains_key(slot) { + continue; + } + let struct_name = format!("sla_struct_symbolic_arg{}", slot + 1); + let fields = profile + .iter() + .map(|(offset, field_type)| StructFieldCandidate { + name: format!("f_{offset:x}"), + offset: *offset, + field_type: field_type.clone(), + confidence: 84, + }) + .collect::>(); + let Some(decl) = build_struct_decl(&struct_name, &fields, ptr_bits) else { + continue; + }; + if !local_structs + .struct_decls + .iter() + .any(|candidate| candidate.name.eq_ignore_ascii_case(&struct_name)) + { + local_structs.struct_decls.push(StructDeclCandidate { + name: struct_name.clone(), + decl, + confidence: 84, + source: StructDeclSource::LocalInferred, + fields, + }); + } + local_structs + .slot_type_overrides + .insert(*slot, format!("struct {struct_name} *")); + } +} + +fn augment_local_struct_artifacts_with_local_field_accesses( + local_structs: &mut LocalStructArtifacts, + local_field_accesses: &[LocalFieldAccessFact], + ptr_bits: u32, +) { + let mut projected_profiles = BTreeMap::>::new(); + for access in local_field_accesses { + let field_type = access + .field_type + .clone() + .unwrap_or_else(|| access.field_name.clone()); + projected_profiles + .entry(access.slot) + .or_default() + .entry(access.field_offset) + .or_insert(field_type); + } + + for (slot, projected) in projected_profiles { + let profile = local_structs.slot_field_profiles.entry(slot).or_default(); + for (offset, field_type) in projected { + profile.entry(offset).or_insert(field_type); + } + if profile.is_empty() || local_structs.slot_type_overrides.contains_key(&slot) { + continue; + } + + let allow_single_field = profile.len() == 1; + if profile.len() < 2 && !allow_single_field { + continue; + } + let mut shape = String::new(); + let fields = profile + .iter() + .map(|(offset, field_type)| { + shape.push_str(&format!("{offset:x}:{field_type};")); + StructFieldCandidate { + name: format!("f_{offset:x}"), + offset: *offset, + field_type: field_type.clone(), + confidence: 90, + } }) - .unwrap_or_default(), - diagnostics: diagnostics.solver_warnings.clone(), - ..FunctionTypeFactInputs::default() - }) - .build(); - let global_type_links = score_global_type_links( - input.ssa_blocks, - &struct_decls, - &var_type_candidates, - input.ptr_bits, - ); + .collect::>(); + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + shape.hash(&mut hasher); + let struct_name = format!("sla_struct_{:016x}", hasher.finish()); + let Some(decl) = build_struct_decl(&struct_name, &fields, ptr_bits) else { + continue; + }; + if !local_structs + .struct_decls + .iter() + .any(|candidate| candidate.name.eq_ignore_ascii_case(&struct_name)) + { + local_structs.struct_decls.push(StructDeclCandidate { + name: struct_name.clone(), + decl, + confidence: 90, + source: StructDeclSource::LocalInferred, + fields, + }); + } + local_structs + .slot_type_overrides + .insert(slot, format!("struct {struct_name} *")); + } +} - let plan = TypeWritebackPlan { - signature: input.inferred_signature.clone(), - var_type_candidates, - var_rename_candidates, - struct_decls: struct_decls.clone(), - global_type_links, - diagnostics: diagnostics.clone(), +fn backward_memory_term_slot_field( + term: &r2sym::BackwardMemoryCondition, + _ptr_bits: u32, +) -> Option<(usize, u64, String)> { + if !term.evidence().is_reliable() { + return None; + } + let slot = match &term.region { + r2sym::BackwardMemoryRegion::Argument { index } => *index, + r2sym::BackwardMemoryRegion::Region(_) => return None, }; - - TypeWritebackAnalysis { - signature: input.inferred_signature, - type_facts, - plan, + if !term.exact_offset && term.offset_lo != term.offset_hi { + return None; + } + if term.offset_lo < 0 || term.offset_hi < 0 || term.offset_lo != term.offset_hi { + return None; } + Some((slot, term.offset_lo as u64, size_to_type(term.size))) } fn canonicalize_param_home_stack_slots( @@ -673,7 +2084,7 @@ fn canonicalize_param_home_stack_slots( ssa_blocks: &[SSABlock], ptr_bits: u32, ) { - if register_params.is_empty() || stack_slots.is_empty() || ssa_blocks.is_empty() { + if register_params.is_empty() || ssa_blocks.is_empty() { return; } @@ -708,20 +2119,34 @@ fn canonicalize_param_home_stack_slots( if rooted_val.version != 0 { continue; } - let Some(slot) = stack_slots.get_mut(&slot_key) else { - continue; - }; - if !matches!( - slot.role, - ExternalStackSlotRole::Unknown | ExternalStackSlotRole::Local - ) { - continue; - } let param_name = merged_signature .and_then(|sig| sig.params.get(param_index)) .map(|param| param.name.clone()) .filter(|name| !name.is_empty()) .unwrap_or_else(|| format!("arg{}", param_index + 1)); + let slot_base = slot_key.base.clone(); + let slot = + stack_slots + .entry(slot_key) + .or_insert_with(|| ExternalStackVarSpec { + name: format!("{param_name}_home"), + ty: merged_signature + .and_then(|sig| sig.params.get(param_index)) + .and_then(|param| param.ty.clone()), + base: slot_base, + role: ExternalStackSlotRole::Unknown, + param_index: None, + param_name: None, + source_reg: None, + }); + if !matches!( + slot.role, + ExternalStackSlotRole::Unknown + | ExternalStackSlotRole::Local + | ExternalStackSlotRole::StackArg + ) { + continue; + } slot.role = ExternalStackSlotRole::ParamHome; slot.param_index = Some(param_index); slot.param_name = Some(param_name.clone()); @@ -2369,119 +3794,7 @@ fn estimate_type_like_size_bytes(ty: &CTypeLike, ptr_bits: u32) -> Option { } fn render_signature_type(ty: &CTypeLike, ptr_bits: u32) -> String { - render_type_like(&materialize_signature_type_like(ty.clone(), ptr_bits)) -} - -fn materialize_signature_type_like(ty: CTypeLike, ptr_bits: u32) -> CTypeLike { - match ty { - CTypeLike::Pointer(inner) => { - if matches!(*inner, CTypeLike::Unknown | CTypeLike::Void) - || matches!( - inner.as_ref(), - CTypeLike::Struct(name) - | CTypeLike::Union(name) - | CTypeLike::Enum(name) - if is_unmaterialized_aggregate_name(name) - ) - { - return CTypeLike::Pointer(Box::new(CTypeLike::Void)); - } - CTypeLike::Pointer(Box::new(materialize_signature_type_like(*inner, ptr_bits))) - } - CTypeLike::Array(inner, len) => { - if matches!(*inner, CTypeLike::Unknown | CTypeLike::Void) { - return CTypeLike::Array( - Box::new(CTypeLike::Int { - bits: 8, - signedness: Signedness::Unsigned, - }), - len, - ); - } - CTypeLike::Array( - Box::new(materialize_signature_type_like(*inner, ptr_bits)), - len, - ) - } - CTypeLike::Unknown => fallback_scalar_type_like(ptr_bits), - CTypeLike::Struct(name) if is_unmaterialized_aggregate_name(&name) => { - fallback_scalar_type_like(ptr_bits) - } - CTypeLike::Union(name) if is_unmaterialized_aggregate_name(&name) => { - fallback_scalar_type_like(ptr_bits) - } - CTypeLike::Enum(name) if is_unmaterialized_aggregate_name(&name) => { - fallback_scalar_type_like(ptr_bits) - } - other => other, - } -} - -fn fallback_scalar_type_like(ptr_bits: u32) -> CTypeLike { - CTypeLike::Int { - bits: if ptr_bits >= 64 { 64 } else { 32 }, - signedness: Signedness::Signed, - } -} - -fn render_type_like(ty: &CTypeLike) -> String { - match ty { - CTypeLike::Void => "void".to_string(), - CTypeLike::Bool => "bool".to_string(), - CTypeLike::Int { - bits: 8, - signedness: Signedness::Signed, - } => "int8_t".to_string(), - CTypeLike::Int { - bits: 16, - signedness: Signedness::Signed, - } => "int16_t".to_string(), - CTypeLike::Int { - bits: 32, - signedness: Signedness::Signed, - } => "int32_t".to_string(), - CTypeLike::Int { - bits: 64, - signedness: Signedness::Signed, - } => "int64_t".to_string(), - CTypeLike::Int { - bits, - signedness: Signedness::Signed | Signedness::Unknown, - } => { - format!("int{bits}_t") - } - CTypeLike::Int { - bits: 8, - signedness: Signedness::Unsigned, - } => "uint8_t".to_string(), - CTypeLike::Int { - bits: 16, - signedness: Signedness::Unsigned, - } => "uint16_t".to_string(), - CTypeLike::Int { - bits: 32, - signedness: Signedness::Unsigned, - } => "uint32_t".to_string(), - CTypeLike::Int { - bits: 64, - signedness: Signedness::Unsigned, - } => "uint64_t".to_string(), - CTypeLike::Int { - bits, - signedness: Signedness::Unsigned, - } => format!("uint{bits}_t"), - CTypeLike::Float(32) => "float".to_string(), - CTypeLike::Float(64) => "double".to_string(), - CTypeLike::Float(bits) => format!("float{bits}"), - CTypeLike::Pointer(inner) => format!("{}*", render_type_like(inner)), - CTypeLike::Array(inner, Some(size)) => format!("{}[{}]", render_type_like(inner), size), - CTypeLike::Array(inner, None) => format!("{}[]", render_type_like(inner)), - CTypeLike::Struct(name) => format!("struct {name}"), - CTypeLike::Union(name) => format!("union {name}"), - CTypeLike::Enum(name) => format!("enum {name}"), - CTypeLike::Function => "void (*)()".to_string(), - CTypeLike::Unknown => "/* unknown */".to_string(), - } + crate::render_signature_type(ty, ptr_bits) } fn format_signature( @@ -2489,12 +3802,7 @@ fn format_signature( ret_type: &str, params: &[InferredSignatureParam], ) -> String { - let args = params - .iter() - .map(|param| format!("{},{}", param.param_type, param.name)) - .collect::>() - .join(","); - format!("{ret_type} {function_name} ({args})") + crate::format_afs_signature(function_name, ret_type, params) } fn build_struct_decl( @@ -2535,11 +3843,6 @@ fn is_generated_local_struct_name(name: &str) -> bool { .starts_with("sla_struct_") } -fn is_unmaterialized_aggregate_name(name: &str) -> bool { - let lower = name.trim().to_ascii_lowercase(); - lower.is_empty() || lower == "anon" || lower.starts_with("anon_") -} - fn is_generic_type_string(ty: &str) -> bool { let normalized = normalize_external_type_name(ty); let lower = normalized.trim().to_ascii_lowercase(); @@ -2663,6 +3966,114 @@ fn ssa_var_block_key(block_addr: u64, var: &SSAVar) -> String { #[cfg(test)] mod tests { use super::*; + use std::collections::{BTreeMap, BTreeSet}; + + fn test_native_summary(slice_class: r2sym::SliceClass) -> r2sym::NativeFunctionSummary { + r2sym::NativeFunctionSummary { + slice_class, + closure_functions: 1, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: Default::default(), + } + } + + fn test_artifact( + stage: r2sym::RefinementStage, + slice_class: r2sym::SliceClass, + skipped_large_cfg: bool, + residual_reasons: Vec, + regions: Vec, + ) -> r2sym::SemanticArtifact { + let regions = regions + .into_iter() + .map(|region| (region.key(), region)) + .collect::>(); + r2sym::SemanticArtifact { + stage, + granularity: r2sym::ArtifactGranularity::Regioned, + execution: r2sym::ExecutionModel::Native, + body: r2sym::SemanticArtifactBody::Native(r2sym::NativeArtifactBody { + summary: test_native_summary(slice_class), + regions, + }), + diagnostics: r2sym::SemanticArtifactDiagnostics { + branches_evaluated: 0, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg, + residual_reasons, + ambiguous_targets: Vec::new(), + cache_hit: false, + }, + } + } + + fn test_arg_memory_term(offset: i64, size: u32) -> r2sym::BackwardMemoryCondition { + r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: offset, + offset_hi: offset, + size, + exact_offset: true, + evidence: r2sym::SemanticEvidence::exact(), + binding: None, + expr: format!("*(arg0 + {offset})"), + value_expr: None, + exact_value: false, + } + } + + fn test_exact_compiled_condition( + simplified: &str, + memory_terms: Vec, + ) -> r2sym::BackwardConditionSummary { + r2sym::BackwardConditionSummary { + simplified: simplified.to_string(), + terms: vec![simplified.to_string()], + memory_terms, + backward_memory_substitutions: 1, + backward_memory_candidate_enumerations: 1, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + } + } + + fn test_region_with_control( + anchor: u64, + target: u64, + condition: &str, + compiled: r2sym::BackwardConditionSummary, + ) -> r2sym::SemanticRegion { + r2sym::SemanticRegion { + anchor, + frontier: BTreeSet::from([target]), + control: vec![r2sym::Judged::new( + r2sym::ControlFact { + target, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some(condition.to_string()), + compiled: Some(compiled), + }, + r2sym::SemanticEvidence::exact(), + )], + memory: Vec::new(), + pre: Vec::new(), + post: Vec::new(), + targets: vec![r2sym::Judged::new( + r2sym::TargetFact { + target, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + }, + r2sym::SemanticEvidence::exact(), + )], + } + } #[test] fn main_signature_canonicalization_updates_signature_output() { @@ -2703,6 +4114,138 @@ mod tests { ); } + #[test] + fn uncorroborated_type_hint_assumptions_are_ignored() { + let mut parsed_context = ParsedExternalContext { + assumptions: r2ssa::AssumptionSet::new(vec![r2ssa::AnalysisAssumption { + id: Some("param0-char-ptr".to_string()), + subject: r2ssa::AssumptionSubject::Parameter { index: 0 }, + value: r2ssa::AssumptionValue::TypeHint { + ty: "char *".to_string(), + }, + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::User, + }]), + ..ParsedExternalContext::default() + }; + let mut inferred_signature = InferredSignature { + function_name: "sym.demo".to_string(), + signature: "void sym.demo(void *)".to_string(), + ret_type: "void".to_string(), + params: vec![InferredSignatureParam { + name: "arg1".to_string(), + param_type: "void *".to_string(), + }], + callconv: "amd64".to_string(), + arch: "x86-64".to_string(), + confidence: 80, + callconv_confidence: 80, + }; + + let usage = apply_type_hint_assumptions_to_context( + &mut parsed_context, + &mut inferred_signature, + 64, + Some(&SemanticTypeProjection::default()), + ); + + assert!(usage.applied.is_empty()); + assert_eq!(usage.ignored.len(), 1); + assert!(usage.conflicts.is_empty()); + assert_eq!(inferred_signature.params[0].param_type, "void *"); + assert!(parsed_context.merged_signature.is_none()); + } + + #[test] + fn corroborated_type_hint_assumptions_update_signature_and_usage() { + let mut parsed_context = ParsedExternalContext { + assumptions: r2ssa::AssumptionSet::new(vec![r2ssa::AnalysisAssumption { + id: Some("param0-char-ptr".to_string()), + subject: r2ssa::AssumptionSubject::Parameter { index: 0 }, + value: r2ssa::AssumptionValue::TypeHint { + ty: "char *".to_string(), + }, + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::User, + }]), + ..ParsedExternalContext::default() + }; + let mut inferred_signature = InferredSignature { + function_name: "sym.demo".to_string(), + signature: "void sym.demo(void *)".to_string(), + ret_type: "void".to_string(), + params: vec![InferredSignatureParam { + name: "arg1".to_string(), + param_type: "void *".to_string(), + }], + callconv: "amd64".to_string(), + arch: "x86-64".to_string(), + confidence: 80, + callconv_confidence: 80, + }; + let root = r2ssa::InterprocFunctionId(0x401000); + let summary_set = InterprocSummarySet { + root: Some(root), + summaries: BTreeMap::from([( + root, + FunctionSemanticSummary { + id: root, + name: Some("sym.demo".to_string()), + arg_count_hint: Some(1), + direct_callees: BTreeSet::new(), + callsite_count: 0, + has_unknown_calls: false, + arg_effects: BTreeMap::from([( + 0, + SummaryArgEffect { + read: true, + write: true, + escape: false, + free: false, + }, + )]), + memory_effects: Vec::new(), + return_relation: SummaryReturnRelation::Void, + reads_global_memory: false, + writes_global_memory: false, + touches_unknown_memory: false, + }, + )]), + diagnostics: Default::default(), + }; + let projection = SemanticTypeProjection::from_inputs( + &InterprocSummaryView::new(Some(summary_set)), + None, + 64, + ); + + let usage = apply_type_hint_assumptions_to_context( + &mut parsed_context, + &mut inferred_signature, + 64, + Some(&projection), + ); + + assert_eq!(usage.applied.len(), 1); + assert!(usage.ignored.is_empty()); + assert!(usage.conflicts.is_empty()); + assert_eq!(inferred_signature.params[0].param_type, "int8_t*"); + assert_eq!( + render_signature_type( + parsed_context + .merged_signature + .as_ref() + .expect("merged signature") + .params[0] + .ty + .as_ref() + .expect("hinted param type"), + 64 + ), + "int8_t*" + ); + } + #[test] fn interproc_heap_alloc_summary_upgrades_pointer_sized_scalar_return() { let root = r2ssa::InterprocFunctionId(0x401000); @@ -4382,4 +5925,182 @@ mod tests { Some(&CTypeLike::Pointer(Box::new(CTypeLike::Void))) ); } + + #[test] + fn symbolic_actionable_memory_terms_seed_local_struct_profiles() { + let compiled = + test_exact_compiled_condition("arg0->f_8 == 0", vec![test_arg_memory_term(8, 4)]); + let artifact = test_artifact( + r2sym::RefinementStage::Compiled, + r2sym::SliceClass::Worker, + false, + Vec::new(), + vec![test_region_with_control( + 0x401000, + 0x401020, + "arg0->f_8 == 0", + compiled, + )], + ); + let mut local_structs = LocalStructArtifacts::default(); + + augment_local_struct_artifacts_with_semantics(&mut local_structs, &artifact, 64); + + assert_eq!( + local_structs + .slot_field_profiles + .get(&0) + .and_then(|profile| profile.get(&8)) + .map(String::as_str), + Some("int32_t") + ); + assert_eq!( + local_structs + .slot_type_overrides + .get(&0) + .map(String::as_str), + Some("struct sla_struct_symbolic_arg1 *") + ); + assert!( + local_structs + .struct_decls + .iter() + .any(|decl| decl.name == "sla_struct_symbolic_arg1") + ); + } + + #[test] + fn symbolic_memory_without_control_or_post_support_is_rejected() { + let artifact = test_artifact( + r2sym::RefinementStage::Compiled, + r2sym::SliceClass::Worker, + false, + Vec::new(), + vec![r2sym::SemanticRegion { + anchor: 0x401000, + frontier: BTreeSet::new(), + control: Vec::new(), + memory: vec![r2sym::Judged::new( + r2sym::MemoryFact { + term: test_arg_memory_term(8, 4), + }, + r2sym::SemanticEvidence::exact(), + )], + pre: Vec::new(), + post: Vec::new(), + targets: Vec::new(), + }], + ); + let mut local_structs = LocalStructArtifacts::default(); + + augment_local_struct_artifacts_with_semantics(&mut local_structs, &artifact, 64); + + assert!( + local_structs.slot_field_profiles.is_empty(), + "memory-only regions without control/post support must not project struct fields" + ); + } + + #[test] + fn symbolic_postconditions_seed_local_struct_profiles_without_control_islands() { + let artifact = test_artifact( + r2sym::RefinementStage::Compiled, + r2sym::SliceClass::Worker, + false, + Vec::new(), + vec![r2sym::SemanticRegion { + anchor: 0x401000, + frontier: BTreeSet::new(), + control: Vec::new(), + memory: Vec::new(), + pre: Vec::new(), + post: vec![r2sym::Judged::new( + r2sym::SemanticPredicate { + expr: "post(arg0->f_8)".to_string(), + compiled: Some(test_exact_compiled_condition( + "post(arg0->f_8)", + vec![test_arg_memory_term(8, 4)], + )), + }, + r2sym::SemanticEvidence::exact(), + )], + targets: Vec::new(), + }], + ); + let mut local_structs = LocalStructArtifacts::default(); + + augment_local_struct_artifacts_with_semantics(&mut local_structs, &artifact, 64); + + assert_eq!( + local_structs + .slot_field_profiles + .get(&0) + .and_then(|profile| profile.get(&8)) + .map(String::as_str), + Some("int32_t") + ); + } + + #[test] + fn semantic_type_fallback_plan_uses_typed_symbolic_summary() { + let artifact = test_artifact( + r2sym::RefinementStage::Residual, + r2sym::SliceClass::Worker, + true, + vec![r2sym::ResidualReason::LargeCfg], + vec![r2sym::SemanticRegion { + anchor: 0x401000, + frontier: BTreeSet::from([0x401010]), + control: vec![r2sym::Judged::new( + r2sym::ControlFact { + target: 0x401010, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("x == 0".to_string()), + compiled: Some(test_exact_compiled_condition("x == 0", Vec::new())), + }, + r2sym::SemanticEvidence::exact(), + )], + memory: vec![r2sym::Judged::new( + r2sym::MemoryFact { + term: test_arg_memory_term(8, 4), + }, + r2sym::SemanticEvidence::exact(), + )], + pre: Vec::new(), + post: Vec::new(), + targets: vec![r2sym::Judged::new( + r2sym::TargetFact { + target: 0x401010, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + }, + r2sym::SemanticEvidence::exact(), + )], + }], + ); + + let plan = build_semantic_type_fallback_plan("fcn.401000", "x86-64", 64, &artifact); + + assert_eq!(plan.signature.params.len(), 1); + assert!(plan.signature.params[0].param_type.contains("struct ")); + assert_eq!(plan.struct_decls.len(), 1); + assert!(plan + .diagnostics + .warnings + .iter() + .any(|warning| warning.contains("semantic fallback: worker slice in residual mode"))); + assert!( + plan.diagnostics + .warnings + .iter() + .any(|warning| warning.contains("regions=1")) + ); + assert!( + plan.diagnostics + .warnings + .iter() + .any(|warning| warning.contains("projected 1 struct candidate")) + ); + } } diff --git a/r2plugin/Cargo.toml b/r2plugin/Cargo.toml index 8a1a740..656ce4e 100644 --- a/r2plugin/Cargo.toml +++ b/r2plugin/Cargo.toml @@ -28,5 +28,6 @@ optional = true default = ["x86"] x86 = ["sleigh-config/x86"] arm = ["sleigh-config/ARM", "sleigh-config/AARCH64"] +mips = ["sleigh-config/MIPS"] riscv = ["sleigh-config/RISCV"] -all-archs = ["x86", "arm", "riscv"] +all-archs = ["x86", "arm", "mips", "riscv"] diff --git a/r2plugin/Makefile b/r2plugin/Makefile index 26b1d7d..6cd74bd 100644 --- a/r2plugin/Makefile +++ b/r2plugin/Makefile @@ -179,7 +179,7 @@ help: @echo "" @echo "Variables:" @echo " RUST_TARGET - 'release' (default) or 'debug'" - @echo " RUST_FEATURES - Sleigh features: 'x86' (default), 'arm' (ARM32 + AArch64), 'riscv', 'all-archs'" + @echo " RUST_FEATURES - Sleigh features: 'x86' (default), 'arm' (ARM32 + AArch64), 'mips', 'riscv', 'all-archs'" @echo "" @echo "Examples:" @echo " make # Build release with x86" diff --git a/r2plugin/r_anal_sleigh.c b/r2plugin/r_anal_sleigh.c index e5cc9ba..c6c145b 100644 --- a/r2plugin/r_anal_sleigh.c +++ b/r2plugin/r_anal_sleigh.c @@ -83,6 +83,62 @@ extern char *r2sym_explore_to(const R2ILContext *ctx, const R2ILBlock **blocks, unsigned long long entry_addr, unsigned long long target_addr); extern char *r2sym_solve_to(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, unsigned long long entry_addr, unsigned long long target_addr); +extern char *r2sym_run_spec_json(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, + unsigned long long entry_addr, const char *spec_json); +typedef struct { + unsigned long long entry_addr; + const char *name; + const R2ILBlock **blocks; + size_t num_blocks; +} R2ILFunctionBlocks; +typedef struct { + const char *name; + unsigned long long value; +} R2SymReplayRegister; +typedef struct { + unsigned long long addr; + const unsigned char *bytes; + size_t size; + const char *label; +} R2SymReplayMemoryWindow; +typedef struct { + const char *name; + const char *symbol; +} R2SymReplayRegisterOverlay; +typedef struct { + unsigned long long addr; + unsigned int size; + const char *name; +} R2SymReplayMemoryOverlay; +typedef struct { + unsigned long long checkpoint_id; + unsigned long long entry_addr; + const R2SymReplayRegister *registers; + size_t num_registers; + const R2SymReplayMemoryWindow *memory; + size_t num_memory; + const R2SymReplayRegisterOverlay *register_overlays; + size_t num_register_overlays; + const R2SymReplayMemoryOverlay *memory_overlays; + size_t num_memory_overlays; + const int *tty_fds; + size_t num_tty_fds; + int skip_sleep_calls; +} R2SymReplaySeed; +extern char *r2sym_function_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, unsigned long long entry_addr, const char *external_context_json); +extern char *r2sym_paths_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, unsigned long long entry_addr, const char *external_context_json); +extern char *r2sym_explore_to_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, + unsigned long long entry_addr, unsigned long long target_addr, const char *external_context_json); +extern char *r2sym_solve_to_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, + unsigned long long entry_addr, unsigned long long target_addr, const char *external_context_json); +extern char *r2sym_explore_to_replay_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, + unsigned long long entry_addr, unsigned long long target_addr, const R2SymReplaySeed *replay_seed, const char *external_context_json); +extern char *r2sym_solve_to_replay_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, + unsigned long long entry_addr, unsigned long long target_addr, const R2SymReplaySeed *replay_seed, const char *external_context_json); +extern char *r2sym_compile_semantics_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, + unsigned long long entry_addr, const char *external_context_json); +extern char *r2sym_run_spec_json_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, + unsigned long long entry_addr, const char *spec_json, const char *external_context_json); extern int r2sym_set_symbol_map_json(const char *json); extern int r2sym_merge_is_enabled(void); extern void r2sym_merge_set_enabled(int enabled); @@ -92,6 +148,16 @@ extern char *r2dec_function_with_context(const R2ILContext *ctx, const R2ILBlock const char *func_name, const char *func_names_json, const char *strings_json, const char *symbols_json, const char *external_context_json); +extern char *r2dec_function_with_context_scope(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, + unsigned long long fcn_addr, const char *func_name, const char *func_names_json, + const char *strings_json, const char *symbols_json, const char *external_context_json, + const R2ILFunctionBlocks *functions, size_t num_functions); +extern char *r2dec_semantic_worker_linearization_scope_ffi(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, + unsigned long long fcn_addr, const char *func_name, size_t block_count, size_t loop_count, + size_t back_edge_count, size_t max_switch_cases, const R2ILFunctionBlocks *functions, size_t num_functions); +extern char *r2dec_block_guard_comment_ffi(const char *func_name, size_t blocks, size_t max_blocks); +extern char *r2dec_cfg_guard_comment_ffi(const char *func_name, size_t block_count, + size_t loop_count, size_t back_edge_count, size_t max_switch_cases); /* CFG */ extern char *r2cfg_function_ascii(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks); @@ -110,8 +176,30 @@ extern char *r2sleigh_infer_type_writeback_json(const R2ILContext *ctx, const R2 extern char *r2sleigh_infer_type_writeback_json_ex(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, unsigned long long fcn_addr, const char *fcn_name, const char *external_context_json, size_t interproc_iter, size_t interproc_max_iters, int interproc_converged, const char *interproc_scope_json); +extern unsigned long long r2sleigh_function_analysis_artifact_cache_key(const R2ILContext *ctx, + const R2ILBlock **blocks, size_t num_blocks, unsigned long long fcn_addr, const char *fcn_name, + const char *external_context_json, size_t interproc_max_iters, const char *interproc_scope_json); +extern char *r2sleigh_function_interproc_summary_json_ex(const R2ILContext *ctx, + const R2ILBlock **blocks, size_t num_blocks, unsigned long long fcn_addr, const char *fcn_name, + const char *external_context_json, size_t interproc_max_iters, const char *interproc_scope_json); +extern char *r2sleigh_infer_type_writeback_json_scope_ex(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, + unsigned long long fcn_addr, const char *fcn_name, const char *external_context_json, + size_t interproc_iter, size_t interproc_max_iters, int interproc_converged, const char *interproc_scope_json, + const R2ILFunctionBlocks *functions, size_t num_functions); +extern char *r2sleigh_function_facts_json_scope_ex(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, + unsigned long long fcn_addr, const char *fcn_name, const char *external_context_json, + size_t interproc_iter, size_t interproc_max_iters, int interproc_converged, const char *interproc_scope_json, + const R2ILFunctionBlocks *functions, size_t num_functions); +extern char *r2sleigh_function_plan_json_scope_ex(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, + unsigned long long fcn_addr, const char *fcn_name, const char *external_context_json, + size_t interproc_iter, size_t interproc_max_iters, int interproc_converged, const char *interproc_scope_json, + const R2ILFunctionBlocks *functions, size_t num_functions); extern char *r2sleigh_get_direct_call_targets_json(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, unsigned long long fcn_addr, const char *fcn_name); +extern char *r2sleigh_get_symbolic_scope_targets_json(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, + unsigned long long fcn_addr, const char *fcn_name, const char *registration_call_targets_json); +extern char *r2sleigh_get_runtime_materialized_sources_json(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, + unsigned long long fcn_addr, const char *fcn_name, const char *copy_call_targets_json); extern int r2sleigh_alias_function_analysis_artifact_cache(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, unsigned long long fcn_addr, const char *fcn_name, const char *source_external_context_json, const char *target_external_context_json); @@ -183,6 +271,7 @@ typedef struct { ut64 dep_hash; ut64 applied_hash; char *payload_json; + char *interproc_scope_json; } TypeWritebackCacheEntry; typedef struct { @@ -241,12 +330,37 @@ typedef struct { size_t capacity; } BlockArray; +#define SLEIGH_SYM_HELPER_MAX_FUNCTIONS 32 +#define SLEIGH_SCOPE_HELPER_MAX_BLOCKS 64 +#define SLEIGH_SCOPE_HELPER_MAX_COST 256 +#define SLEIGH_RUNTIME_MATERIALIZED_MAX_BYTES 0x4000 +#define SLEIGH_RUNTIME_MATERIALIZED_SLOT_BYTES 16 +#define SLEIGH_AUTO_CALLBACK_MAX_BLOCKS 96 +#define SLEIGH_AUTO_CALLBACK_MAX_COST 512 +#define SLEIGH_AUTO_CALLBACK_MAX_LINEAR_SIZE (256ULL * 1024ULL) +#define SLEIGH_POST_ANALYSIS_FAST_BUDGET_USEC (2ULL * 1000000ULL) +#define SLEIGH_POST_ANALYSIS_BALANCED_BUDGET_USEC (10ULL * 1000000ULL) + +typedef struct { + R2ILFunctionBlocks *functions; + BlockArray *owned_blocks; + char **owned_names; + size_t count; + size_t capacity; +} SymFunctionScope; + +typedef struct { + ut64 addr; + ut64 size; +} RuntimeMaterializedSource; + static char *build_type_interproc_scope_json( RCore *core, RAnal *anal, R2ILContext *ctx, RAnalFunction *fcn, - const BlockArray *blocks + const BlockArray *blocks, + int max_iters ); static bool warm_type_payload_cache_for_function( @@ -268,6 +382,62 @@ static ut64 *collect_type_interproc_direct_targets_from_blocks( const char *fcn_name, size_t *out_count ); +static ut64 *collect_runtime_scope_targets_from_blocks( + R2ILContext *ctx, + const BlockArray *blocks, + ut64 fcn_addr, + const char *fcn_name, + const ut64 *registration_targets, + size_t registration_target_count, + size_t *out_count +); +static RuntimeMaterializedSource *collect_runtime_materialized_sources_from_blocks( + R2ILContext *ctx, + const BlockArray *blocks, + ut64 fcn_addr, + const char *fcn_name, + const ut64 *copy_targets, + size_t copy_target_count, + size_t *out_count +); +static void sym_function_scope_init(SymFunctionScope *scope); +static void sym_function_scope_free(SymFunctionScope *scope); +static bool sym_function_scope_ensure_capacity(SymFunctionScope *scope, size_t needed); +static bool sym_function_scope_append( + SymFunctionScope *scope, + RAnal *anal, + RAnalFunction *fcn, + R2ILContext *ctx, + bool include_linear_gap_blocks +); +static bool sym_function_scope_append_runtime_source( + SymFunctionScope *scope, + RAnal *anal, + R2ILContext *ctx, + ut64 addr, + ut64 size +); +static bool build_symbolic_function_scope( + RAnal *anal, + RAnalFunction *root_fcn, + R2ILContext *ctx, + SymFunctionScope *scope +); +static bool build_symbolic_function_scope_with_target( + RAnal *anal, + RAnalFunction *root_fcn, + R2ILContext *ctx, + SymFunctionScope *scope, + ut64 target_hint +); +static char *resolve_interproc_seed_name(RCore *core, RAnal *anal, ut64 addr); +static ut64 *collect_type_interproc_direct_targets_from_blocks( + R2ILContext *ctx, + const BlockArray *blocks, + ut64 fcn_addr, + const char *fcn_name, + size_t *out_count +); static void block_array_init(BlockArray *arr) { arr->blocks = NULL; @@ -275,6 +445,38 @@ static void block_array_init(BlockArray *arr) { arr->capacity = 0; } +static bool sleigh_debug_scope_enabled(void) { + return r_sys_getenv_asbool ("R2SLEIGH_DEBUG_SCOPE"); +} + +static void sleigh_debug_scope_log(const char *fmt, ...) { + char *path = NULL; + FILE *fd = NULL; + va_list ap; + + if (!sleigh_debug_scope_enabled ()) { + return; + } + path = r_sys_getenv ("R2SLEIGH_DEBUG_SCOPE_LOG"); + if (!path || !*path) { + free (path); + path = strdup ("/tmp/r2sleigh_scope.log"); + } + if (!path) { + return; + } + fd = fopen (path, "a"); + free (path); + if (!fd) { + return; + } + va_start (ap, fmt); + vfprintf (fd, fmt, ap); + va_end (ap); + fputc ('\n', fd); + fclose (fd); +} + static void block_array_push(BlockArray *arr, R2ILBlock *block) { if (arr->count >= arr->capacity) { arr->capacity = arr->capacity ? arr->capacity * 2 : 8; @@ -283,6 +485,56 @@ static void block_array_push(BlockArray *arr, R2ILBlock *block) { arr->blocks[arr->count++] = block; } +static int block_array_compare_addr(const void *a, const void *b) { + const R2ILBlock *const *block_a = (const R2ILBlock *const *)a; + const R2ILBlock *const *block_b = (const R2ILBlock *const *)b; + const ut64 addr_a = (block_a && *block_a)? r2il_block_addr (*block_a): 0; + const ut64 addr_b = (block_b && *block_b)? r2il_block_addr (*block_b): 0; + if (addr_a < addr_b) { + return -1; + } + if (addr_a > addr_b) { + return 1; + } + return 0; +} + +static void block_array_sort(BlockArray *arr) { + if (!arr || arr->count < 2 || !arr->blocks) { + return; + } + qsort (arr->blocks, arr->count, sizeof (R2ILBlock *), block_array_compare_addr); +} + +static bool block_array_addr_covered(const BlockArray *arr, ut64 addr, ut64 *covered_end) { + size_t i; + if (!arr) { + return false; + } + for (i = 0; i < arr->count; i++) { + const R2ILBlock *block = arr->blocks[i]; + ut64 block_addr; + ut64 block_size; + ut64 end; + if (!block) { + continue; + } + block_addr = r2il_block_addr (block); + block_size = r2il_block_size (block); + if (!block_size) { + continue; + } + end = block_addr + block_size; + if (addr >= block_addr && addr < end) { + if (covered_end) { + *covered_end = end; + } + return true; + } + } + return false; +} + static void block_array_free(BlockArray *arr) { size_t i; for (i = 0; i < arr->count; i++) { @@ -449,6 +701,8 @@ static char *sleigh_collect_external_context_json(RAnal *anal, RAnalFunction *fc } RAnalFcnContext *ctx = sleigh_function_context_api.collect (anal, fcn); RList *base_types = r_anal_types_baselist (anal); + char *assumptions_json = NULL; + RJson *assumptions = NULL; PJ *pj = pj_new (); if (!pj) { @@ -548,6 +802,17 @@ static char *sleigh_collect_external_context_json(RAnal *anal, RAnalFunction *fc } pj_end (pj); + if (ctx && ctx->assumptions_json) { + assumptions_json = strdup (ctx->assumptions_json); + if (assumptions_json) { + assumptions = r_json_parse (assumptions_json); + if (assumptions && assumptions->type == R_JSON_ARRAY) { + pj_k (pj, "assumptions"); + pj_raw (pj, ctx->assumptions_json); + } + } + } + pj_k (pj, "base_types"); pj_a (pj); RAnalBaseType *type; @@ -560,11 +825,84 @@ static char *sleigh_collect_external_context_json(RAnal *anal, RAnalFunction *fc pj_end (pj); char *json = pj_drain (pj); + r_json_free (assumptions); + free (assumptions_json); r_list_free (base_types); sleigh_function_context_api.free (ctx); return json? json: strdup ("{}"); } +static char *sleigh_collect_sym_external_context_json(RAnal *anal, RAnalFunction *fcn) { + char *parse_json = NULL; + if (!anal || !fcn) { + return strdup ("{}"); + } + char *assumptions_json = r_anal_function_get_assumptions_json (anal, fcn); + if (R_STR_ISEMPTY (assumptions_json)) { + free (assumptions_json); + return strdup ("{}"); + } + + parse_json = strdup (assumptions_json); + if (!parse_json) { + free (assumptions_json); + return strdup ("{}"); + } + RJson *assumptions = r_json_parse (parse_json); + if (!assumptions || assumptions->type != R_JSON_ARRAY) { + r_json_free (assumptions); + free (parse_json); + free (assumptions_json); + return strdup ("{}"); + } + r_json_free (assumptions); + free (parse_json); + + PJ *pj = pj_new (); + if (!pj) { + free (assumptions_json); + return strdup ("{}"); + } + pj_o (pj); + pj_k (pj, "assumptions"); + pj_raw (pj, assumptions_json); + pj_end (pj); + free (assumptions_json); + char *json = pj_drain (pj); + return json? json: strdup ("{}"); +} + +static char *sleigh_collect_function_assumptions_json(RAnal *anal, RAnalFunction *fcn) { + char *assumptions_json; + char *parse_json = NULL; + RJson *assumptions = NULL; + + if (!anal || !fcn) { + return strdup ("[]"); + } + assumptions_json = r_anal_function_get_assumptions_json (anal, fcn); + if (R_STR_ISEMPTY (assumptions_json)) { + free (assumptions_json); + return strdup ("[]"); + } + + parse_json = strdup (assumptions_json); + if (!parse_json) { + free (assumptions_json); + return strdup ("[]"); + } + assumptions = r_json_parse (parse_json); + if (!assumptions || assumptions->type != R_JSON_ARRAY) { + r_json_free (assumptions); + free (parse_json); + free (assumptions_json); + return strdup ("[]"); + } + r_json_free (assumptions); + free (parse_json); + return assumptions_json; +} + static bool sleigh_resolve_function_context_api(void) { if (sleigh_function_context_api.resolved) { return sleigh_function_context_api.available; @@ -604,6 +942,7 @@ static void type_writeback_cache_clear(void) { size_t i; for (i = 0; i < type_writeback_cache_count; i++) { free (type_writeback_cache[i].payload_json); + free (type_writeback_cache[i].interproc_scope_json); } free (type_writeback_cache); type_writeback_cache = NULL; @@ -763,18 +1102,22 @@ static TypeWritebackCacheEntry *type_writeback_cache_get(ut64 addr) { static bool is_caller_propagation_ref_type(RAnalRefType type); -static bool type_writeback_cache_put(ut64 addr, ut64 key, ut64 dep_hash, ut64 payload_hash, ut64 applied_hash, const char *payload_json) { +static bool type_writeback_cache_put(ut64 addr, ut64 key, ut64 dep_hash, ut64 payload_hash, ut64 applied_hash, + const char *payload_json, const char *interproc_scope_json) { TypeWritebackCacheEntry *entry = type_writeback_cache_get (addr); TypeWritebackCacheEntry *next; char *payload_dup = payload_json? strdup (payload_json): NULL; + char *scope_dup = interproc_scope_json? strdup (interproc_scope_json): NULL; if (entry) { free (entry->payload_json); + free (entry->interproc_scope_json); entry->key = key; entry->payload_hash = payload_hash; entry->dep_hash = dep_hash; entry->applied_hash = applied_hash; entry->payload_json = payload_dup; + entry->interproc_scope_json = scope_dup; return true; } @@ -783,6 +1126,7 @@ static bool type_writeback_cache_put(ut64 addr, ut64 key, ut64 dep_hash, ut64 pa next = realloc (type_writeback_cache, new_capacity * sizeof (TypeWritebackCacheEntry)); if (!next) { free (payload_dup); + free (scope_dup); return false; } type_writeback_cache = next; @@ -795,6 +1139,7 @@ static bool type_writeback_cache_put(ut64 addr, ut64 key, ut64 dep_hash, ut64 pa type_writeback_cache[type_writeback_cache_count].dep_hash = dep_hash; type_writeback_cache[type_writeback_cache_count].applied_hash = applied_hash; type_writeback_cache[type_writeback_cache_count].payload_json = payload_dup; + type_writeback_cache[type_writeback_cache_count].interproc_scope_json = scope_dup; if (!type_writeback_cache_index) { type_writeback_cache_index = ht_up_new0 (); } @@ -951,164 +1296,2377 @@ static bool parse_sym_target_expr(RCore *core, const char *expr, ut64 *target) { return true; } -static RAnalFunction *resolve_function_target_by_name(RAnal *anal, const char *target_name) { - if (!anal || !target_name || !*target_name) { - return NULL; +static bool parse_replay_target_and_json(RCore *core, const char *arg, ut64 *target, char **out_json) { + char *owned = NULL; + char *json = NULL; + char *sep; + const char *json_start; + if (!core || !arg || !*arg || !target || !out_json) { + return false; } - - RAnalFunction *fcn = r_anal_get_function_byname (anal, target_name); - if (fcn) { - return fcn; + *out_json = NULL; + owned = strdup (arg); + if (!owned) { + return false; } - - char *trimmed = r_str_trim_dup (target_name); - if (!trimmed || !*trimmed) { - free (trimmed); - return NULL; + sep = owned; + while (*sep && !isspace ((unsigned char)*sep)) { + sep++; } - - char *base = trimmed; - for (;;) { - if (r_str_startswith (base, "dbg.")) { - base += 4; - continue; - } - if (r_str_startswith (base, "sym.")) { - base += 4; - continue; - } - if (r_str_startswith (base, "fcn.")) { - base += 4; - continue; - } - break; + if (!*sep) { + free (owned); + return false; } - - const char *plain = (*base == '_')? base + 1: base; - char *candidates[] = { - strdup (base), - *plain? strdup (plain): NULL, - r_str_newf ("sym.%s", base), - *plain? r_str_newf ("sym.%s", plain): NULL, - *plain? r_str_newf ("sym._%s", plain): NULL, - r_str_newf ("dbg.%s", base), - *plain? r_str_newf ("dbg.%s", plain): NULL, - r_str_newf ("fcn.%s", base), - *plain? r_str_newf ("fcn.%s", plain): NULL, - (*base == '_')? strdup (plain): r_str_newf ("_%s", plain), - }; - size_t i; - for (i = 0; i < R_ARRAY_SIZE (candidates); i++) { - const char *candidate = candidates[i]; - if (!candidate || !*candidate) { - continue; - } - fcn = r_anal_get_function_byname (anal, candidate); - if (fcn) { - break; - } + *sep++ = '\0'; + json_start = skip_cmd_spaces (sep); + if (!*json_start || !parse_sym_target_expr (core, owned, target)) { + free (owned); + return false; } - for (i = 0; i < R_ARRAY_SIZE (candidates); i++) { - free (candidates[i]); + json = strdup (json_start); + free (owned); + if (!json) { + return false; } - free (trimmed); - return fcn; + r_str_unescape (json); + *out_json = json; + return true; } -static RAnalFunction *resolve_function_target(RCore *core, RAnal *anal, const char *target_arg) { - ut64 target_addr = 0; - RAnalFunction *fcn; +static bool parse_target_and_optional_json(RCore *core, const char *arg, ut64 *target, char **out_json) { + char *owned = NULL; + char *json = NULL; + char *sep; + const char *json_start = NULL; - if (!core || !anal || !target_arg || !*target_arg) { - return NULL; + if (!core || !arg || !*arg || !target || !out_json) { + return false; } - - fcn = resolve_function_target_by_name (anal, target_arg); - if (fcn) { - return fcn; + *out_json = NULL; + owned = strdup (arg); + if (!owned) { + return false; } - - if (!parse_sym_target_expr (core, target_arg, &target_addr)) { - return NULL; + sep = owned; + while (*sep && !isspace ((unsigned char)*sep)) { + sep++; } - return r_anal_get_fcn_in (anal, target_addr, R_ANAL_FCN_TYPE_ANY); -} - -static char *build_sym_symbol_map_json(RCore *core) { - if (!core) { - return strdup ("{}"); + if (*sep) { + *sep++ = '\0'; + json_start = skip_cmd_spaces (sep); } - - PJ *pj = pj_new (); - if (!pj) { - return strdup ("{}"); + if (!parse_sym_target_expr (core, owned, target)) { + free (owned); + return false; } - pj_o (pj); - - /* aflj: [{addr:0x...,name:"..."}] */ - char *aflj = r_core_cmd_str (core, "aflj"); - if (aflj && aflj[0] == '[') { - RJson *root = r_json_parse (aflj); - if (root && root->type == R_JSON_ARRAY) { - RJson *elem; - for (elem = root->children.first; elem; elem = elem->next) { - if (elem->type != R_JSON_OBJECT) { - continue; - } - const RJson *addr = r_json_get (elem, "addr"); - const RJson *name = r_json_get (elem, "name"); - if (addr && name && addr->type == R_JSON_INTEGER && name->type == R_JSON_STRING && name->str_value) { - char key[32]; - snprintf (key, sizeof (key), "0x%llx", (unsigned long long)addr->num.u_value); - pj_ks (pj, key, name->str_value); - } - } - r_json_free (root); - } + if (json_start && *json_start) { + json = strdup (json_start); + } else { + json = strdup ("{}"); } - free (aflj); - - /* fs *;fj: include import/plt flags such as sym.imp.memcpy */ - char *fj = r_core_cmd_str (core, "fs *;fj"); - if (fj && fj[0] == '[') { - RJson *root = r_json_parse (fj); - if (root && root->type == R_JSON_ARRAY) { - RJson *elem; - for (elem = root->children.first; elem; elem = elem->next) { - if (elem->type != R_JSON_OBJECT) { - continue; - } - const RJson *addr = r_json_get (elem, "addr"); - const RJson *name = r_json_get (elem, "name"); - if (addr && name && addr->type == R_JSON_INTEGER && name->type == R_JSON_STRING && name->str_value) { - char key[32]; - snprintf (key, sizeof (key), "0x%llx", (unsigned long long)addr->num.u_value); - pj_ks (pj, key, name->str_value); - } - } - r_json_free (root); - } + free (owned); + if (!json) { + return false; } - free (fj); - - pj_end (pj); - return pj_drain (pj); + r_str_unescape (json); + *out_json = json; + return true; } -static bool ssa_var_to_reg_name(const char *ssa_name, char *out, size_t out_size) { - if (!ssa_name || !out || out_size == 0) { - return false; - } +typedef enum { + REPLAY_EXPR_CONST = 0, + REPLAY_EXPR_REG, + REPLAY_EXPR_MEM, + REPLAY_EXPR_META, + REPLAY_EXPR_UNARY, + REPLAY_EXPR_BINARY, +} ReplayExprKind; - const char *suffix = strrchr (ssa_name, '_'); - size_t len = suffix ? (size_t)(suffix - ssa_name) : strlen (ssa_name); - if (len == 0 || len >= out_size) { - return false; - } +typedef enum { + REPLAY_MEM_U8 = 8, + REPLAY_MEM_U16 = 16, + REPLAY_MEM_U32 = 32, + REPLAY_MEM_U64 = 64, +} ReplayMemWidth; - char base[128]; - if (len >= sizeof (base)) { - return false; +typedef enum { + REPLAY_META_DEPTH = 0, + REPLAY_META_INPUT_LEN, +} ReplayMetaKind; + +typedef enum { + REPLAY_UN_NEG = 0, + REPLAY_UN_NOT, +} ReplayUnaryOp; + +typedef enum { + REPLAY_BIN_ADD = 0, + REPLAY_BIN_SUB, + REPLAY_BIN_MUL, + REPLAY_BIN_DIV, + REPLAY_BIN_MOD, + REPLAY_BIN_SHL, + REPLAY_BIN_SHR, + REPLAY_BIN_BAND, + REPLAY_BIN_BOR, + REPLAY_BIN_BXOR, + REPLAY_BIN_EQ, + REPLAY_BIN_NE, + REPLAY_BIN_LT, + REPLAY_BIN_LE, + REPLAY_BIN_GT, + REPLAY_BIN_GE, + REPLAY_BIN_AND, + REPLAY_BIN_OR, + REPLAY_BIN_ABSDIFF, +} ReplayBinaryOp; + +typedef enum { + REPLAY_SCORE_MAX = 0, + REPLAY_SCORE_MIN, +} ReplayScoreOrder; + +typedef struct replay_expr_t ReplayExpr; + +struct replay_expr_t { + int kind; + union { + st64 const_value; + char *reg_name; + struct { + ut64 addr; + int width_bits; + } mem; + int meta_kind; + struct { + int op; + ReplayExpr *arg; + } unary; + struct { + int op; + ReplayExpr *lhs; + ReplayExpr *rhs; + } binary; + }; +}; + +typedef struct { + bool ok; + bool is_bool; + union { + st64 i; + bool b; + }; +} ReplayEvalValue; + +typedef struct { + const RDebugStateSnapshot *snapshot; + size_t depth; + size_t input_len; + bool big_endian; +} ReplayEvalContext; + +typedef struct { + ut64 seed_checkpoint; + int replay_fd; + char *alphabet; + size_t max_depth; + size_t beam_width; + ReplayExpr **frontier_preds; + size_t frontier_count; + ReplayExpr **find_preds; + size_t find_count; + ReplayExpr **avoid_preds; + size_t avoid_count; + ReplayExpr *score_expr; + int score_order; + RDebugStateRequest *snapshot_request; + ut64 *frontier_stop_addrs; + size_t frontier_stop_count; + ut64 *stop_addrs; + size_t stop_count; + bool big_endian; +} ReplaySearchSpec; + +typedef struct { + char *name; + char *symbol; +} ReplaySymRegisterOverlay; + +typedef struct { + ut64 addr; + ut32 size; + char *name; +} ReplaySymMemoryOverlay; + +typedef struct { + ut64 checkpoint_id; + ut64 entry_addr; + RDebugStateRequest *snapshot_request; + ReplaySymRegisterOverlay *register_overlays; + size_t register_overlay_count; + ReplaySymMemoryOverlay *memory_overlays; + size_t memory_overlay_count; + int *tty_fds; + size_t tty_fd_count; + bool skip_sleep_calls; +} ReplaySymSeedSpec; + +typedef struct { + ut64 checkpoint_id; + char *input; + size_t input_len; + st64 score; + char *snapshot_json; +} ReplaySearchNode; + +typedef struct { + ut64 checkpoint_id; + char *input; + size_t input_len; + ut64 hit_addr; + st64 score; + char *snapshot_json; +} ReplaySearchMatch; + +typedef enum { + REPLAY_SEARCH_STOP_NONE = 0, + REPLAY_SEARCH_STOP_FRONTIER, + REPLAY_SEARCH_STOP_FIND, + REPLAY_SEARCH_STOP_AVOID, + REPLAY_SEARCH_STOP_OTHER, +} ReplaySearchStopKind; + +typedef struct { + ut64 *addrs; + size_t count; +} ReplayTempBpSet; + +static bool replay_parse_num_expr(RCore *core, const RJson *value, st64 *out) { + if (!core || !value || !out) { + return false; + } + if (value->type == R_JSON_INTEGER) { + *out = value->num.s_value; + return true; + } + if (value->type == R_JSON_STRING && value->str_value && *value->str_value) { + *out = (st64)r_num_math (core->num, value->str_value); + return true; + } + return false; +} + +static bool replay_parse_addr_expr(RCore *core, const RJson *value, ut64 *out) { + st64 signed_value = 0; + if (!replay_parse_num_expr (core, value, &signed_value) || signed_value < 0) { + return false; + } + *out = (ut64)signed_value; + return true; +} + +static void replay_expr_free(ReplayExpr *expr) { + if (!expr) { + return; + } + switch (expr->kind) { + case REPLAY_EXPR_REG: + free (expr->reg_name); + break; + case REPLAY_EXPR_UNARY: + replay_expr_free (expr->unary.arg); + break; + case REPLAY_EXPR_BINARY: + replay_expr_free (expr->binary.lhs); + replay_expr_free (expr->binary.rhs); + break; + default: + break; + } + free (expr); +} + +static void replay_expr_array_free(ReplayExpr **exprs, size_t count) { + size_t i; + if (!exprs) { + return; + } + for (i = 0; i < count; i++) { + replay_expr_free (exprs[i]); + } + free (exprs); +} + +static bool replay_is_pc_reg_name(const char *name) { + return name && !strcasecmp (name, "pc"); +} + +static bool replay_expr_is_const_int(const ReplayExpr *expr, st64 *out) { + if (!expr || expr->kind != REPLAY_EXPR_CONST) { + return false; + } + if (out) { + *out = expr->const_value; + } + return true; +} + +static bool replay_expr_extract_pc_eq_addr(const ReplayExpr *expr, ut64 *out_addr) { + st64 value = 0; + if (!expr || expr->kind != REPLAY_EXPR_BINARY || expr->binary.op != REPLAY_BIN_EQ) { + return false; + } + if (expr->binary.lhs && expr->binary.lhs->kind == REPLAY_EXPR_REG + && replay_is_pc_reg_name (expr->binary.lhs->reg_name) + && replay_expr_is_const_int (expr->binary.rhs, &value) + && value >= 0) { + *out_addr = (ut64)value; + return true; + } + if (expr->binary.rhs && expr->binary.rhs->kind == REPLAY_EXPR_REG + && replay_is_pc_reg_name (expr->binary.rhs->reg_name) + && replay_expr_is_const_int (expr->binary.lhs, &value) + && value >= 0) { + *out_addr = (ut64)value; + return true; + } + return false; +} + +static bool replay_addr_list_contains(const ut64 *addrs, size_t count, ut64 addr) { + size_t i; + for (i = 0; i < count; i++) { + if (addrs[i] == addr) { + return true; + } + } + return false; +} + +static bool replay_addr_list_push_unique(ut64 **addrs, size_t *count, ut64 addr) { + ut64 *next; + if (!addrs || !count || !addr) { + return false; + } + if (replay_addr_list_contains (*addrs, *count, addr)) { + return true; + } + next = realloc (*addrs, (*count + 1) * sizeof (ut64)); + if (!next) { + return false; + } + *addrs = next; + (*addrs)[(*count)++] = addr; + return true; +} + +static const char *replay_json_kind_name(const RJson *value) { + if (!value || value->type != R_JSON_OBJECT) { + return NULL; + } + const RJson *kind = r_json_get (value, "kind"); + return (kind && kind->type == R_JSON_STRING)? kind->str_value: NULL; +} + +static const char *replay_json_op_name(const RJson *value) { + if (!value || value->type != R_JSON_OBJECT) { + return NULL; + } + const RJson *op = r_json_get (value, "op"); + return (op && op->type == R_JSON_STRING)? op->str_value: NULL; +} + +static bool replay_json_get_arg_array(const RJson *value, const RJson **first, const RJson **second, size_t *count) { + const RJson *args = r_json_get (value, "args"); + RJson *child; + size_t idx = 0; + if (!first || !second || !count) { + return false; + } + *first = NULL; + *second = NULL; + *count = 0; + if (!args || args->type != R_JSON_ARRAY) { + return false; + } + for (child = args->children.first; child; child = child->next) { + if (idx == 0) { + *first = child; + } else if (idx == 1) { + *second = child; + } + idx++; + } + *count = idx; + return true; +} + +static bool replay_parse_unary_op(const char *name, int *out) { + if (!name || !out) { + return false; + } + if (!strcmp (name, "neg")) { + *out = REPLAY_UN_NEG; + return true; + } + if (!strcmp (name, "not")) { + *out = REPLAY_UN_NOT; + return true; + } + return false; +} + +static bool replay_parse_binary_op(const char *name, int *out) { + if (!name || !out) { + return false; + } + if (!strcmp (name, "add")) { *out = REPLAY_BIN_ADD; return true; } + if (!strcmp (name, "sub")) { *out = REPLAY_BIN_SUB; return true; } + if (!strcmp (name, "mul")) { *out = REPLAY_BIN_MUL; return true; } + if (!strcmp (name, "div")) { *out = REPLAY_BIN_DIV; return true; } + if (!strcmp (name, "mod")) { *out = REPLAY_BIN_MOD; return true; } + if (!strcmp (name, "shl")) { *out = REPLAY_BIN_SHL; return true; } + if (!strcmp (name, "shr")) { *out = REPLAY_BIN_SHR; return true; } + if (!strcmp (name, "band")) { *out = REPLAY_BIN_BAND; return true; } + if (!strcmp (name, "bor")) { *out = REPLAY_BIN_BOR; return true; } + if (!strcmp (name, "bxor")) { *out = REPLAY_BIN_BXOR; return true; } + if (!strcmp (name, "eq")) { *out = REPLAY_BIN_EQ; return true; } + if (!strcmp (name, "ne")) { *out = REPLAY_BIN_NE; return true; } + if (!strcmp (name, "lt")) { *out = REPLAY_BIN_LT; return true; } + if (!strcmp (name, "le")) { *out = REPLAY_BIN_LE; return true; } + if (!strcmp (name, "gt")) { *out = REPLAY_BIN_GT; return true; } + if (!strcmp (name, "ge")) { *out = REPLAY_BIN_GE; return true; } + if (!strcmp (name, "and")) { *out = REPLAY_BIN_AND; return true; } + if (!strcmp (name, "or")) { *out = REPLAY_BIN_OR; return true; } + if (!strcmp (name, "absdiff")) { *out = REPLAY_BIN_ABSDIFF; return true; } + return false; +} + +static bool replay_parse_meta_kind(const char *name, int *out) { + if (!name || !out) { + return false; + } + if (!strcmp (name, "depth")) { + *out = REPLAY_META_DEPTH; + return true; + } + if (!strcmp (name, "input_len")) { + *out = REPLAY_META_INPUT_LEN; + return true; + } + return false; +} + +static bool replay_expr_parse(RCore *core, const RJson *value, ReplayExpr **out_expr) { + const char *kind_name; + const char *op_name; + ReplayExpr *expr = NULL; + const RJson *lhs = NULL; + const RJson *rhs = NULL; + const RJson *arg = NULL; + const RJson *first = NULL; + const RJson *second = NULL; + size_t arg_count = 0; + + R_RETURN_VAL_IF_FAIL (core && value && out_expr, false); + *out_expr = NULL; + if (value->type != R_JSON_OBJECT) { + return false; + } + expr = R_NEW0 (ReplayExpr); + if (!expr) { + return false; + } + + kind_name = replay_json_kind_name (value); + if (kind_name) { + if (!strcmp (kind_name, "const")) { + expr->kind = REPLAY_EXPR_CONST; + if (!replay_parse_num_expr (core, r_json_get (value, "value"), &expr->const_value)) { + goto fail; + } + } else if (!strcmp (kind_name, "reg")) { + const RJson *name = r_json_get (value, "name"); + expr->kind = REPLAY_EXPR_REG; + if (!name || name->type != R_JSON_STRING || R_STR_ISEMPTY (name->str_value)) { + goto fail; + } + expr->reg_name = strdup (name->str_value); + if (!expr->reg_name) { + goto fail; + } + } else if (!strcmp (kind_name, "mem_u8") || !strcmp (kind_name, "mem_u16") + || !strcmp (kind_name, "mem_u32") || !strcmp (kind_name, "mem_u64")) { + expr->kind = REPLAY_EXPR_MEM; + if (!replay_parse_addr_expr (core, r_json_get (value, "addr"), &expr->mem.addr)) { + goto fail; + } + if (!strcmp (kind_name, "mem_u8")) { + expr->mem.width_bits = REPLAY_MEM_U8; + } else if (!strcmp (kind_name, "mem_u16")) { + expr->mem.width_bits = REPLAY_MEM_U16; + } else if (!strcmp (kind_name, "mem_u32")) { + expr->mem.width_bits = REPLAY_MEM_U32; + } else { + expr->mem.width_bits = REPLAY_MEM_U64; + } + } else if (!strcmp (kind_name, "meta")) { + const RJson *name = r_json_get (value, "name"); + expr->kind = REPLAY_EXPR_META; + if (!name || name->type != R_JSON_STRING || !replay_parse_meta_kind (name->str_value, &expr->meta_kind)) { + goto fail; + } + } else { + goto fail; + } + *out_expr = expr; + return true; + } + + op_name = replay_json_op_name (value); + if (!op_name) { + goto fail; + } + if (replay_parse_unary_op (op_name, &expr->unary.op)) { + expr->kind = REPLAY_EXPR_UNARY; + arg = r_json_get (value, "arg"); + if (!arg && replay_json_get_arg_array (value, &first, &second, &arg_count) && arg_count == 1) { + arg = first; + } + if (!arg || !replay_expr_parse (core, arg, &expr->unary.arg)) { + goto fail; + } + *out_expr = expr; + return true; + } + if (!replay_parse_binary_op (op_name, &expr->binary.op)) { + goto fail; + } + expr->kind = REPLAY_EXPR_BINARY; + lhs = r_json_get (value, "lhs"); + rhs = r_json_get (value, "rhs"); + if ((!lhs || !rhs) && replay_json_get_arg_array (value, &first, &second, &arg_count) && arg_count == 2) { + lhs = first; + rhs = second; + } + if (!lhs || !rhs) { + goto fail; + } + if (!replay_expr_parse (core, lhs, &expr->binary.lhs) || !replay_expr_parse (core, rhs, &expr->binary.rhs)) { + goto fail; + } + *out_expr = expr; + return true; + +fail: + replay_expr_free (expr); + return false; +} + +static bool replay_parse_predicate_array(RCore *core, const RJson *value, bool allow_empty, ReplayExpr ***out_exprs, size_t *out_count) { + ReplayExpr **exprs = NULL; + size_t count = 0; + RJson *child; + if (!out_exprs || !out_count) { + return false; + } + *out_exprs = NULL; + *out_count = 0; + if (!value || value->type != R_JSON_ARRAY) { + return false; + } + for (child = value->children.first; child; child = child->next) { + ReplayExpr *expr = NULL; + ReplayExpr **next; + if (!replay_expr_parse (core, child, &expr)) { + replay_expr_array_free (exprs, count); + return false; + } + next = realloc (exprs, (count + 1) * sizeof (ReplayExpr *)); + if (!next) { + replay_expr_free (expr); + replay_expr_array_free (exprs, count); + return false; + } + exprs = next; + exprs[count++] = expr; + } + if (!count) { + return allow_empty; + } + *out_exprs = exprs; + *out_count = count; + return true; +} + +static RDebugStateRequest *replay_state_request_new(void) { + RDebugStateRequest *request = R_NEW0 (RDebugStateRequest); + if (!request) { + return NULL; + } + request->registers = r_list_newf ((RListFree)r_debug_state_reg_spec_free); + request->memory = r_list_newf ((RListFree)r_debug_state_mem_spec_free); + if (!request->registers || !request->memory) { + r_debug_state_request_free (request); + return NULL; + } + return request; +} + +static bool replay_state_request_add_reg(RDebugStateRequest *request, const char *name) { + RListIter *iter; + RDebugStateRegSpec *spec; + if (!request || !name || replay_is_pc_reg_name (name)) { + return true; + } + r_list_foreach (request->registers, iter, spec) { + if (spec->name && !strcasecmp (spec->name, name)) { + return true; + } + } + spec = R_NEW0 (RDebugStateRegSpec); + if (!spec) { + return false; + } + spec->name = strdup (name); + if (!spec->name) { + r_debug_state_reg_spec_free (spec); + return false; + } + r_list_append (request->registers, spec); + return true; +} + +static bool replay_state_request_add_mem_range(RDebugStateRequest *request, ut64 addr, ut32 size, const char *label) { + RListIter *iter; + RDebugStateMemSpec *spec; + if (!request || !size) { + return false; + } + r_list_foreach (request->memory, iter, spec) { + if (spec->addr == addr && spec->size == size) { + return true; + } + } + spec = R_NEW0 (RDebugStateMemSpec); + if (!spec) { + return false; + } + spec->addr = addr; + spec->size = size; + if (label && *label) { + spec->label = strdup (label); + if (!spec->label) { + r_debug_state_mem_spec_free (spec); + return false; + } + } + r_list_append (request->memory, spec); + return true; +} + +static bool replay_state_request_add_mem(RDebugStateRequest *request, ut64 addr, int width_bits) { + ut32 size = (ut32)(width_bits / 8); + return replay_state_request_add_mem_range (request, addr, size, NULL); +} + +static bool replay_state_request_add_all_gprs(RDebug *dbg, RDebugStateRequest *request) { + RListIter *iter; + RRegItem *item; + RList *regs; + if (!dbg || !dbg->reg || !request) { + return false; + } + regs = r_reg_get_list (dbg->reg, R_REG_TYPE_GPR); + if (!regs) { + return false; + } + r_list_foreach (regs, iter, item) { + if (item && item->name && !replay_state_request_add_reg (request, item->name)) { + return false; + } + } + return true; +} + +static bool replay_expr_collect_state(const ReplayExpr *expr, RDebugStateRequest *request) { + if (!expr || !request) { + return false; + } + switch (expr->kind) { + case REPLAY_EXPR_REG: + return replay_state_request_add_reg (request, expr->reg_name); + case REPLAY_EXPR_MEM: + return replay_state_request_add_mem (request, expr->mem.addr, expr->mem.width_bits); + case REPLAY_EXPR_UNARY: + return replay_expr_collect_state (expr->unary.arg, request); + case REPLAY_EXPR_BINARY: + return replay_expr_collect_state (expr->binary.lhs, request) + && replay_expr_collect_state (expr->binary.rhs, request); + default: + return true; + } +} + +static bool replay_collect_stop_addrs(ReplayExpr **exprs, size_t count, ut64 **out_addrs, size_t *out_count) { + size_t i; + if (!out_addrs || !out_count) { + return false; + } + *out_addrs = NULL; + *out_count = 0; + for (i = 0; i < count; i++) { + ut64 addr = 0; + if (replay_expr_extract_pc_eq_addr (exprs[i], &addr) && !replay_addr_list_push_unique (out_addrs, out_count, addr)) { + free (*out_addrs); + *out_addrs = NULL; + *out_count = 0; + return false; + } + } + return true; +} + +static void replay_search_spec_fini(ReplaySearchSpec *spec) { + if (!spec) { + return; + } + free (spec->alphabet); + spec->alphabet = NULL; + replay_expr_array_free (spec->frontier_preds, spec->frontier_count); + replay_expr_array_free (spec->find_preds, spec->find_count); + replay_expr_array_free (spec->avoid_preds, spec->avoid_count); + spec->frontier_preds = NULL; + spec->find_preds = NULL; + spec->avoid_preds = NULL; + spec->frontier_count = 0; + spec->find_count = 0; + spec->avoid_count = 0; + replay_expr_free (spec->score_expr); + spec->score_expr = NULL; + r_debug_state_request_free (spec->snapshot_request); + spec->snapshot_request = NULL; + free (spec->frontier_stop_addrs); + free (spec->stop_addrs); + spec->frontier_stop_addrs = NULL; + spec->stop_addrs = NULL; + spec->frontier_stop_count = 0; + spec->stop_count = 0; +} + +static void replay_sym_seed_spec_fini(ReplaySymSeedSpec *spec) { + size_t i; + if (!spec) { + return; + } + r_debug_state_request_free (spec->snapshot_request); + spec->snapshot_request = NULL; + for (i = 0; i < spec->register_overlay_count; i++) { + free (spec->register_overlays[i].name); + free (spec->register_overlays[i].symbol); + } + free (spec->register_overlays); + spec->register_overlays = NULL; + spec->register_overlay_count = 0; + for (i = 0; i < spec->memory_overlay_count; i++) { + free (spec->memory_overlays[i].name); + } + free (spec->memory_overlays); + spec->memory_overlays = NULL; + spec->memory_overlay_count = 0; + free (spec->tty_fds); + spec->tty_fds = NULL; + spec->tty_fd_count = 0; + spec->checkpoint_id = 0; + spec->entry_addr = 0; + spec->skip_sleep_calls = false; +} + +static bool replay_sym_seed_add_register_overlay(ReplaySymSeedSpec *spec, const char *name, const char *symbol) { + ReplaySymRegisterOverlay *next; + size_t index; + if (!spec || !name || !*name || !symbol || !*symbol) { + return false; + } + next = realloc (spec->register_overlays, (spec->register_overlay_count + 1) * sizeof (*next)); + if (!next) { + return false; + } + spec->register_overlays = next; + index = spec->register_overlay_count++; + memset (&spec->register_overlays[index], 0, sizeof (spec->register_overlays[index])); + spec->register_overlays[index].name = strdup (name); + spec->register_overlays[index].symbol = strdup (symbol); + if (!spec->register_overlays[index].name || !spec->register_overlays[index].symbol) { + free (spec->register_overlays[index].name); + free (spec->register_overlays[index].symbol); + spec->register_overlays[index].name = NULL; + spec->register_overlays[index].symbol = NULL; + spec->register_overlay_count--; + return false; + } + return true; +} + +static bool replay_sym_seed_add_memory_overlay(ReplaySymSeedSpec *spec, ut64 addr, ut32 size, const char *name) { + ReplaySymMemoryOverlay *next; + size_t index; + if (!spec || !size || !name || !*name) { + return false; + } + next = realloc (spec->memory_overlays, (spec->memory_overlay_count + 1) * sizeof (*next)); + if (!next) { + return false; + } + spec->memory_overlays = next; + index = spec->memory_overlay_count++; + memset (&spec->memory_overlays[index], 0, sizeof (spec->memory_overlays[index])); + spec->memory_overlays[index].addr = addr; + spec->memory_overlays[index].size = size; + spec->memory_overlays[index].name = strdup (name); + if (!spec->memory_overlays[index].name) { + spec->memory_overlay_count--; + return false; + } + return true; +} + +static bool replay_sym_seed_add_tty_fd(ReplaySymSeedSpec *spec, int fd) { + int *next; + if (!spec) { + return false; + } + next = realloc (spec->tty_fds, (spec->tty_fd_count + 1) * sizeof (*next)); + if (!next) { + return false; + } + spec->tty_fds = next; + spec->tty_fds[spec->tty_fd_count++] = fd; + return true; +} + +static bool replay_sym_seed_spec_parse(RCore *core, const char *json, ReplaySymSeedSpec *spec, bool require_checkpoint) { + char *json_copy; + char *owned_json = NULL; + RJson *root; + const RJson *value; + size_t i; + + R_RETURN_VAL_IF_FAIL (core && core->dbg && json && spec, false); + memset (spec, 0, sizeof (*spec)); + spec->snapshot_request = replay_state_request_new (); + if (!spec->snapshot_request || !replay_state_request_add_all_gprs (core->dbg, spec->snapshot_request)) { + replay_sym_seed_spec_fini (spec); + return false; + } + + json_copy = strdup (json); + if (!json_copy) { + replay_sym_seed_spec_fini (spec); + return false; + } + owned_json = json_copy; + root = r_json_parse (json_copy); + if (!root || root->type != R_JSON_OBJECT) { + R_LOG_ERROR ("r2sleigh replay sym seed: json root parse failed"); + free (owned_json); + r_json_free (root); + replay_sym_seed_spec_fini (spec); + return false; + } + + value = r_json_get (root, "checkpoint"); + if (!value) { + value = r_json_get (root, "seed_checkpoint"); + } + if (value) { + if (!replay_parse_addr_expr (core, value, &spec->checkpoint_id) || !spec->checkpoint_id) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid checkpoint"); + goto fail; + } + } else if (require_checkpoint) { + R_LOG_ERROR ("r2sleigh replay sym seed: missing/invalid checkpoint"); + goto fail; + } + value = r_json_get (root, "entry"); + if (value && !replay_parse_addr_expr (core, value, &spec->entry_addr)) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid entry"); + goto fail; + } + value = r_json_get (root, "skip_sleep"); + if (value) { + if (value->type != R_JSON_BOOLEAN) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid skip_sleep"); + goto fail; + } + spec->skip_sleep_calls = value->num.u_value; + } + value = r_json_get (root, "tty_fds"); + if (value) { + if (value->type != R_JSON_ARRAY) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid tty_fds"); + goto fail; + } + for (i = 0; i < value->children.count; i++) { + const RJson *item = r_json_item (value, i); + st64 fd = 0; + if (!replay_parse_num_expr (core, item, &fd)) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid tty fd"); + goto fail; + } + if (!replay_sym_seed_add_tty_fd (spec, (int)fd)) { + goto fail; + } + } + } + value = r_json_get (root, "memory"); + if (value) { + if (value->type != R_JSON_ARRAY) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid memory"); + goto fail; + } + for (i = 0; i < value->children.count; i++) { + const RJson *item = r_json_item (value, i); + const RJson *label_json; + char *label = NULL; + ut64 addr = 0; + st64 size_value = 0; + if (!item || item->type != R_JSON_OBJECT) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid memory item"); + goto fail; + } + if (!replay_parse_addr_expr (core, r_json_get (item, "addr"), &addr) + || !replay_parse_num_expr (core, r_json_get (item, "size"), &size_value) + || size_value <= 0 || size_value > UT32_MAX) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid memory window"); + goto fail; + } + label_json = r_json_get (item, "label"); + if (label_json && label_json->type == R_JSON_STRING && label_json->str_value) { + label = strdup (label_json->str_value); + if (!label) { + goto fail; + } + } + if (!replay_state_request_add_mem_range (spec->snapshot_request, addr, (ut32)size_value, label)) { + free (label); + goto fail; + } + free (label); + } + } + value = r_json_get (root, "symbolic_registers"); + if (value) { + if (value->type != R_JSON_ARRAY) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid symbolic_registers"); + goto fail; + } + for (i = 0; i < value->children.count; i++) { + const RJson *item = r_json_item (value, i); + const char *name = NULL; + const char *symbol = NULL; + char default_symbol[128]; + if (!item) { + goto fail; + } + if (item->type == R_JSON_STRING && item->str_value) { + name = item->str_value; + } else if (item->type == R_JSON_OBJECT) { + const RJson *name_json = r_json_get (item, "name"); + const RJson *symbol_json = r_json_get (item, "symbol"); + if (name_json && name_json->type == R_JSON_STRING) { + name = name_json->str_value; + } + if (symbol_json && symbol_json->type == R_JSON_STRING) { + symbol = symbol_json->str_value; + } + } + if (!name || !*name) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid symbolic register"); + goto fail; + } + if (!symbol || !*symbol) { + snprintf (default_symbol, sizeof (default_symbol), "replay_%s", name); + symbol = default_symbol; + } + if (!replay_sym_seed_add_register_overlay (spec, name, symbol)) { + goto fail; + } + } + } + value = r_json_get (root, "symbolic_memory"); + if (value) { + if (value->type != R_JSON_ARRAY) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid symbolic_memory"); + goto fail; + } + for (i = 0; i < value->children.count; i++) { + const RJson *item = r_json_item (value, i); + const RJson *name_json; + char default_name[128]; + const char *name = NULL; + ut64 addr = 0; + st64 size_value = 0; + if (!item || item->type != R_JSON_OBJECT) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid symbolic memory item"); + goto fail; + } + if (!replay_parse_addr_expr (core, r_json_get (item, "addr"), &addr) + || !replay_parse_num_expr (core, r_json_get (item, "size"), &size_value) + || size_value <= 0 || size_value > UT32_MAX) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid symbolic memory window"); + goto fail; + } + name_json = r_json_get (item, "name"); + if (name_json && name_json->type == R_JSON_STRING && name_json->str_value) { + name = name_json->str_value; + } else { + snprintf (default_name, sizeof (default_name), "replay_mem_%zu", i); + name = default_name; + } + if (!replay_state_request_add_mem_range (spec->snapshot_request, addr, (ut32)size_value, name) + || !replay_sym_seed_add_memory_overlay (spec, addr, (ut32)size_value, name)) { + goto fail; + } + } + } + + free (owned_json); + r_json_free (root); + return true; + +fail: + free (owned_json); + r_json_free (root); + replay_sym_seed_spec_fini (spec); + return false; +} + +static RDebugStateSnapshot *replay_sym_collect_seed_snapshot(RCore *core, const ReplaySymSeedSpec *spec) { + RDebugStateSnapshot *snapshot; + ut64 previous_checkpoint; + + R_RETURN_VAL_IF_FAIL (core && core->dbg && spec && spec->snapshot_request, NULL); + if (!spec->checkpoint_id) { + return r_debug_state_snapshot_collect (core->dbg, spec->snapshot_request); + } + R_RETURN_VAL_IF_FAIL (core->dbg->session, NULL); + previous_checkpoint = core->dbg->session->current_checkpoint_id; + if (!r_debug_session_restore_checkpoint (core->dbg, spec->checkpoint_id)) { + return NULL; + } + snapshot = r_debug_state_snapshot_collect (core->dbg, spec->snapshot_request); + if (previous_checkpoint != UT64_MAX && previous_checkpoint != spec->checkpoint_id) { + r_debug_session_restore_checkpoint (core->dbg, previous_checkpoint); + } + return snapshot; +} + +static char *replay_sym_query_run(RCore *core, const R2ILContext *ctx, const SymFunctionScope *scope, + ut64 entry_addr, ut64 target_addr, const ReplaySymSeedSpec *spec, bool is_explore, + const char *external_context_json) { + RDebugStateSnapshot *snapshot = NULL; + R2SymReplayRegister *registers = NULL; + R2SymReplayMemoryWindow *memory = NULL; + R2SymReplayRegisterOverlay *register_overlays = NULL; + R2SymReplayMemoryOverlay *memory_overlays = NULL; + R2SymReplaySeed seed = {0}; + RListIter *iter; + RDebugStateRegValue *reg; + RDebugStateMemValue *memv; + size_t reg_count = 0; + size_t mem_count = 0; + size_t idx = 0; + char *result = NULL; + + R_RETURN_VAL_IF_FAIL (core && ctx && scope && spec, NULL); + + snapshot = replay_sym_collect_seed_snapshot (core, spec); + if (!snapshot) { + return NULL; + } + r_list_foreach (snapshot->registers, iter, reg) { + if (reg && reg->found && reg->name) { + reg_count++; + } + } + r_list_foreach (snapshot->memory, iter, memv) { + if (memv && memv->ok && memv->bytes && memv->size > 0) { + mem_count++; + } + } + registers = reg_count? calloc (reg_count, sizeof (*registers)): NULL; + memory = mem_count? calloc (mem_count, sizeof (*memory)): NULL; + register_overlays = spec->register_overlay_count? calloc (spec->register_overlay_count, sizeof (*register_overlays)): NULL; + memory_overlays = spec->memory_overlay_count? calloc (spec->memory_overlay_count, sizeof (*memory_overlays)): NULL; + if ((reg_count && !registers) || (mem_count && !memory) + || (spec->register_overlay_count && !register_overlays) + || (spec->memory_overlay_count && !memory_overlays)) { + goto cleanup; + } + + idx = 0; + r_list_foreach (snapshot->registers, iter, reg) { + if (!reg || !reg->found || !reg->name) { + continue; + } + registers[idx].name = reg->name; + registers[idx].value = reg->value; + idx++; + } + idx = 0; + r_list_foreach (snapshot->memory, iter, memv) { + if (!memv || !memv->ok || !memv->bytes || !memv->size) { + continue; + } + memory[idx].addr = memv->addr; + memory[idx].bytes = memv->bytes; + memory[idx].size = memv->size; + memory[idx].label = memv->label; + idx++; + } + for (idx = 0; idx < spec->register_overlay_count; idx++) { + register_overlays[idx].name = spec->register_overlays[idx].name; + register_overlays[idx].symbol = spec->register_overlays[idx].symbol; + } + for (idx = 0; idx < spec->memory_overlay_count; idx++) { + memory_overlays[idx].addr = spec->memory_overlays[idx].addr; + memory_overlays[idx].size = spec->memory_overlays[idx].size; + memory_overlays[idx].name = spec->memory_overlays[idx].name; + } + + seed.checkpoint_id = spec->checkpoint_id; + seed.entry_addr = spec->entry_addr? spec->entry_addr: snapshot->pc; + seed.registers = registers; + seed.num_registers = reg_count; + seed.memory = memory; + seed.num_memory = mem_count; + seed.register_overlays = register_overlays; + seed.num_register_overlays = spec->register_overlay_count; + seed.memory_overlays = memory_overlays; + seed.num_memory_overlays = spec->memory_overlay_count; + seed.tty_fds = spec->tty_fds; + seed.num_tty_fds = spec->tty_fd_count; + seed.skip_sleep_calls = spec->skip_sleep_calls? 1: 0; + + result = is_explore + ? r2sym_explore_to_replay_scope (ctx, scope->functions, scope->count, entry_addr, target_addr, &seed, external_context_json) + : r2sym_solve_to_replay_scope (ctx, scope->functions, scope->count, entry_addr, target_addr, &seed, external_context_json); + +cleanup: + free (registers); + free (memory); + free (register_overlays); + free (memory_overlays); + r_debug_state_snapshot_free (snapshot); + return result; +} + +static void replay_search_node_free(ReplaySearchNode *node) { + if (!node) { + return; + } + free (node->input); + free (node->snapshot_json); + free (node); +} + +static void replay_search_match_free(ReplaySearchMatch *match) { + if (!match) { + return; + } + free (match->input); + free (match->snapshot_json); + free (match); +} + +static bool replay_eval_snapshot_reg(const ReplayEvalContext *ctx, const char *name, st64 *out) { + RListIter *iter; + RDebugStateRegValue *reg; + if (!ctx || !ctx->snapshot || !name || !out) { + return false; + } + if (replay_is_pc_reg_name (name)) { + *out = (st64)ctx->snapshot->pc; + return true; + } + r_list_foreach (ctx->snapshot->registers, iter, reg) { + if (reg->name && !strcasecmp (reg->name, name) && reg->found) { + *out = (st64)reg->value; + return true; + } + } + return false; +} + +static bool replay_eval_snapshot_mem(const ReplayEvalContext *ctx, ut64 addr, int width_bits, st64 *out) { + RListIter *iter; + RDebugStateMemValue *mem; + ut32 size = (ut32)(width_bits / 8); + if (!ctx || !ctx->snapshot || !out || !size) { + return false; + } + r_list_foreach (ctx->snapshot->memory, iter, mem) { + if (mem->addr == addr && mem->size == size && mem->ok && mem->bytes) { + *out = (st64)r_read_ble (mem->bytes, ctx->big_endian, size); + return true; + } + } + return false; +} + +static ReplayEvalValue replay_eval_error(void) { + ReplayEvalValue value = {0}; + return value; +} + +static ReplayEvalValue replay_eval_int(st64 i) { + ReplayEvalValue value = {0}; + value.ok = true; + value.i = i; + value.is_bool = false; + return value; +} + +static ReplayEvalValue replay_eval_bool(bool b) { + ReplayEvalValue value = {0}; + value.ok = true; + value.b = b; + value.is_bool = true; + return value; +} + +static ReplayEvalValue replay_eval_expr(const ReplayExpr *expr, const ReplayEvalContext *ctx) { + ReplayEvalValue lhs; + ReplayEvalValue rhs; + if (!expr || !ctx) { + return replay_eval_error (); + } + switch (expr->kind) { + case REPLAY_EXPR_CONST: + return replay_eval_int (expr->const_value); + case REPLAY_EXPR_REG: { + st64 value = 0; + return replay_eval_snapshot_reg (ctx, expr->reg_name, &value)? replay_eval_int (value): replay_eval_error (); + } + case REPLAY_EXPR_MEM: { + st64 value = 0; + return replay_eval_snapshot_mem (ctx, expr->mem.addr, expr->mem.width_bits, &value)? replay_eval_int (value): replay_eval_error (); + } + case REPLAY_EXPR_META: + return replay_eval_int (expr->meta_kind == REPLAY_META_DEPTH? (st64)ctx->depth: (st64)ctx->input_len); + case REPLAY_EXPR_UNARY: { + ReplayEvalValue arg = replay_eval_expr (expr->unary.arg, ctx); + if (!arg.ok) { + return arg; + } + if (expr->unary.op == REPLAY_UN_NEG) { + return arg.is_bool? replay_eval_error (): replay_eval_int (-arg.i); + } + if (expr->unary.op == REPLAY_UN_NOT) { + return arg.is_bool? replay_eval_bool (!arg.b): replay_eval_error (); + } + return replay_eval_error (); + } + case REPLAY_EXPR_BINARY: + lhs = replay_eval_expr (expr->binary.lhs, ctx); + rhs = replay_eval_expr (expr->binary.rhs, ctx); + if (!lhs.ok || !rhs.ok) { + return replay_eval_error (); + } + switch (expr->binary.op) { + case REPLAY_BIN_ADD: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_int (lhs.i + rhs.i): replay_eval_error (); + case REPLAY_BIN_SUB: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_int (lhs.i - rhs.i): replay_eval_error (); + case REPLAY_BIN_MUL: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_int (lhs.i * rhs.i): replay_eval_error (); + case REPLAY_BIN_DIV: return (!lhs.is_bool && !rhs.is_bool && rhs.i != 0)? replay_eval_int (lhs.i / rhs.i): replay_eval_error (); + case REPLAY_BIN_MOD: return (!lhs.is_bool && !rhs.is_bool && rhs.i != 0)? replay_eval_int (lhs.i % rhs.i): replay_eval_error (); + case REPLAY_BIN_SHL: return (!lhs.is_bool && !rhs.is_bool && rhs.i >= 0)? replay_eval_int ((st64)((ut64)lhs.i << rhs.i)): replay_eval_error (); + case REPLAY_BIN_SHR: return (!lhs.is_bool && !rhs.is_bool && rhs.i >= 0)? replay_eval_int ((st64)((ut64)lhs.i >> rhs.i)): replay_eval_error (); + case REPLAY_BIN_BAND: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_int ((st64)((ut64)lhs.i & (ut64)rhs.i)): replay_eval_error (); + case REPLAY_BIN_BOR: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_int ((st64)((ut64)lhs.i | (ut64)rhs.i)): replay_eval_error (); + case REPLAY_BIN_BXOR: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_int ((st64)((ut64)lhs.i ^ (ut64)rhs.i)): replay_eval_error (); + case REPLAY_BIN_EQ: + if (lhs.is_bool != rhs.is_bool) { + return replay_eval_error (); + } + return lhs.is_bool? replay_eval_bool (lhs.b == rhs.b): replay_eval_bool (lhs.i == rhs.i); + case REPLAY_BIN_NE: + if (lhs.is_bool != rhs.is_bool) { + return replay_eval_error (); + } + return lhs.is_bool? replay_eval_bool (lhs.b != rhs.b): replay_eval_bool (lhs.i != rhs.i); + case REPLAY_BIN_LT: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_bool (lhs.i < rhs.i): replay_eval_error (); + case REPLAY_BIN_LE: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_bool (lhs.i <= rhs.i): replay_eval_error (); + case REPLAY_BIN_GT: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_bool (lhs.i > rhs.i): replay_eval_error (); + case REPLAY_BIN_GE: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_bool (lhs.i >= rhs.i): replay_eval_error (); + case REPLAY_BIN_AND: return (lhs.is_bool && rhs.is_bool)? replay_eval_bool (lhs.b && rhs.b): replay_eval_error (); + case REPLAY_BIN_OR: return (lhs.is_bool && rhs.is_bool)? replay_eval_bool (lhs.b || rhs.b): replay_eval_error (); + case REPLAY_BIN_ABSDIFF: + if (lhs.is_bool || rhs.is_bool) { + return replay_eval_error (); + } + return replay_eval_int (lhs.i > rhs.i? lhs.i - rhs.i: rhs.i - lhs.i); + default: + return replay_eval_error (); + } + default: + return replay_eval_error (); + } +} + +static bool replay_eval_predicates(ReplayExpr **exprs, size_t count, const ReplayEvalContext *ctx) { + size_t i; + for (i = 0; i < count; i++) { + ReplayEvalValue value = replay_eval_expr (exprs[i], ctx); + if (value.ok && value.is_bool && value.b) { + return true; + } + } + return false; +} + +static bool replay_eval_score(const ReplaySearchSpec *spec, const ReplayEvalContext *ctx, st64 *out_score) { + ReplayEvalValue value; + if (!spec || !spec->score_expr || !out_score) { + return false; + } + value = replay_eval_expr (spec->score_expr, ctx); + if (!value.ok || value.is_bool) { + return false; + } + *out_score = value.i; + return true; +} + +static RDebugStateSnapshot *replay_collect_snapshot(RCore *core, const ReplaySearchSpec *spec) { + if (!core || !spec || !spec->snapshot_request) { + return NULL; + } + return r_debug_state_snapshot_collect (core->dbg, spec->snapshot_request); +} + +static bool replay_search_spec_parse(RCore *core, const char *json, ReplaySearchSpec *spec) { + char *json_copy; + char *owned_json = NULL; + RJson *root; + const RJson *value; + size_t i; + + R_RETURN_VAL_IF_FAIL (core && json && spec, false); + memset (spec, 0, sizeof (*spec)); + spec->replay_fd = 0; + spec->max_depth = 1; + spec->beam_width = 16; + spec->score_order = REPLAY_SCORE_MAX; + spec->big_endian = core->rasm && core->rasm->config + ? R_ARCH_CONFIG_IS_BIG_ENDIAN (core->rasm->config) + : false; + + json_copy = strdup (json); + if (!json_copy) { + return false; + } + owned_json = json_copy; + root = r_json_parse (json_copy); + if (!root || root->type != R_JSON_OBJECT) { + R_LOG_ERROR ("r2sleigh replayj: json root parse failed"); + free (owned_json); + r_json_free (root); + return false; + } + + value = r_json_get (root, "seed_checkpoint"); + if (!value) { + value = r_json_get (root, "seed"); + } + if (!replay_parse_addr_expr (core, value, &spec->seed_checkpoint) || !spec->seed_checkpoint) { + R_LOG_ERROR ("r2sleigh replayj: missing/invalid seed_checkpoint"); + goto fail; + } + value = r_json_get (root, "replay_fd"); + if (value) { + if (value->type != R_JSON_INTEGER) { + R_LOG_ERROR ("r2sleigh replayj: invalid replay_fd"); + goto fail; + } + spec->replay_fd = value->num.s_value; + } + value = r_json_get (root, "alphabet"); + if (!value || value->type != R_JSON_STRING || !value->str_value || !*value->str_value) { + R_LOG_ERROR ("r2sleigh replayj: missing/invalid alphabet"); + goto fail; + } + spec->alphabet = strdup (value->str_value); + if (!spec->alphabet) { + goto fail; + } + value = r_json_get (root, "max_depth"); + if (value) { + if (value->type != R_JSON_INTEGER || !value->num.u_value) { + R_LOG_ERROR ("r2sleigh replayj: invalid max_depth"); + goto fail; + } + spec->max_depth = (size_t)value->num.u_value; + } + value = r_json_get (root, "beam_width"); + if (value) { + if (value->type != R_JSON_INTEGER || !value->num.u_value) { + R_LOG_ERROR ("r2sleigh replayj: invalid beam_width"); + goto fail; + } + spec->beam_width = (size_t)value->num.u_value; + } + if (!replay_parse_predicate_array (core, r_json_get (root, "frontier"), false, &spec->frontier_preds, &spec->frontier_count)) { + R_LOG_ERROR ("r2sleigh replayj: missing/invalid frontier"); + goto fail; + } + if (!replay_parse_predicate_array (core, r_json_get (root, "find"), false, &spec->find_preds, &spec->find_count)) { + R_LOG_ERROR ("r2sleigh replayj: missing/invalid find"); + goto fail; + } + value = r_json_get (root, "avoid"); + if (value && !replay_parse_predicate_array (core, value, true, &spec->avoid_preds, &spec->avoid_count)) { + R_LOG_ERROR ("r2sleigh replayj: invalid avoid"); + goto fail; + } + value = r_json_get (root, "score"); + if (!value || value->type != R_JSON_OBJECT) { + R_LOG_ERROR ("r2sleigh replayj: missing/invalid score"); + goto fail; + } + { + const RJson *order = r_json_get (value, "order"); + if (!order || order->type != R_JSON_STRING) { + R_LOG_ERROR ("r2sleigh replayj: missing score.order"); + goto fail; + } + if (!strcmp (order->str_value, "max")) { + spec->score_order = REPLAY_SCORE_MAX; + } else if (!strcmp (order->str_value, "min")) { + spec->score_order = REPLAY_SCORE_MIN; + } else { + R_LOG_ERROR ("r2sleigh replayj: invalid score.order"); + goto fail; + } + if (!replay_expr_parse (core, r_json_get (value, "expr"), &spec->score_expr)) { + R_LOG_ERROR ("r2sleigh replayj: invalid score.expr"); + goto fail; + } + } + + spec->snapshot_request = replay_state_request_new (); + if (!spec->snapshot_request) { + goto fail; + } + for (i = 0; i < spec->frontier_count; i++) { + if (!replay_expr_collect_state (spec->frontier_preds[i], spec->snapshot_request)) { + goto fail; + } + } + for (i = 0; i < spec->find_count; i++) { + if (!replay_expr_collect_state (spec->find_preds[i], spec->snapshot_request)) { + goto fail; + } + } + for (i = 0; i < spec->avoid_count; i++) { + if (!replay_expr_collect_state (spec->avoid_preds[i], spec->snapshot_request)) { + goto fail; + } + } + if (!replay_expr_collect_state (spec->score_expr, spec->snapshot_request)) { + goto fail; + } + + if (!replay_collect_stop_addrs (spec->frontier_preds, spec->frontier_count, &spec->frontier_stop_addrs, &spec->frontier_stop_count) + || !spec->frontier_stop_count) { + R_LOG_ERROR ("r2sleigh replayj: frontier must contain at least one exact PC == const predicate"); + goto fail; + } + for (i = 0; i < spec->frontier_stop_count; i++) { + if (!replay_addr_list_push_unique (&spec->stop_addrs, &spec->stop_count, spec->frontier_stop_addrs[i])) { + goto fail; + } + } + { + ut64 *tmp = NULL; + size_t tmp_count = 0; + if (!replay_collect_stop_addrs (spec->find_preds, spec->find_count, &tmp, &tmp_count)) { + goto fail; + } + for (i = 0; i < tmp_count; i++) { + if (!replay_addr_list_push_unique (&spec->stop_addrs, &spec->stop_count, tmp[i])) { + free (tmp); + goto fail; + } + } + free (tmp); + tmp = NULL; + tmp_count = 0; + if (spec->avoid_count && !replay_collect_stop_addrs (spec->avoid_preds, spec->avoid_count, &tmp, &tmp_count)) { + goto fail; + } + for (i = 0; i < tmp_count; i++) { + if (!replay_addr_list_push_unique (&spec->stop_addrs, &spec->stop_count, tmp[i])) { + free (tmp); + goto fail; + } + } + free (tmp); + } + + free (owned_json); + r_json_free (root); + return true; + +fail: + free (owned_json); + r_json_free (root); + replay_search_spec_fini (spec); + return false; +} + +static char *replay_input_append_char(const char *input, size_t input_len, char ch) { + char *next = malloc (input_len + 2); + if (!next) { + return NULL; + } + if (input_len && input) { + memcpy (next, input, input_len); + } + next[input_len] = ch; + next[input_len + 1] = '\0'; + return next; +} + +static void replay_temp_bps_fini(RCore *core, ReplayTempBpSet *set) { + size_t i; + if (!core || !set || !set->addrs) { + return; + } + for (i = 0; i < set->count; i++) { + r_bp_del (core->dbg->bp, set->addrs[i]); + } + free (set->addrs); + set->addrs = NULL; + set->count = 0; +} + +static bool replay_temp_bps_add(RCore *core, ReplayTempBpSet *set, ut64 addr) { + ut64 *next; + if (!core || !set || !addr) { + return false; + } + if (r_bp_get_in (core->dbg->bp, addr, R_BP_PROT_EXEC)) { + return true; + } + if (!r_bp_add_sw (core->dbg->bp, addr, core->dbg->bpsize, R_BP_PROT_EXEC)) { + return false; + } + next = realloc (set->addrs, (set->count + 1) * sizeof (ut64)); + if (!next) { + r_bp_del (core->dbg->bp, addr); + return false; + } + set->addrs = next; + set->addrs[set->count++] = addr; + return true; +} + +static ReplaySearchStopKind replay_classify_stop(const ReplaySearchSpec *spec, const ReplayEvalContext *ctx) { + if (replay_eval_predicates (spec->find_preds, spec->find_count, ctx)) { + return REPLAY_SEARCH_STOP_FIND; + } + if (replay_eval_predicates (spec->avoid_preds, spec->avoid_count, ctx)) { + return REPLAY_SEARCH_STOP_AVOID; + } + if (replay_eval_predicates (spec->frontier_preds, spec->frontier_count, ctx)) { + return REPLAY_SEARCH_STOP_FRONTIER; + } + return REPLAY_SEARCH_STOP_OTHER; +} + +static ReplaySearchStopKind replay_continue_to_any(RCore *core, const ReplaySearchSpec *spec, size_t depth, size_t input_len, + ut64 *hit_addr, RDebugStateSnapshot **out_snapshot) { + ReplayTempBpSet temps = {0}; + size_t i; + ut64 pc = 0; + RDebugStateSnapshot *snapshot = NULL; + ReplayEvalContext eval_ctx; + + R_RETURN_VAL_IF_FAIL (core && core->dbg && spec && hit_addr && out_snapshot, REPLAY_SEARCH_STOP_NONE); + *hit_addr = 0; + *out_snapshot = NULL; + + snapshot = replay_collect_snapshot (core, spec); + if (!snapshot) { + goto cleanup; + } + eval_ctx.snapshot = snapshot; + eval_ctx.depth = depth; + eval_ctx.input_len = input_len; + eval_ctx.big_endian = spec->big_endian; + pc = snapshot->pc; + if (replay_eval_predicates (spec->find_preds, spec->find_count, &eval_ctx)) { + *hit_addr = pc; + *out_snapshot = snapshot; + return REPLAY_SEARCH_STOP_FIND; + } + if (replay_eval_predicates (spec->avoid_preds, spec->avoid_count, &eval_ctx)) { + *hit_addr = pc; + *out_snapshot = snapshot; + return REPLAY_SEARCH_STOP_AVOID; + } + if (replay_addr_list_contains (spec->frontier_stop_addrs, spec->frontier_stop_count, pc)) { + r_debug_state_snapshot_free (snapshot); + snapshot = NULL; + if (r_debug_step (core->dbg, 1) != 1) { + goto cleanup; + } + snapshot = replay_collect_snapshot (core, spec); + if (!snapshot) { + goto cleanup; + } + pc = snapshot->pc; + } + + for (i = 0; i < spec->stop_count; i++) { + if (spec->stop_addrs[i] != pc && !replay_temp_bps_add (core, &temps, spec->stop_addrs[i])) { + goto cleanup; + } + } + r_debug_state_snapshot_free (snapshot); + snapshot = NULL; + if (r_debug_continue (core->dbg) <= 0) { + goto cleanup; + } + snapshot = replay_collect_snapshot (core, spec); + if (!snapshot) { + goto cleanup; + } + eval_ctx.snapshot = snapshot; + eval_ctx.depth = depth; + eval_ctx.input_len = input_len; + eval_ctx.big_endian = spec->big_endian; + *hit_addr = snapshot->pc; + *out_snapshot = snapshot; + snapshot = NULL; + replay_temp_bps_fini (core, &temps); + return replay_classify_stop (spec, &eval_ctx); + +cleanup: + replay_temp_bps_fini (core, &temps); + r_debug_state_snapshot_free (snapshot); + return REPLAY_SEARCH_STOP_NONE; +} + +static int replay_search_node_cmp(const ReplaySearchSpec *spec, const ReplaySearchNode *na, const ReplaySearchNode *nb) { + if (na->score != nb->score) { + if (spec->score_order == REPLAY_SCORE_MAX) { + return (na->score < nb->score) - (na->score > nb->score); + } + return (na->score > nb->score) - (na->score < nb->score); + } + if (na->input_len != nb->input_len) { + return (na->input_len > nb->input_len) - (na->input_len < nb->input_len); + } + if (!na->input || !nb->input) { + return (!na->input && nb->input) ? -1 : (na->input && !nb->input); + } + return strcmp (na->input, nb->input); +} + +static void replay_search_sort_nodes(const ReplaySearchSpec *spec, ReplaySearchNode **nodes, size_t count) { + size_t i; + size_t j; + for (i = 1; i < count; i++) { + ReplaySearchNode *key = nodes[i]; + j = i; + while (j > 0 && replay_search_node_cmp (spec, nodes[j - 1], key) > 0) { + nodes[j] = nodes[j - 1]; + j--; + } + nodes[j] = key; + } +} + +static char *replay_search_run_json(RCore *core, const ReplaySearchSpec *spec) { + RList *active = NULL; + RList *next = NULL; + RList *found = NULL; + ReplaySearchNode *seed = NULL; + size_t explored = 0; + size_t depth; + char *out = NULL; + + R_RETURN_VAL_IF_FAIL (core && core->dbg && core->dbg->session && spec && spec->score_expr, NULL); + + active = r_list_newf ((RListFree)replay_search_node_free); + next = r_list_newf ((RListFree)replay_search_node_free); + found = r_list_newf ((RListFree)replay_search_match_free); + if (!active || !next || !found) { + goto cleanup; + } + seed = R_NEW0 (ReplaySearchNode); + if (!seed) { + goto cleanup; + } + seed->checkpoint_id = spec->seed_checkpoint; + seed->input = strdup (""); + if (!seed->input) { + replay_search_node_free (seed); + goto cleanup; + } + r_list_append (active, seed); + + for (depth = 0; depth < spec->max_depth && !r_list_empty (active) && r_list_empty (found); depth++) { + RListIter *iter; + ReplaySearchNode *node; + r_list_free (next); + next = r_list_newf ((RListFree)replay_search_node_free); + if (!next) { + goto cleanup; + } + r_list_foreach (active, iter, node) { + const char *alphabet = spec->alphabet; + while (alphabet && *alphabet) { + ut64 child_checkpoint = 0; + ut64 frontier_checkpoint = 0; + ut64 hit_addr = 0; + ReplaySearchStopKind stop; + RDebugStateSnapshot *snapshot = NULL; + ReplayEvalContext eval_ctx; + char *next_input = NULL; + char *snapshot_json = NULL; + st64 score = 0; + + if (!r_debug_session_restore_checkpoint (core->dbg, node->checkpoint_id)) { + alphabet++; + continue; + } + child_checkpoint = r_debug_checkpoint_create (core->dbg, node->checkpoint_id, NULL); + if (!child_checkpoint) { + alphabet++; + continue; + } + if (!r_debug_session_checkpoint_replay_append (core->dbg->session, child_checkpoint, + spec->replay_fd, (const ut8 *)alphabet, 1, NULL)) { + alphabet++; + continue; + } + if (!r_debug_session_restore_checkpoint (core->dbg, child_checkpoint)) { + alphabet++; + continue; + } + if (!r_debug_session_checkpoint_replay_apply (core->dbg, child_checkpoint, spec->replay_fd)) { + alphabet++; + continue; + } + + explored++; + stop = replay_continue_to_any (core, spec, depth + 1, node->input_len + 1, &hit_addr, &snapshot); + next_input = replay_input_append_char (node->input, node->input_len, *alphabet); + if (!next_input || !snapshot) { + free (next_input); + r_debug_state_snapshot_free (snapshot); + alphabet++; + continue; + } + eval_ctx.snapshot = snapshot; + eval_ctx.depth = depth + 1; + eval_ctx.input_len = node->input_len + 1; + eval_ctx.big_endian = spec->big_endian; + if (!replay_eval_score (spec, &eval_ctx, &score)) { + free (next_input); + r_debug_state_snapshot_free (snapshot); + alphabet++; + continue; + } + snapshot_json = r_debug_state_snapshot_to_json (snapshot); + r_debug_state_snapshot_free (snapshot); + snapshot = NULL; + + if (stop == REPLAY_SEARCH_STOP_FIND) { + ReplaySearchMatch *match = R_NEW0 (ReplaySearchMatch); + if (match) { + match->checkpoint_id = child_checkpoint; + match->input = next_input; + match->input_len = node->input_len + 1; + match->hit_addr = hit_addr; + match->score = score; + match->snapshot_json = snapshot_json; + r_list_append (found, match); + next_input = NULL; + snapshot_json = NULL; + } + } else if (stop == REPLAY_SEARCH_STOP_FRONTIER) { + ReplaySearchNode *frontier = R_NEW0 (ReplaySearchNode); + frontier_checkpoint = r_debug_checkpoint_create (core->dbg, child_checkpoint, NULL); + if (frontier && frontier_checkpoint) { + frontier->checkpoint_id = frontier_checkpoint; + frontier->input = next_input; + frontier->input_len = node->input_len + 1; + frontier->score = score; + frontier->snapshot_json = snapshot_json; + r_list_append (next, frontier); + next_input = NULL; + snapshot_json = NULL; + } else { + replay_search_node_free (frontier); + } + } + free (snapshot_json); + free (next_input); + if (!r_list_empty (found)) { + break; + } + alphabet++; + } + if (!r_list_empty (found)) { + break; + } + } + + { + int next_len = r_list_length (next); + if (spec->beam_width && next_len > 0 && (size_t)next_len > spec->beam_width) { + size_t count = (size_t)next_len; + size_t i; + ReplaySearchNode **nodes = calloc (count, sizeof (ReplaySearchNode *)); + if (!nodes) { + goto cleanup; + } + i = 0; + { + RListIter *iter; + ReplaySearchNode *node; + r_list_foreach (next, iter, node) { + nodes[i++] = node; + } + } + replay_search_sort_nodes (spec, nodes, count); + for (i = spec->beam_width; i < count; i++) { + r_list_delete_data (next, nodes[i]); + } + free (nodes); + } + } + + { + RList *tmp = active; + active = next; + next = tmp; + } + } + + { + PJ *pj = pj_new (); + RListIter *iter; + ReplaySearchMatch *match; + ReplaySearchNode *node; + if (!pj) { + goto cleanup; + } + pj_o (pj); + pj_kn (pj, "seed_checkpoint", spec->seed_checkpoint); + pj_kn (pj, "replay_fd", spec->replay_fd); + pj_ks (pj, "alphabet", spec->alphabet); + pj_kn (pj, "max_depth", spec->max_depth); + pj_kn (pj, "beam_width", spec->beam_width); + pj_ks (pj, "score_order", spec->score_order == REPLAY_SCORE_MAX? "max": "min"); + pj_kn (pj, "explored_branches", explored); + pj_kb (pj, "found", !r_list_empty (found)); + pj_ka (pj, "matches"); + r_list_foreach (found, iter, match) { + pj_o (pj); + pj_kn (pj, "checkpoint", match->checkpoint_id); + pj_ks (pj, "input", match->input ? match->input : ""); + pj_kn (pj, "hit", match->hit_addr); + pj_ki (pj, "score", match->score); + if (match->snapshot_json) { + pj_k (pj, "snapshot"); + pj_raw (pj, match->snapshot_json); + } else { + pj_knull (pj, "snapshot"); + } + pj_end (pj); + } + pj_end (pj); + pj_ka (pj, "active"); + r_list_foreach (active, iter, node) { + pj_o (pj); + pj_kn (pj, "checkpoint", node->checkpoint_id); + pj_ks (pj, "input", node->input ? node->input : ""); + pj_ki (pj, "score", node->score); + if (node->snapshot_json) { + pj_k (pj, "snapshot"); + pj_raw (pj, node->snapshot_json); + } else { + pj_knull (pj, "snapshot"); + } + pj_end (pj); + } + pj_end (pj); + pj_end (pj); + out = strdup (pj_string (pj)); + pj_free (pj); + } + +cleanup: + r_list_free (active); + r_list_free (next); + r_list_free (found); + return out; +} + +static RAnalFunction *resolve_function_target_by_name(RAnal *anal, const char *target_name) { + if (!anal || !target_name || !*target_name) { + return NULL; + } + + RAnalFunction *fcn = r_anal_get_function_byname (anal, target_name); + if (fcn) { + return fcn; + } + + char *trimmed = r_str_trim_dup (target_name); + if (!trimmed || !*trimmed) { + free (trimmed); + return NULL; + } + + char *base = trimmed; + for (;;) { + if (r_str_startswith (base, "dbg.")) { + base += 4; + continue; + } + if (r_str_startswith (base, "sym.")) { + base += 4; + continue; + } + if (r_str_startswith (base, "fcn.")) { + base += 4; + continue; + } + break; + } + + const char *plain = (*base == '_')? base + 1: base; + char *candidates[] = { + strdup (base), + *plain? strdup (plain): NULL, + r_str_newf ("sym.%s", base), + *plain? r_str_newf ("sym.%s", plain): NULL, + *plain? r_str_newf ("sym._%s", plain): NULL, + r_str_newf ("dbg.%s", base), + *plain? r_str_newf ("dbg.%s", plain): NULL, + r_str_newf ("fcn.%s", base), + *plain? r_str_newf ("fcn.%s", plain): NULL, + (*base == '_')? strdup (plain): r_str_newf ("_%s", plain), + }; + size_t i; + for (i = 0; i < R_ARRAY_SIZE (candidates); i++) { + const char *candidate = candidates[i]; + if (!candidate || !*candidate) { + continue; + } + fcn = r_anal_get_function_byname (anal, candidate); + if (fcn) { + break; + } + } + for (i = 0; i < R_ARRAY_SIZE (candidates); i++) { + free (candidates[i]); + } + free (trimmed); + return fcn; +} + +static int function_bb_count(const RAnalFunction *fcn) { + return (fcn && fcn->bbs)? r_list_length (fcn->bbs): 0; +} + +static bool function_exceeds_helper_scope_budget(const RAnalFunction *fcn) { + ut32 cost; + int bb_count; + if (!fcn) { + return true; + } + bb_count = function_bb_count (fcn); + if (bb_count > SLEIGH_SCOPE_HELPER_MAX_BLOCKS) { + return true; + } + cost = r_anal_function_cost ((RAnalFunction *)fcn); + return cost > SLEIGH_SCOPE_HELPER_MAX_COST; +} + +static bool function_exceeds_auto_callback_budget(const RAnalFunction *fcn) { + ut32 cost; + int bb_count; + int linear_size; + if (!fcn) { + return true; + } + bb_count = function_bb_count (fcn); + if (bb_count > SLEIGH_AUTO_CALLBACK_MAX_BLOCKS) { + return true; + } + linear_size = r_anal_function_linear_size ((RAnalFunction *)fcn); + if (linear_size > 0 && (ut64)linear_size > SLEIGH_AUTO_CALLBACK_MAX_LINEAR_SIZE) { + return true; + } + cost = r_anal_function_cost ((RAnalFunction *)fcn); + return cost > SLEIGH_AUTO_CALLBACK_MAX_COST; +} + +static bool is_autogenerated_function_name(const char *name) { + if (!name || !*name) { + return true; + } + return !strncmp (name, "fcn.", 4) + || !strncmp (name, "fcn_", 4) + || !strncmp (name, "sub.", 4) + || !strncmp (name, "sub_", 4) + || !strncmp (name, "loc.", 4); +} + +static bool should_skip_decompile_symbolic_scope(const RAnalFunction *fcn) { + return fcn && is_autogenerated_function_name (fcn->name) + && function_exceeds_helper_scope_budget (fcn); +} + +typedef struct { + size_t block_count; + size_t loop_count; + size_t back_edge_count; + size_t max_switch_cases; +} DecompileCFGRiskSummary; + +typedef struct { + RAnal *anal; + RAnalFunction *fcn; + DecompileCFGRiskSummary *summary; + HtUP *visited; + HtUP *in_stack; + HtUP *loop_headers; +} DecompileCFGRiskWalk; + +static void decompile_cfg_risk_visit_block(DecompileCFGRiskWalk *walk, RAnalBlock *bb); + +static void decompile_cfg_risk_visit_addr(DecompileCFGRiskWalk *walk, ut64 addr) { + RAnalBlock *succ; + bool found; + if (!walk || !walk->anal || !walk->fcn || addr == UT64_MAX) { + return; + } + succ = r_anal_function_bbget_at (walk->anal, walk->fcn, addr); + if (!succ) { + succ = r_anal_function_bbget_in (walk->anal, walk->fcn, addr); + } + if (!succ) { + return; + } + ht_up_find (walk->in_stack, succ->addr, &found); + if (found) { + bool seen_header; + walk->summary->back_edge_count++; + ht_up_find (walk->loop_headers, succ->addr, &seen_header); + if (!seen_header) { + ht_up_insert (walk->loop_headers, succ->addr, (void *)1); + walk->summary->loop_count++; + } + return; + } + ht_up_find (walk->visited, succ->addr, &found); + if (!found) { + decompile_cfg_risk_visit_block (walk, succ); + } +} + +static void decompile_cfg_risk_visit_block(DecompileCFGRiskWalk *walk, RAnalBlock *bb) { + size_t case_count = 0; + bool found; + RListIter *iter; + RAnalCaseOp *caseop; + if (!walk || !bb) { + return; + } + ht_up_find (walk->visited, bb->addr, &found); + if (found) { + return; + } + ht_up_insert (walk->visited, bb->addr, (void *)1); + ht_up_insert (walk->in_stack, bb->addr, (void *)1); + + if (bb->switch_op) { + if (bb->switch_op->amount > 0) { + case_count = (size_t)bb->switch_op->amount; + } + if (bb->switch_op->cases) { + size_t listed_cases = (size_t)r_list_length (bb->switch_op->cases); + if (listed_cases > case_count) { + case_count = listed_cases; + } + } + if (case_count > walk->summary->max_switch_cases) { + walk->summary->max_switch_cases = case_count; + } + } + + decompile_cfg_risk_visit_addr (walk, bb->jump); + decompile_cfg_risk_visit_addr (walk, bb->fail); + if (bb->switch_op && bb->switch_op->cases) { + r_list_foreach (bb->switch_op->cases, iter, caseop) { + if (!caseop) { + continue; + } + decompile_cfg_risk_visit_addr (walk, caseop->jump); + } + } + + ht_up_delete (walk->in_stack, bb->addr); +} + +static bool compute_decompile_cfg_risk_summary(RAnal *anal, RAnalFunction *fcn, DecompileCFGRiskSummary *out) { + DecompileCFGRiskWalk walk; + RAnalBlock *entry; + if (!anal || !fcn || !out) { + return false; + } + memset (out, 0, sizeof (*out)); + out->block_count = (size_t)function_bb_count (fcn); + walk.anal = anal; + walk.fcn = fcn; + walk.summary = out; + walk.visited = ht_up_new0 (); + walk.in_stack = ht_up_new0 (); + walk.loop_headers = ht_up_new0 (); + if (!walk.visited || !walk.in_stack || !walk.loop_headers) { + ht_up_free (walk.visited); + ht_up_free (walk.in_stack); + ht_up_free (walk.loop_headers); + return false; + } + entry = r_anal_function_bbget_in (anal, fcn, fcn->addr); + if (!entry) { + entry = r_anal_function_bbget_at (anal, fcn, fcn->addr); + } + if (entry) { + decompile_cfg_risk_visit_block (&walk, entry); + } + ht_up_free (walk.visited); + ht_up_free (walk.in_stack); + ht_up_free (walk.loop_headers); + return true; +} + +static size_t decompiler_max_blocks_preflight(void) { + const char *raw = getenv ("SLEIGH_DEC_MAX_BLOCKS"); + char *endptr = NULL; + unsigned long long parsed; + if (!raw || !*raw) { + return 200; + } + errno = 0; + parsed = strtoull (raw, &endptr, 10); + if (errno != 0 || endptr == raw || parsed == 0) { + return 200; + } + if (parsed > (unsigned long long)SIZE_MAX) { + return SIZE_MAX; + } + return (size_t)parsed; +} + +static RAnalFunction *materialize_function_at(RAnal *anal, ut64 addr) { + RAnalFunction *fcn; + int ret; + RCore *core; + + if (!anal || addr == UT64_MAX) { + return NULL; + } + + fcn = r_anal_get_fcn_in (anal, addr, R_ANAL_FCN_TYPE_ANY); + if (fcn) { + return fcn; + } + + core = anal->coreb.core; + if (core) { + if (r_core_anal_fcn (core, addr, UT64_MAX, R_ANAL_REF_TYPE_NULL, 1)) { + fcn = r_anal_get_fcn_in (anal, addr, R_ANAL_FCN_TYPE_ANY); + if (fcn) { + return fcn; + } + } + } + + fcn = r_anal_create_function (anal, NULL, addr, R_ANAL_FCN_TYPE_FCN, NULL); + if (!fcn) { + return r_anal_get_fcn_in (anal, addr, R_ANAL_FCN_TYPE_ANY); + } + + ret = r_anal_function (anal, fcn, addr, R_ANAL_REF_TYPE_NULL); + if ((ret < 0 && ret != R_ANAL_RET_END) || function_bb_count (fcn) <= 0) { + if (!r_anal_function_delete (anal, fcn)) { + r_anal_function_free (fcn); + } + return NULL; + } + + return r_anal_get_fcn_in (anal, addr, R_ANAL_FCN_TYPE_ANY); +} + +static ut64 resolve_local_direct_jump_thunk_target(RAnal *anal, ut64 addr) { + ut64 cur = addr; + size_t depth = 0; + + while (anal && cur != UT64_MAX && cur && depth++ < 4) { + ut8 buf[16] = {0}; + RAnalOp op = {0}; + int size; + + if (!anal->iob.read_at || anal->iob.read_at (anal->iob.io, cur, buf, sizeof (buf)) <= 0) { + break; + } + size = r_anal_op (anal, &op, cur, buf, sizeof (buf), R_ARCH_OP_MASK_BASIC); + if (size <= 0) { + r_anal_op_fini (&op); + break; + } + if ((op.type != R_ANAL_OP_TYPE_JMP && op.type != R_ANAL_OP_TYPE_UJMP) + || op.jump == UT64_MAX || !op.jump || op.jump == cur || op.size > 8) { + r_anal_op_fini (&op); + break; + } + cur = op.jump; + r_anal_op_fini (&op); + } + + return cur; +} + +static const char *detect_local_summary_alias(RAnal *anal, ut64 addr) { + ut64 resolved; + ut8 buf[16] = {0}; + + if (!anal || addr == UT64_MAX || !addr || !anal->iob.read_at) { + return NULL; + } + + resolved = resolve_local_direct_jump_thunk_target (anal, addr); + if (resolved == UT64_MAX || !resolved) { + return NULL; + } + if (!anal->iob.read_at (anal->iob.io, resolved, buf, sizeof (buf))) { + return NULL; + } + + /* MSVC-style x64 small memcpy/memmove dispatcher: + * mov rax, rcx + * lea r10, [rip + table] + * cmp r8, imm8 + */ + if (buf[0] == 0x48 && buf[1] == 0x8b && buf[2] == 0xc1 + && buf[3] == 0x4c && buf[4] == 0x8d && buf[5] == 0x15 + && buf[10] == 0x49 && buf[11] == 0x83 && buf[12] == 0xf8) { + return "memcpy"; + } + + return NULL; +} + +static RAnalFunction *resolve_or_materialize_function_target(RCore *core, RAnal *anal, const char *target_arg) { + ut64 target_addr = 0; + RAnalFunction *fcn; + + if (!core || !anal || !target_arg || !*target_arg) { + return NULL; + } + + fcn = resolve_function_target_by_name (anal, target_arg); + if (fcn) { + return fcn; + } + + if (!parse_sym_target_expr (core, target_arg, &target_addr)) { + return NULL; + } + return materialize_function_at (anal, target_addr); +} + +static RAnalFunction *resolve_or_materialize_current_function(RCore *core, RAnal *anal) { + if (!core || !anal) { + return NULL; + } + return materialize_function_at (anal, core->addr); +} + +static char *build_sym_symbol_map_json(RCore *core, RAnal *anal, R2ILContext *ctx, const SymFunctionScope *scope) { + if (!core || !anal || !ctx || !scope || !scope->functions || !scope->count) { + return strdup ("{}"); + } + + PJ *pj = pj_new (); + if (!pj) { + return strdup ("{}"); + } + pj_o (pj); + + for (size_t i = 0; i < scope->count; i++) { + const R2ILFunctionBlocks *function = &scope->functions[i]; + const BlockArray *blocks = &scope->owned_blocks[i]; + size_t direct_target_count = 0; + ut64 *direct_targets = NULL; + if (function->name && *function->name) { + char key[32]; + snprintf (key, sizeof (key), "0x%llx", (unsigned long long)function->entry_addr); + pj_ks (pj, key, function->name); + } + direct_targets = collect_type_interproc_direct_targets_from_blocks ( + ctx, + blocks, + function->entry_addr, + function->name, + &direct_target_count + ); + for (size_t j = 0; j < direct_target_count; j++) { + char *target_name = resolve_interproc_seed_name (core, anal, direct_targets[j]); + const char *summary_alias = NULL; + if (target_name && *target_name) { + char key[32]; + snprintf (key, sizeof (key), "0x%llx", (unsigned long long)direct_targets[j]); + pj_ks (pj, key, target_name); + } else if ((summary_alias = detect_local_summary_alias (anal, direct_targets[j]))) { + char key[32]; + snprintf (key, sizeof (key), "0x%llx", (unsigned long long)direct_targets[j]); + pj_ks (pj, key, summary_alias); + } + free (target_name); + } + free (direct_targets); + } + + pj_end (pj); + return pj_drain (pj); +} + +static bool ssa_var_to_reg_name(const char *ssa_name, char *out, size_t out_size) { + if (!ssa_name || !out || out_size == 0) { + return false; + } + + const char *suffix = strrchr (ssa_name, '_'); + size_t len = suffix ? (size_t)(suffix - ssa_name) : strlen (ssa_name); + if (len == 0 || len >= out_size) { + return false; + } + + char base[128]; + if (len >= sizeof (base)) { + return false; } memcpy (base, ssa_name, len); base[len] = '\0'; @@ -3533,8 +6091,62 @@ static R2ILBlock *lift_function_block_healed( return NULL; } +static void lift_function_linear_gap_blocks(RAnal *anal, RAnalFunction *fcn, R2ILContext *ctx, BlockArray *out) { + ut64 min_addr; + ut64 max_addr; + ut64 addr; + + R_RETURN_IF_FAIL (anal && fcn && ctx && out); + + min_addr = r_anal_function_min_addr (fcn); + max_addr = r_anal_function_max_addr (fcn); + if (min_addr == UT64_MAX || max_addr == UT64_MAX || max_addr <= min_addr) { + return; + } + + for (addr = min_addr; addr < max_addr;) { + ut64 covered_end = UT64_MAX; + ut8 buf[SLEIGH_MIN_BYTES] = {0}; + RAnalOp op = {0}; + int len; + R2ILBlock *block; + + if (block_array_addr_covered (out, addr, &covered_end)) { + addr = covered_end > addr? covered_end: addr + 1; + continue; + } + if (!anal->iob.read_at (anal->iob.io, addr, buf, sizeof (buf))) { + addr++; + continue; + } + + len = r_anal_op (anal, &op, addr, buf, sizeof (buf), R_ARCH_OP_MASK_BASIC); + r_anal_op_fini (&op); + if (len < 1) { + len = 1; + } + + block = r2il_lift (ctx, buf, sizeof (buf), addr); + if (block) { + if (r2il_block_validate (ctx, block)) { + block_array_push (out, block); + } else { + r2il_block_free (block); + } + } + + addr += (ut64)len; + } +} + /* Lift all basic blocks of a function */ -static bool lift_function_blocks(RAnal *anal, RAnalFunction *fcn, R2ILContext *ctx, BlockArray *out) { +static bool lift_function_blocks( + RAnal *anal, + RAnalFunction *fcn, + R2ILContext *ctx, + BlockArray *out, + bool include_linear_gap_blocks +) { R_RETURN_VAL_IF_FAIL (anal && fcn && ctx && out, false); RListIter *iter; @@ -3636,6 +6248,11 @@ static bool lift_function_blocks(RAnal *anal, RAnalFunction *fcn, R2ILContext *c free (buf); } + if (include_linear_gap_blocks) { + lift_function_linear_gap_blocks (anal, fcn, ctx, out); + block_array_sort (out); + } + return out->count > 0; } @@ -3675,6 +6292,10 @@ static bool sleigh_mode_is_fast(RAnal *anal) { return cfg_get_mode_default_balanced (anal) == SLEIGH_MODE_FAST; } +static bool sleigh_mode_allows_deep_auto_callbacks(RAnal *anal) { + return cfg_get_mode_default_balanced (anal) == SLEIGH_MODE_FULL; +} + static SleighMode sleigh_mode_effective_for_post_analysis(RAnal *anal) { SleighMode mode = cfg_get_mode_default_balanced (anal); return mode == SLEIGH_MODE_FAST ? SLEIGH_MODE_FAST : SLEIGH_MODE_FULL; @@ -3872,10 +6493,30 @@ R2ILContext *get_context(RAnal *anal) { sleigh_arch_str = "riscv32"; } else if (!strcmp (arch, "riscv64") || !strcmp (arch, "rv64")) { sleigh_arch_str = "riscv64"; - } else if (!strcmp (arch, "mips")) { - /* Simple heuristic for MIPS (assuming default is 32be/le) */ - /* Note: This is partial, better use manual override for complex variants */ - sleigh_arch_str = "mips"; /* Placeholder - mapped often to general mips */ + } else if (!strcmp (arch, "mips") || !strcmp (arch, "mips32") + || !strcmp (arch, "mips32be") || !strcmp (arch, "mipsbe") + || !strcmp (arch, "mipseb") || !strcmp (arch, "mipsel") + || !strcmp (arch, "mips32le") || !strcmp (arch, "mips32el") + || !strcmp (arch, "mips64") || !strcmp (arch, "mips64be") + || !strcmp (arch, "mips64le") || !strcmp (arch, "mips64el")) { + bool is64 = bits >= 64 + || !strcmp (arch, "mips64") + || !strcmp (arch, "mips64be") + || !strcmp (arch, "mips64le") + || !strcmp (arch, "mips64el"); + bool big_endian = R_ARCH_CONFIG_IS_BIG_ENDIAN (anal->config); + if (!strcmp (arch, "mipsel") || !strcmp (arch, "mips32le") + || !strcmp (arch, "mips32el") + || !strcmp (arch, "mips64le") + || !strcmp (arch, "mips64el")) { + big_endian = false; + } else if (!strcmp (arch, "mips32be") || !strcmp (arch, "mipsbe") + || !strcmp (arch, "mipseb") || !strcmp (arch, "mips64be")) { + big_endian = true; + } + sleigh_arch_str = is64 + ? (big_endian ? "mips64be" : "mips64le") + : (big_endian ? "mips32be" : "mips32le"); } else { return NULL; /* unsupported arch */ } @@ -4052,93 +6693,387 @@ static void append_pszj_string_to_pj(RCore *core, PJ *pj, ut64 addr) { snprintf (addr_str, sizeof (addr_str), "0x%llx", (unsigned long long)addr); pj_ks (pj, addr_str, str->str_value); } - r_json_free (root); + r_json_free (root); + } + + free (pszj); +} + +static void extend_string_map_with_function_ptr_strings(RCore *core, RAnalFunction *fcn, PJ *pj) { + if (!core || !fcn || !pj) { + return; + } + + char *pdfj = r_core_cmd_strf (core, "pdfj @ 0x%"PFMT64x, fcn->addr); + if (!pdfj || pdfj[0] != '{') { + free (pdfj); + return; + } + + RJson *root = r_json_parse (pdfj); + if (root && root->type == R_JSON_OBJECT) { + const RJson *ops = r_json_get (root, "ops"); + if (ops && ops->type == R_JSON_ARRAY) { + RJson *elem; + for (elem = ops->children.first; elem; elem = elem->next) { + if (elem->type != R_JSON_OBJECT) { + continue; + } + const RJson *ptr = r_json_get (elem, "ptr"); + if (ptr && ptr->type == R_JSON_INTEGER && ptr->num.u_value) { + append_pszj_string_to_pj (core, pj, (ut64)ptr->num.u_value); + } + } + } + r_json_free (root); + } + + free (pdfj); +} + +static char *sleigh_cmd(RAnal *anal, const char *cmd) { + bool is_sla_ns = r_str_startswith (cmd, "sla"); + bool is_sym_ns = r_str_startswith (cmd, "sym"); + if (!is_sla_ns && !is_sym_ns) { + return NULL; + } + + RCore *core = anal->coreb.core; + RCons *cons = core ? core->cons : NULL; + + if (cmd[3] == '?') { + if (cons) { + r_cons_println (cons, "| a:sla - Show r2sleigh status"); + r_cons_println (cons, "| a:sla.info - Show current architecture info"); + r_cons_println (cons, "| a:sla.arch [name] - Get/Set Sleigh architecture manually"); + r_cons_println (cons, "| a:sla.json - Dump r2il ops as JSON for current instruction"); + r_cons_println (cons, "| a:sla.regs - Show registers read/written by instruction"); + r_cons_println (cons, "| a:sla.opvals - Show analysis srcs/dsts for current instruction"); + r_cons_println (cons, "| a:sla.mem - Show memory accesses by instruction"); + r_cons_println (cons, "| a:sla.vars - Show all varnodes used by instruction"); + r_cons_println (cons, "| a:sla.ssa - Show SSA form of instruction"); + r_cons_println (cons, "| a:sla.defuse - Show def-use analysis of instruction"); + r_cons_println (cons, "| a:sla.types [name|addr] - Dump inferred type write-back payload (current by default)"); + r_cons_println (cons, "| a:sla.facts [name|addr] - Dump combined typed/semantic fact payload (current by default)"); + r_cons_println (cons, "| a:sla.plan [name|addr] - Dump canonical analysis/decompile plan payload (current by default)"); + r_cons_println (cons, "| a:sla.ssa.func - Show function SSA with phi nodes"); + r_cons_println (cons, "| a:sla.ssa.func.opt - Show optimized function SSA"); + r_cons_println (cons, "| a:sla.defuse.func - Show function-wide def-use analysis"); + r_cons_println (cons, "| a:sla.dom - Show dominator tree for current function"); + r_cons_println (cons, "| a:sla.slice - Backward slice from variable (e.g. rax_3)"); + r_cons_println (cons, "| a:sla.sym [name|addr] - Symbolic execution summary (current by default)"); + r_cons_println (cons, "| a:sla.sym.paths [name|addr] - Explore paths in function (current by default)"); + r_cons_println (cons, "| a:sla.assumptions [name|addr] - Show persisted function assumptions (current by default)"); + r_cons_println (cons, "| a:sla.assumptions- [name|addr] - Clear persisted function assumptions"); + r_cons_println (cons, "| a:sla.assumej - Replace current function assumptions"); + r_cons_println (cons, "| a:sla.sym.merge [on|off] - Toggle symbolic state merging"); + r_cons_println (cons, "| a:sla.taint - Taint analysis for current function"); + r_cons_println (cons, "| a:sla.dec [name|addr] - Decompile function (current by default)"); + r_cons_println (cons, "| a:sla.cfg - Show ASCII CFG for current function"); + r_cons_println (cons, "| a:sla.cfg.json - Show CFG as JSON for current function"); + r_cons_println (cons, "| a:sym.explore - Explore symbolic paths reaching target"); + r_cons_println (cons, "| a:sym.solve - Solve concrete input for target reachability"); + r_cons_println (cons, "| a:sym.explore.replayj - Explore from a replay checkpoint frontier"); + r_cons_println (cons, "| a:sym.solve.replayj - Solve from a replay checkpoint frontier"); + r_cons_println (cons, "| a:sym.explore.state [json-spec] - Explore from current debugger state"); + r_cons_println (cons, "| a:sym.solve.state [json-spec] - Solve from current debugger state"); + r_cons_println (cons, "| a:sym.runj - Run typed symbolic exploration spec"); + r_cons_println (cons, "| a:sym.replayj - Search checkpointed replay branches"); + r_cons_println (cons, "| a:sym.state - Show last symbolic explore/solve cached result"); + } + return strdup(""); + } + + if (is_sym_ns && !strcmp (cmd, "sym.state")) { + char *state_json = sym_state_cache_to_json (); + if (cons && state_json) { + r_cons_printf (cons, "%s\n", state_json); + } + free (state_json); + return strdup(""); + } + + if (is_sym_ns && !strncmp (cmd, "sym.runj", 8)) { + const char *arg = skip_cmd_spaces (cmd + 8); + R2ILContext *ctx; + RAnalFunction *fcn; + SymFunctionScope scope; + char *spec_json = NULL; + char *external_context_json = NULL; + char *result = NULL; + bool rust_owned = true; + + if (!arg || !*arg) { + if (cons) { + r_cons_println (cons, "Usage: a:sym.runj "); + } + return strdup(""); + } + + ctx = get_context (anal); + if (!ctx) { + R_LOG_ERROR ("r2sleigh: no context"); + return strdup(""); + } + fcn = r_anal_get_fcn_in (anal, core->addr, R_ANAL_FCN_TYPE_ANY); + if (!fcn) { + R_LOG_ERROR ("r2sleigh: no function at current address"); + return strdup(""); + } + if (!build_symbolic_function_scope (anal, fcn, ctx, &scope)) { + R_LOG_ERROR ("r2sleigh: failed to build symbolic function scope"); + return strdup(""); + } + char *sym_map_json = build_sym_symbol_map_json (core, anal, ctx, &scope); + if (sym_map_json) { + r2sym_set_symbol_map_json (sym_map_json); + free (sym_map_json); + } + external_context_json = sleigh_collect_sym_external_context_json (anal, fcn); + + spec_json = strdup (arg); + if (!spec_json) { + free (external_context_json); + sym_function_scope_free (&scope); + return strdup(""); + } + r_str_unescape (spec_json); + result = r2sym_run_spec_json_scope (ctx, scope.functions, scope.count, fcn->addr, spec_json, external_context_json); + free (spec_json); + free (external_context_json); + if (!result) { + rust_owned = false; + result = strdup ("{\"error\":\"symbolic execution failed\"}"); + } + + if (cons && result) { + r_cons_printf (cons, "%s\n", result); + } + if (result && !sym_result_has_error (result)) { + sym_state_cache_update ("runj", fcn->addr, fcn->addr, 0, result); + } + if (rust_owned) { + r2il_string_free (result); + } else { + free (result); + } + sym_function_scope_free (&scope); + return strdup(""); + } + + if (is_sym_ns && !strncmp (cmd, "sym.replayj", 11)) { + const char *arg = skip_cmd_spaces (cmd + 11); + ReplaySearchSpec spec; + char *result = NULL; + + if (!arg || !*arg) { + if (cons) { + r_cons_println (cons, "Usage: a:sym.replayj "); + } + return strdup (""); + } + if (!core->dbg || !core->dbg->session) { + R_LOG_ERROR ("r2sleigh: debug session with checkpoints is required"); + return strdup (""); + } + R_LOG_DEBUG ("r2sleigh replayj arg: %s", arg); + if (!replay_search_spec_parse (core, arg, &spec)) { + R_LOG_ERROR ("r2sleigh: invalid replay search spec"); + return strdup (""); + } + result = replay_search_run_json (core, &spec); + replay_search_spec_fini (&spec); + if (!result) { + result = strdup ("{\"error\":\"replay search failed\"}"); + } + if (cons && result) { + r_cons_printf (cons, "%s\n", result); + } + if (result && !sym_result_has_error (result)) { + sym_state_cache_update ("replayj", 0, 0, 0, result); + } + free (result); + return strdup (""); } - free (pszj); -} + if (is_sym_ns && (!strncmp (cmd, "sym.explore.replayj", 19) || !strncmp (cmd, "sym.solve.replayj", 17))) { + bool is_explore = r_str_startswith (cmd, "sym.explore.replayj"); + size_t prefix_len = is_explore ? 19 : 17; + const char *arg = skip_cmd_spaces (cmd + prefix_len); + ReplaySymSeedSpec spec; + ut64 target = 0; + R2ILContext *ctx; + RAnalFunction *fcn; + SymFunctionScope scope; + char *spec_json = NULL; + char *external_context_json = NULL; + char *result = NULL; + bool rust_owned = true; -static void extend_string_map_with_function_ptr_strings(RCore *core, RAnalFunction *fcn, PJ *pj) { - if (!core || !fcn || !pj) { - return; - } + if (!arg || !*arg) { + if (cons) { + r_cons_println (cons, is_explore + ? "Usage: a:sym.explore.replayj " + : "Usage: a:sym.solve.replayj "); + } + return strdup (""); + } + if (!core->dbg || !core->dbg->session) { + R_LOG_ERROR ("r2sleigh: debug session with checkpoints is required"); + return strdup (""); + } + if (!parse_replay_target_and_json (core, arg, &target, &spec_json)) { + R_LOG_ERROR ("r2sleigh: invalid replay symbolic target/spec"); + if (cons) { + r_cons_println (cons, is_explore + ? "Usage: a:sym.explore.replayj " + : "Usage: a:sym.solve.replayj "); + } + return strdup (""); + } + if (!replay_sym_seed_spec_parse (core, spec_json, &spec, true)) { + R_LOG_ERROR ("r2sleigh: invalid replay symbolic seed spec"); + free (spec_json); + return strdup (""); + } + free (spec_json); - char *pdfj = r_core_cmd_strf (core, "pdfj @ 0x%"PFMT64x, fcn->addr); - if (!pdfj || pdfj[0] != '{') { - free (pdfj); - return; - } + ctx = get_context (anal); + if (!ctx) { + R_LOG_ERROR ("r2sleigh: no context"); + replay_sym_seed_spec_fini (&spec); + return strdup (""); + } + fcn = resolve_or_materialize_current_function (core, anal); + if (!fcn) { + R_LOG_ERROR ("r2sleigh: no function at current address"); + replay_sym_seed_spec_fini (&spec); + return strdup (""); + } + if (!build_symbolic_function_scope_with_target (anal, fcn, ctx, &scope, target)) { + R_LOG_ERROR ("r2sleigh: failed to build symbolic function scope"); + replay_sym_seed_spec_fini (&spec); + return strdup (""); + } + char *sym_map_json = build_sym_symbol_map_json (core, anal, ctx, &scope); + if (sym_map_json) { + r2sym_set_symbol_map_json (sym_map_json); + free (sym_map_json); + } + external_context_json = sleigh_collect_sym_external_context_json (anal, fcn); - RJson *root = r_json_parse (pdfj); - if (root && root->type == R_JSON_OBJECT) { - const RJson *ops = r_json_get (root, "ops"); - if (ops && ops->type == R_JSON_ARRAY) { - RJson *elem; - for (elem = ops->children.first; elem; elem = elem->next) { - if (elem->type != R_JSON_OBJECT) { - continue; - } - const RJson *ptr = r_json_get (elem, "ptr"); - if (ptr && ptr->type == R_JSON_INTEGER && ptr->num.u_value) { - append_pszj_string_to_pj (core, pj, (ut64)ptr->num.u_value); - } - } + result = replay_sym_query_run (core, ctx, &scope, fcn->addr, target, &spec, is_explore, external_context_json); + free (external_context_json); + if (!result) { + rust_owned = false; + result = strdup ("{\"error\":\"replay symbolic execution failed\"}"); } - r_json_free (root); + + if (cons && result) { + r_cons_printf (cons, "%s\n", result); + } + if (result && !sym_result_has_error (result)) { + sym_state_cache_update (is_explore ? "explore.replayj" : "solve.replayj", + fcn->addr, spec.entry_addr? spec.entry_addr: fcn->addr, target, result); + } + if (rust_owned) { + r2il_string_free (result); + } else { + free (result); + } + sym_function_scope_free (&scope); + replay_sym_seed_spec_fini (&spec); + return strdup (""); } - free (pdfj); -} + if (is_sym_ns && (!strncmp (cmd, "sym.explore.state", 17) || !strncmp (cmd, "sym.solve.state", 15))) { + bool is_explore = r_str_startswith (cmd, "sym.explore.state"); + size_t prefix_len = is_explore ? 17 : 15; + const char *arg = skip_cmd_spaces (cmd + prefix_len); + ReplaySymSeedSpec spec; + ut64 target = 0; + R2ILContext *ctx; + RAnalFunction *fcn; + SymFunctionScope scope; + char *spec_json = NULL; + char *external_context_json = NULL; + char *result = NULL; + bool rust_owned = true; -static char *sleigh_cmd(RAnal *anal, const char *cmd) { - bool is_sla_ns = r_str_startswith (cmd, "sla"); - bool is_sym_ns = r_str_startswith (cmd, "sym"); - if (!is_sla_ns && !is_sym_ns) { - return NULL; - } + if (!arg || !*arg) { + if (cons) { + r_cons_println (cons, is_explore + ? "Usage: a:sym.explore.state [json-spec]" + : "Usage: a:sym.solve.state [json-spec]"); + } + return strdup (""); + } + if (!core->dbg) { + R_LOG_ERROR ("r2sleigh: active debugger state is required"); + return strdup (""); + } + if (!parse_target_and_optional_json (core, arg, &target, &spec_json)) { + R_LOG_ERROR ("r2sleigh: invalid state symbolic target/spec"); + if (cons) { + r_cons_println (cons, is_explore + ? "Usage: a:sym.explore.state [json-spec]" + : "Usage: a:sym.solve.state [json-spec]"); + } + return strdup (""); + } + if (!replay_sym_seed_spec_parse (core, spec_json, &spec, false)) { + R_LOG_ERROR ("r2sleigh: invalid state symbolic seed spec"); + free (spec_json); + return strdup (""); + } + free (spec_json); - RCore *core = anal->coreb.core; - RCons *cons = core ? core->cons : NULL; + ctx = get_context (anal); + if (!ctx) { + R_LOG_ERROR ("r2sleigh: no context"); + replay_sym_seed_spec_fini (&spec); + return strdup (""); + } + fcn = resolve_or_materialize_current_function (core, anal); + if (!fcn) { + R_LOG_ERROR ("r2sleigh: no function at current address"); + replay_sym_seed_spec_fini (&spec); + return strdup (""); + } + if (!build_symbolic_function_scope_with_target (anal, fcn, ctx, &scope, target)) { + R_LOG_ERROR ("r2sleigh: failed to build symbolic function scope"); + replay_sym_seed_spec_fini (&spec); + return strdup (""); + } + char *sym_map_json = build_sym_symbol_map_json (core, anal, ctx, &scope); + if (sym_map_json) { + r2sym_set_symbol_map_json (sym_map_json); + free (sym_map_json); + } + external_context_json = sleigh_collect_sym_external_context_json (anal, fcn); - if (cmd[3] == '?') { - if (cons) { - r_cons_println (cons, "| a:sla - Show r2sleigh status"); - r_cons_println (cons, "| a:sla.info - Show current architecture info"); - r_cons_println (cons, "| a:sla.arch [name] - Get/Set Sleigh architecture manually"); - r_cons_println (cons, "| a:sla.json - Dump r2il ops as JSON for current instruction"); - r_cons_println (cons, "| a:sla.regs - Show registers read/written by instruction"); - r_cons_println (cons, "| a:sla.opvals - Show analysis srcs/dsts for current instruction"); - r_cons_println (cons, "| a:sla.mem - Show memory accesses by instruction"); - r_cons_println (cons, "| a:sla.vars - Show all varnodes used by instruction"); - r_cons_println (cons, "| a:sla.ssa - Show SSA form of instruction"); - r_cons_println (cons, "| a:sla.defuse - Show def-use analysis of instruction"); - r_cons_println (cons, "| a:sla.types - Dump inferred type write-back payload for current function"); - r_cons_println (cons, "| a:sla.ssa.func - Show function SSA with phi nodes"); - r_cons_println (cons, "| a:sla.ssa.func.opt - Show optimized function SSA"); - r_cons_println (cons, "| a:sla.defuse.func - Show function-wide def-use analysis"); - r_cons_println (cons, "| a:sla.dom - Show dominator tree for current function"); - r_cons_println (cons, "| a:sla.slice - Backward slice from variable (e.g. rax_3)"); - r_cons_println (cons, "| a:sla.sym - Symbolic execution summary for current function"); - r_cons_println (cons, "| a:sla.sym.paths - Explore paths in current function"); - r_cons_println (cons, "| a:sla.sym.merge [on|off] - Toggle symbolic state merging"); - r_cons_println (cons, "| a:sla.taint - Taint analysis for current function"); - r_cons_println (cons, "| a:sla.dec [name|addr] - Decompile function (current by default)"); - r_cons_println (cons, "| a:sla.cfg - Show ASCII CFG for current function"); - r_cons_println (cons, "| a:sla.cfg.json - Show CFG as JSON for current function"); - r_cons_println (cons, "| a:sym.explore - Explore symbolic paths reaching target"); - r_cons_println (cons, "| a:sym.solve - Solve concrete input for target reachability"); - r_cons_println (cons, "| a:sym.state - Show last symbolic explore/solve cached result"); + result = replay_sym_query_run (core, ctx, &scope, fcn->addr, target, &spec, is_explore, external_context_json); + free (external_context_json); + if (!result) { + rust_owned = false; + result = strdup ("{\"error\":\"state symbolic execution failed\"}"); } - return strdup(""); - } - if (is_sym_ns && !strcmp (cmd, "sym.state")) { - char *state_json = sym_state_cache_to_json (); - if (cons && state_json) { - r_cons_printf (cons, "%s\n", state_json); + if (cons && result) { + r_cons_printf (cons, "%s\n", result); } - free (state_json); - return strdup(""); + if (result && !sym_result_has_error (result)) { + sym_state_cache_update (is_explore ? "explore.state" : "solve.state", + fcn->addr, spec.entry_addr? spec.entry_addr: core->addr, target, result); + } + if (rust_owned) { + r2il_string_free (result); + } else { + free (result); + } + sym_function_scope_free (&scope); + replay_sym_seed_spec_fini (&spec); + return strdup (""); } if (is_sym_ns && (!strncmp (cmd, "sym.explore", 11) || !strncmp (cmd, "sym.solve", 9))) { @@ -4148,8 +7083,9 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { ut64 target = 0; R2ILContext *ctx; RAnalFunction *fcn; - BlockArray blocks; + SymFunctionScope scope; char *result = NULL; + char *external_context_json = NULL; bool rust_owned = true; if (!arg || !*arg) { @@ -4175,26 +7111,35 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { R_LOG_ERROR ("r2sleigh: no context"); return strdup(""); } - fcn = r_anal_get_fcn_in (anal, core->addr, R_ANAL_FCN_TYPE_ANY); + fcn = resolve_or_materialize_current_function (core, anal); if (!fcn) { R_LOG_ERROR ("r2sleigh: no function at current address"); return strdup(""); } - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { - R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); + if (!build_symbolic_function_scope_with_target (anal, fcn, ctx, &scope, target)) { + R_LOG_ERROR ("r2sleigh: failed to build symbolic function scope"); return strdup(""); } - char *sym_map_json = build_sym_symbol_map_json (core); + sleigh_debug_scope_log ("sym_query_stage scope_ready count=%zu", scope.count); + sleigh_debug_scope_log ("sym_query_stage sym_map_begin"); + char *sym_map_json = build_sym_symbol_map_json (core, anal, ctx, &scope); if (sym_map_json) { r2sym_set_symbol_map_json (sym_map_json); free (sym_map_json); } + sleigh_debug_scope_log ("sym_query_stage sym_map_done"); + sleigh_debug_scope_log ("sym_query_stage assumptions_begin"); + external_context_json = sleigh_collect_sym_external_context_json (anal, fcn); + sleigh_debug_scope_log ("sym_query_stage assumptions_done"); + sleigh_debug_scope_log ("sym_query_stage rust_call_begin target=0x%"PFMT64x, target); if (is_explore) { - result = r2sym_explore_to (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, target); + result = r2sym_explore_to_scope (ctx, scope.functions, scope.count, fcn->addr, target, external_context_json); } else { - result = r2sym_solve_to (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, target); + result = r2sym_solve_to_scope (ctx, scope.functions, scope.count, fcn->addr, target, external_context_json); } + sleigh_debug_scope_log ("sym_query_stage rust_call_done"); + free (external_context_json); if (!result) { rust_owned = false; result = strdup ("{\"error\":\"symbolic execution failed\"}"); @@ -4212,10 +7157,90 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { } else { free (result); } - block_array_free (&blocks); + sym_function_scope_free (&scope); return strdup(""); } + if (!strcmp (cmd, "sla.assumptions-") || (!strncmp (cmd, "sla.assumptions-", 16) && isspace ((unsigned char)cmd[16]))) { + const char *target_arg = skip_cmd_spaces (cmd + 16); + RAnalFunction *fcn = (target_arg && *target_arg) + ? resolve_or_materialize_function_target (core, anal, target_arg) + : resolve_or_materialize_current_function (core, anal); + if (!fcn) { + if (target_arg && *target_arg) { + R_LOG_ERROR ("r2sleigh: function target not found: %s", target_arg); + } else { + R_LOG_ERROR ("r2sleigh: no function at current address"); + } + return strdup (""); + } + if (!r_anal_function_set_assumptions_json (anal, fcn, "")) { + R_LOG_ERROR ("r2sleigh: failed to clear assumptions for 0x%"PFMT64x, fcn->addr); + return strdup (""); + } + if (cons) { + r_cons_println (cons, "[]"); + } + return strdup (""); + } + + if (!strncmp (cmd, "sla.assumptions", 15) && (!cmd[15] || isspace ((unsigned char)cmd[15]))) { + const char *target_arg = skip_cmd_spaces (cmd + 15); + RAnalFunction *fcn = (target_arg && *target_arg) + ? resolve_or_materialize_function_target (core, anal, target_arg) + : resolve_or_materialize_current_function (core, anal); + char *assumptions_json; + if (!fcn) { + if (target_arg && *target_arg) { + R_LOG_ERROR ("r2sleigh: function target not found: %s", target_arg); + } else { + R_LOG_ERROR ("r2sleigh: no function at current address"); + } + return strdup (""); + } + assumptions_json = sleigh_collect_function_assumptions_json (anal, fcn); + if (cons) { + r_cons_printf (cons, "%s\n", assumptions_json? assumptions_json: "[]"); + } + free (assumptions_json); + return strdup (""); + } + + if (!strncmp (cmd, "sla.assumej", 11) && (!cmd[11] || isspace ((unsigned char)cmd[11]))) { + const char *arg = skip_cmd_spaces (cmd + 11); + RAnalFunction *fcn; + char *assumptions_json; + + if (!arg || !*arg) { + if (cons) { + r_cons_println (cons, "Usage: a:sla.assumej "); + } + return strdup (""); + } + fcn = resolve_or_materialize_current_function (core, anal); + if (!fcn) { + R_LOG_ERROR ("r2sleigh: no function at current address"); + return strdup (""); + } + assumptions_json = strdup (arg); + if (!assumptions_json) { + return strdup (""); + } + r_str_unescape (assumptions_json); + if (!r_anal_function_set_assumptions_json (anal, fcn, assumptions_json)) { + R_LOG_ERROR ("r2sleigh: invalid assumptions json array"); + free (assumptions_json); + return strdup (""); + } + free (assumptions_json); + assumptions_json = sleigh_collect_function_assumptions_json (anal, fcn); + if (cons) { + r_cons_printf (cons, "%s\n", assumptions_json? assumptions_json: "[]"); + } + free (assumptions_json); + return strdup (""); + } + if (!strncmp (cmd, "sla.arch", 8)) { const char *arg = cmd + 8; if (*arg == ' ') { @@ -4505,62 +7530,195 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { return strdup(""); } - R2ILBlock *block = r2il_lift (ctx, buf, sizeof (buf), addr); - if (!block) { - R_LOG_ERROR ("r2sleigh: lift failed"); - return strdup(""); + R2ILBlock *block = r2il_lift (ctx, buf, sizeof (buf), addr); + if (!block) { + R_LOG_ERROR ("r2sleigh: lift failed"); + return strdup(""); + } + + char *defuse_json = r2il_block_defuse_json (ctx, block); + if (cons && defuse_json) { + r_cons_printf (cons, "%s\n", defuse_json); + } + + r2il_string_free (defuse_json); + r2il_block_free (block); + return strdup(""); + } + + if ((!strncmp (cmd, "sla.facts", 9) && (!cmd[9] || isspace ((unsigned char)cmd[9]))) + || (!strncmp (cmd, "sla.plan", 8) && (!cmd[8] || isspace ((unsigned char)cmd[8])))) { + R2ILContext *ctx = get_context (anal); + RAnalFunction *fcn; + BlockArray blocks; + char *external_context_json = NULL; + char *interproc_scope_json = NULL; + char *result = NULL; + bool is_plan_cmd = r_str_startswith (cmd, "sla.plan"); + size_t prefix_len = is_plan_cmd? 8: 9; + const char *target_arg = skip_cmd_spaces (cmd + prefix_len); + ut64 *seen_addrs = NULL; + size_t seen_count = 0; + size_t seen_cap = 0; + int interproc_max_iters = cfg_get_type_interproc_max_iters (anal); + bool prefer_bounded_semantic_type_plan = false; + + if (!ctx) { + R_LOG_ERROR ("r2sleigh: no context"); + return strdup (""); + } + + fcn = (target_arg && *target_arg) + ? resolve_or_materialize_function_target (core, anal, target_arg) + : resolve_or_materialize_current_function (core, anal); + if (!fcn) { + if (target_arg && *target_arg) { + R_LOG_ERROR ("r2sleigh: function target not found: %s", target_arg); + } else { + R_LOG_ERROR ("r2sleigh: no function at current address"); + } + return strdup (""); } - - char *defuse_json = r2il_block_defuse_json (ctx, block); - if (cons && defuse_json) { - r_cons_printf (cons, "%s\n", defuse_json); + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { + R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); + return strdup (""); } + prefer_bounded_semantic_type_plan = should_skip_decompile_symbolic_scope (fcn); - r2il_string_free (defuse_json); - r2il_block_free (block); - return strdup(""); + external_context_json = sleigh_collect_external_context_json (anal, fcn); + if (!external_context_json || (external_context_json[0] != '{' && external_context_json[0] != '[')) { + free (external_context_json); + external_context_json = strdup ("{}"); + } + if (!prefer_bounded_semantic_type_plan) { + warm_type_payload_cache_for_function (core, anal, ctx, fcn, interproc_max_iters, + &seen_addrs, &seen_count, &seen_cap); + interproc_scope_json = build_type_interproc_scope_json ( + core, anal, ctx, fcn, &blocks, interproc_max_iters); + } + SymFunctionScope sym_scope; + if (build_symbolic_function_scope (anal, fcn, ctx, &sym_scope)) { + result = is_plan_cmd + ? r2sleigh_function_plan_json_scope_ex (ctx, + (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, + external_context_json, + 1, + prefer_bounded_semantic_type_plan? 1: interproc_max_iters, + prefer_bounded_semantic_type_plan? 0: 1, + interproc_scope_json? interproc_scope_json: "{}", + sym_scope.functions, sym_scope.count) + : r2sleigh_function_facts_json_scope_ex (ctx, + (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, + external_context_json, + 1, + prefer_bounded_semantic_type_plan? 1: interproc_max_iters, + prefer_bounded_semantic_type_plan? 0: 1, + interproc_scope_json? interproc_scope_json: "{}", + sym_scope.functions, sym_scope.count); + sym_function_scope_free (&sym_scope); + } else { + result = is_plan_cmd + ? r2sleigh_function_plan_json_scope_ex (ctx, + (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, + external_context_json, + 1, + prefer_bounded_semantic_type_plan? 1: interproc_max_iters, + prefer_bounded_semantic_type_plan? 0: 1, + interproc_scope_json? interproc_scope_json: "{}", + NULL, 0) + : r2sleigh_function_facts_json_scope_ex (ctx, + (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, + external_context_json, + 1, + prefer_bounded_semantic_type_plan? 1: interproc_max_iters, + prefer_bounded_semantic_type_plan? 0: 1, + interproc_scope_json? interproc_scope_json: "{}", + NULL, 0); + } + if (cons) { + if (result && *result) { + r_cons_printf (cons, "%s\n", result); + } else { + r_cons_println (cons, "{}"); + } + } + if (result) { + r2il_string_free (result); + } + free (seen_addrs); + free (interproc_scope_json); + free (external_context_json); + block_array_free (&blocks); + return strdup (""); } - if (!strcmp (cmd, "sla.types")) { + if (!strncmp (cmd, "sla.types", 9) && (!cmd[9] || isspace ((unsigned char)cmd[9]))) { R2ILContext *ctx = get_context (anal); RAnalFunction *fcn; BlockArray blocks; char *external_context_json = NULL; char *interproc_scope_json = NULL; char *result = NULL; + const char *target_arg = skip_cmd_spaces (cmd + 9); ut64 *seen_addrs = NULL; size_t seen_count = 0; size_t seen_cap = 0; int interproc_max_iters = cfg_get_type_interproc_max_iters (anal); + bool prefer_bounded_semantic_type_plan = false; if (!ctx) { R_LOG_ERROR ("r2sleigh: no context"); return strdup (""); } - fcn = r_anal_get_fcn_in (anal, core->addr, R_ANAL_FCN_TYPE_ANY); + fcn = (target_arg && *target_arg) + ? resolve_or_materialize_function_target (core, anal, target_arg) + : resolve_or_materialize_current_function (core, anal); if (!fcn) { - R_LOG_ERROR ("r2sleigh: no function at current address"); + if (target_arg && *target_arg) { + R_LOG_ERROR ("r2sleigh: function target not found: %s", target_arg); + } else { + R_LOG_ERROR ("r2sleigh: no function at current address"); + } return strdup (""); } - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup (""); } + prefer_bounded_semantic_type_plan = should_skip_decompile_symbolic_scope (fcn); external_context_json = sleigh_collect_external_context_json (anal, fcn); if (!external_context_json || (external_context_json[0] != '{' && external_context_json[0] != '[')) { free (external_context_json); external_context_json = strdup ("{}"); } - - warm_type_payload_cache_for_function (core, anal, ctx, fcn, interproc_max_iters, - &seen_addrs, &seen_count, &seen_cap); - interproc_scope_json = build_type_interproc_scope_json (core, anal, ctx, fcn, &blocks); - result = r2sleigh_infer_type_writeback_json_ex (ctx, - (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, - external_context_json, - 1, interproc_max_iters, 1, interproc_scope_json? interproc_scope_json: "{}"); + if (!prefer_bounded_semantic_type_plan) { + warm_type_payload_cache_for_function (core, anal, ctx, fcn, interproc_max_iters, + &seen_addrs, &seen_count, &seen_cap); + interproc_scope_json = build_type_interproc_scope_json ( + core, anal, ctx, fcn, &blocks, interproc_max_iters); + } + SymFunctionScope sym_scope; + if (build_symbolic_function_scope (anal, fcn, ctx, &sym_scope)) { + result = r2sleigh_infer_type_writeback_json_scope_ex (ctx, + (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, + external_context_json, + 1, + prefer_bounded_semantic_type_plan? 1: interproc_max_iters, + prefer_bounded_semantic_type_plan? 0: 1, + interproc_scope_json? interproc_scope_json: "{}", + sym_scope.functions, sym_scope.count); + sym_function_scope_free (&sym_scope); + } else { + result = r2sleigh_infer_type_writeback_json_ex (ctx, + (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, + external_context_json, + 1, + prefer_bounded_semantic_type_plan? 1: interproc_max_iters, + prefer_bounded_semantic_type_plan? 0: 1, + interproc_scope_json? interproc_scope_json: "{}"); + } if (cons) { if (result && *result) { r_cons_printf (cons, "%s\n", result); @@ -4594,9 +7752,28 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { return strdup(""); } + if (is_autogenerated_function_name (fcn->name)) { + DecompileCFGRiskSummary cfg_summary; + char *cfg_guard_comment = NULL; + if (compute_decompile_cfg_risk_summary (anal, fcn, &cfg_summary)) { + cfg_guard_comment = r2dec_cfg_guard_comment_ffi ( + fcn->name, + cfg_summary.block_count, + cfg_summary.loop_count, + cfg_summary.back_edge_count, + cfg_summary.max_switch_cases); + } + if (cfg_guard_comment) { + if (cons) { + r_cons_printf (cons, "%s\n", cfg_guard_comment); + } + r2il_string_free (cfg_guard_comment); + return strdup(""); + } + } /* Lift all blocks */ BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup(""); } @@ -4627,7 +7804,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { } BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup(""); } @@ -4659,7 +7836,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { /* Lift all blocks */ BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup(""); } @@ -4692,7 +7869,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { /* Lift all blocks */ BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup(""); } @@ -4742,7 +7919,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { /* Lift all blocks */ BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup(""); } @@ -4790,27 +7967,37 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { return strdup(""); } - if (!strcmp (cmd, "sla.sym") || !strcmp (cmd, "sla.sym.paths")) { + if ((!strncmp (cmd, "sla.sym.paths", 13) && (!cmd[13] || isspace ((unsigned char)cmd[13]))) + || (!strncmp (cmd, "sla.sym", 7) && (!cmd[7] || isspace ((unsigned char)cmd[7])))) { R2ILContext *ctx = get_context (anal); + bool is_paths_cmd = r_str_startswith (cmd, "sla.sym.paths"); + size_t prefix_len = is_paths_cmd ? 13: 7; + const char *target_arg = skip_cmd_spaces (cmd + prefix_len); + RAnalFunction *fcn; if (!ctx) { R_LOG_ERROR ("r2sleigh: no context"); return strdup(""); } - /* Get current function */ - RAnalFunction *fcn = r_anal_get_fcn_in (anal, core->addr, R_ANAL_FCN_TYPE_ANY); + fcn = (target_arg && *target_arg) + ? resolve_or_materialize_function_target (core, anal, target_arg) + : resolve_or_materialize_current_function (core, anal); if (!fcn) { - R_LOG_ERROR ("r2sleigh: no function at current address"); + if (target_arg && *target_arg) { + R_LOG_ERROR ("r2sleigh: function target not found: %s", target_arg); + } else { + R_LOG_ERROR ("r2sleigh: no function at current address"); + } return strdup(""); } - /* Lift all blocks */ - BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { - R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); + /* Lift root + reachable helper closure */ + SymFunctionScope scope; + if (!build_symbolic_function_scope (anal, fcn, ctx, &scope)) { + R_LOG_ERROR ("r2sleigh: failed to build symbolic function scope"); return strdup(""); } - char *sym_map_json = build_sym_symbol_map_json (core); + char *sym_map_json = build_sym_symbol_map_json (core, anal, ctx, &scope); if (sym_map_json) { r2sym_set_symbol_map_json (sym_map_json); free (sym_map_json); @@ -4818,18 +8005,20 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { /* Call symbolic execution */ char *result; - if (!strcmp (cmd, "sla.sym.paths")) { - result = r2sym_paths (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr); + char *external_context_json = sleigh_collect_sym_external_context_json (anal, fcn); + if (is_paths_cmd) { + result = r2sym_paths_scope (ctx, scope.functions, scope.count, fcn->addr, external_context_json); } else { - result = r2sym_function (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr); + result = r2sym_function_scope (ctx, scope.functions, scope.count, fcn->addr, external_context_json); } + free (external_context_json); if (cons && result) { r_cons_printf (cons, "%s\n", result); } r2il_string_free (result); - block_array_free (&blocks); + sym_function_scope_free (&scope); return strdup(""); } @@ -4849,7 +8038,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { /* Lift all blocks */ BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup(""); } @@ -4875,16 +8064,16 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { const char *target_arg = skip_cmd_spaces (cmd + 7); RAnalFunction *fcn = NULL; if (target_arg && *target_arg) { - fcn = resolve_function_target (core, anal, target_arg); + fcn = resolve_or_materialize_function_target (core, anal, target_arg); } else { - fcn = r_anal_get_fcn_in (anal, core->addr, R_ANAL_FCN_TYPE_ANY); + fcn = resolve_or_materialize_current_function (core, anal); } if (!fcn) { if (target_arg && *target_arg) { if (cons) { r_cons_printf (cons, - "/* r2dec: function target '%s' not found. It may be inlined or stripped. Try 'afl' or 'a:sla.dec 0xADDR'. */\n", + "/* r2dec: function target '%s' not found or could not be materialized (it may be inlined or stripped). */\n", target_arg); } } else { @@ -4893,13 +8082,73 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { return strdup(""); } + size_t decompile_max_blocks = decompiler_max_blocks_preflight (); + int bb_count = function_bb_count (fcn); + DecompileCFGRiskSummary cfg_summary; + bool have_cfg_summary = false; + if (is_autogenerated_function_name (fcn->name)) { + have_cfg_summary = compute_decompile_cfg_risk_summary (anal, fcn, &cfg_summary); + } + if (is_autogenerated_function_name (fcn->name) + && bb_count > 0 + && (size_t)bb_count > decompile_max_blocks) { + char *guard_comment = r2dec_block_guard_comment_ffi ( + fcn->name, (size_t)bb_count, decompile_max_blocks); + if (cons) { + if (guard_comment && guard_comment[0]) { + r_cons_printf (cons, "%s\n", guard_comment); + } else { + const char *fname = (fcn && fcn->name) ? fcn->name : "unknown"; + r_cons_printf (cons, + "/* r2dec fallback: skipped decompilation for %s (complex loop graph; block preflight exceeded %zu) */\n", + fname, decompile_max_blocks); + } + } + if (guard_comment) { + r2il_string_free (guard_comment); + } + return strdup(""); + } /* Lift all blocks */ BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup(""); } + char *result = NULL; + SymFunctionScope sym_scope; + bool have_sym_scope = build_symbolic_function_scope (anal, fcn, ctx, &sym_scope); + if (have_cfg_summary) { + result = r2dec_semantic_worker_linearization_scope_ffi ( + ctx, + (const R2ILBlock **)blocks.blocks, + blocks.count, + fcn->addr, + fcn->name, + cfg_summary.block_count, + cfg_summary.loop_count, + cfg_summary.back_edge_count, + cfg_summary.max_switch_cases, + have_sym_scope? sym_scope.functions: NULL, + have_sym_scope? sym_scope.count: 0); + if (result && result[0]) { + if (cons) { + r_cons_printf (cons, "%s\n", result); + } + r2il_string_free (result); + if (have_sym_scope) { + sym_function_scope_free (&sym_scope); + } + block_array_free (&blocks); + return strdup(""); + } + if (result) { + r2il_string_free (result); + result = NULL; + } + } + /* Gather function names from r2 */ char *func_names_json = NULL; char *strings_json = NULL; @@ -5010,9 +8259,16 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { } /* Decompile with context */ - char *result = r2dec_function_with_context (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, - fcn->name, func_names_json, strings_json, symbols_json, - external_context_json); + if (have_sym_scope) { + result = r2dec_function_with_context_scope (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, + fcn->addr, fcn->name, func_names_json, strings_json, symbols_json, + external_context_json, sym_scope.functions, sym_scope.count); + sym_function_scope_free (&sym_scope); + } else { + result = r2dec_function_with_context (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, + fcn->name, func_names_json, strings_json, symbols_json, + external_context_json); + } if (cons) { if (result && result[0]) { @@ -5050,7 +8306,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { /* Lift all blocks */ BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup(""); } @@ -5087,18 +8343,23 @@ static bool sleigh_analyze_fcn(RAnal *anal, RAnalFunction *fcn) { return false; } + if (!sleigh_mode_allows_deep_auto_callbacks (anal)) { + return true; + } + if (function_exceeds_auto_callback_budget (fcn)) { + R_LOG_DEBUG ("r2sleigh: auto analyze_fcn skipped by budget fcn=0x%"PFMT64x" blocks=%d", + fcn->addr, function_bb_count (fcn)); + return true; + } + R2ILContext *ctx = get_context (anal); if (!ctx) { return false; } - if (sleigh_mode_is_fast (anal)) { - return true; - } - BlockArray blocks; ut64 cache_key; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { return false; } @@ -5138,7 +8399,12 @@ static RList *sleigh_recover_vars(RAnal *anal, RAnalFunction *fcn) { if (!fcn || !anal) { return NULL; } - if (sleigh_mode_is_fast (anal)) { + if (!sleigh_mode_allows_deep_auto_callbacks (anal)) { + return NULL; + } + if (function_exceeds_auto_callback_budget (fcn)) { + R_LOG_DEBUG ("r2sleigh: auto recover_vars skipped by budget fcn=0x%"PFMT64x" blocks=%d", + fcn->addr, function_bb_count (fcn)); return NULL; } @@ -5148,7 +8414,7 @@ static RList *sleigh_recover_vars(RAnal *anal, RAnalFunction *fcn) { } BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { return NULL; } @@ -5394,7 +8660,12 @@ static RVecAnalRef *sleigh_get_data_refs(RAnal *anal, RAnalFunction *fcn) { if (!fcn || !anal) { return NULL; } - if (sleigh_mode_is_fast (anal)) { + if (!sleigh_mode_allows_deep_auto_callbacks (anal)) { + return NULL; + } + if (function_exceeds_auto_callback_budget (fcn)) { + R_LOG_DEBUG ("r2sleigh: auto get_data_refs skipped by budget fcn=0x%"PFMT64x" blocks=%d", + fcn->addr, function_bb_count (fcn)); return NULL; } @@ -5405,7 +8676,7 @@ static RVecAnalRef *sleigh_get_data_refs(RAnal *anal, RAnalFunction *fcn) { BlockArray blocks; ut64 cache_key; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { return NULL; } cache_key = compute_xref_cache_key (fcn, &blocks, sleigh_mode_effective_for_post_analysis (anal)); @@ -5752,6 +9023,8 @@ static bool verify_practical_signature_consistency ( ConsistencyReasonCounters *reason_counters ); +static bool reconcile_signature_register_arg_vars(RAnal *anal, RAnalFunction *fcn, const RJson *sig_root); + static bool apply_inferred_signature_typed( RAnal *anal, RAnalFunction *fcn, @@ -5826,6 +9099,9 @@ static bool apply_inferred_signature_typed( input.params = param_list; input.noreturn = fcn->is_noreturn; ok = r_anal_function_set_signature (anal, fcn, &input); + if (ok) { + reconcile_signature_register_arg_vars (anal, fcn, sig_root); + } r_list_free (param_list); if (!ok) { write_reason_msg (reason, reason_sz, "typed signature apply failed"); @@ -5847,6 +9123,7 @@ static WritebackApplyResult apply_inferred_signature( return res; } if (verify_signature_type_db_ex (anal, fcn, sig_root, res.detail, sizeof (res.detail))) { + reconcile_signature_register_arg_vars (anal, fcn, sig_root); res.already_applied = true; if (!res.detail[0]) { write_reason_msg (res.detail, sizeof (res.detail), "signature already matches"); @@ -5854,6 +9131,7 @@ static WritebackApplyResult apply_inferred_signature( return res; } if (verify_practical_signature_consistency (anal, fcn, sig_root, true, false, NULL, NULL)) { + reconcile_signature_register_arg_vars (anal, fcn, sig_root); res.already_applied = true; write_reason_msg (res.detail, sizeof (res.detail), "practical signature already matches"); return res; @@ -6064,9 +9342,9 @@ static int resolve_reg_index(RAnal *anal, const char *reg_name) { *p = toupper ((unsigned char)*p); } } - ri = upper_reg? r_reg_get (anal->reg, upper_reg, R_REG_TYPE_GPR): NULL; + ri = r_reg_get (anal->reg, reg_name, R_REG_TYPE_GPR); if (!ri) { - ri = r_reg_get (anal->reg, reg_name, R_REG_TYPE_GPR); + ri = upper_reg? r_reg_get (anal->reg, upper_reg, R_REG_TYPE_GPR): NULL; } if (ri) { index = ri->index; @@ -6101,6 +9379,106 @@ static RAnalVar *lookup_var_for_candidate(RAnal *anal, RAnalFunction *fcn, const return NULL; } +static int score_signature_register_arg_var(RAnalVar *var, const char *expected_name) { + if (!var) { + return -1; + } + if (expected_name && *expected_name && var->name && strings_match_normalized (var->name, expected_name)) { + return 100; + } + if (var->name && !is_generated_var_name (var->name)) { + return 50; + } + return 10; +} + +static bool is_stack_arg_name_conflict(RAnalVar *var, RAnalVar *best, const char *expected_name) { + if (!var || var == best || !expected_name || !*expected_name || !var->name) { + return false; + } + if (!var->isarg || (var->kind != R_ANAL_VAR_KIND_BPV && var->kind != R_ANAL_VAR_KIND_SPV)) { + return false; + } + return strings_match_normalized (var->name, expected_name); +} + +static bool reconcile_signature_register_arg_vars(RAnal *anal, RAnalFunction *fcn, const RJson *sig_root) { + const RJson *j_params; + const RJson *j_param; + bool changed = false; + int idx = 0; + + if (!anal || !fcn || !sig_root || sig_root->type != R_JSON_OBJECT) { + return false; + } + j_params = r_json_get (sig_root, "params"); + if (!j_params || j_params->type != R_JSON_ARRAY) { + return false; + } + + for (j_param = json_next_object (j_params->children.first); j_param; j_param = json_next_object (j_param->next), idx++) { + const RJson *j_name = r_json_get (j_param, "name"); + const RJson *j_type = r_json_get (j_param, "type"); + const char *expected_name = json_is_string_with_value (j_name)? j_name->str_value: NULL; + const char *expected_type = json_is_string_with_value (j_type)? j_type->str_value: NULL; + RAnalVar *best = NULL; + int best_score = -1; + RListIter *iter; + RAnalVar *var; + RList *vars = r_anal_var_all_list (anal, fcn); + + if (!vars) { + continue; + } + r_list_foreach (vars, iter, var) { + int score; + if (!var || !var->isarg || var->kind != R_ANAL_VAR_KIND_REG) { + continue; + } + if (r_anal_var_get_argnum (var) != idx) { + continue; + } + score = score_signature_register_arg_var (var, expected_name); + if (score > best_score) { + best = var; + best_score = score; + } + } + r_list_free (vars); + vars = NULL; + if (best) { + if (expected_type && *expected_type && (!best->type || !strings_match_normalized (best->type, expected_type))) { + r_anal_var_set_type (anal, best, expected_type); + changed = true; + } + if (expected_name && *expected_name && (!best->name || !strings_match_normalized (best->name, expected_name))) { + RAnalVar *conflict = r_anal_function_get_var_byname (fcn, expected_name); + if (is_stack_arg_name_conflict (conflict, best, expected_name)) { + r_anal_var_delete (anal, conflict); + changed = true; + } + r_anal_var_rename (anal, best, expected_name); + changed = true; + } + vars = r_anal_var_all_list (anal, fcn); + if (!vars) { + continue; + } + r_list_foreach (vars, iter, var) { + if (!var || var == best || !var->isarg || var->kind != R_ANAL_VAR_KIND_REG) { + continue; + } + if (r_anal_var_get_argnum (var) == idx) { + r_anal_var_delete (anal, var); + changed = true; + } + } + r_list_free (vars); + } + } + return changed; +} + static bool verify_var_type_applied(RAnalVar *var, const char *expected_type) { if (!var || !expected_type || !*expected_type || !var->type || !*var->type) { return false; @@ -6381,6 +9759,10 @@ static bool apply_var_rename_candidate( if (!is_generated_var_name (var->name)) { return false; } + RAnalVar *conflict = r_anal_function_get_var_byname (fcn, new_name); + if (is_stack_arg_name_conflict (conflict, var, new_name)) { + r_anal_var_delete (anal, conflict); + } if (r_anal_var_rename (anal, var, new_name) && verify_var_rename_applied (var, new_name)) { return true; } @@ -6437,8 +9819,7 @@ static bool apply_global_type_link_candidate(RAnal *anal, RCore *core, ut64 addr } static ut64 compute_type_cache_key( - RAnalFunction *fcn, - const char *external_context_json, + ut64 artifact_key, ut64 dep_hash, SleighTypeWritebackMode mode, int min_conf, @@ -6446,19 +9827,13 @@ static ut64 compute_type_cache_key( int struct_min_conf, int max_iters ) { - ut64 key = 0; - int bb_count = (fcn && fcn->bbs)? r_list_length (fcn->bbs): 0; - int linear_size = fcn? r_anal_function_linear_size (fcn): 0; - key ^= fcn? fcn->addr: 0; - key ^= ((ut64)bb_count << 32); - key ^= (ut64)linear_size; + ut64 key = artifact_key; key ^= ((ut64)mode << 56); key ^= ((ut64)(min_conf & 0xff) << 8); key ^= ((ut64)(rename_min_conf & 0xff) << 16); key ^= ((ut64)(struct_min_conf & 0xff) << 24); key ^= ((ut64)(max_iters & 0xffff) << 40); key ^= dep_hash; - key ^= r_str_hash64 (external_context_json? external_context_json: ""); return key; } @@ -6552,16 +9927,154 @@ static char *resolve_interproc_seed_name(RCore *core, RAnal *anal, ut64 addr) { raw_name = target_fcn->name; } } - return raw_name? strdup (raw_name): NULL; + return raw_name? strdup (raw_name): NULL; +} + +static ut64 *collect_type_interproc_direct_targets_from_blocks( + R2ILContext *ctx, + const BlockArray *blocks, + ut64 fcn_addr, + const char *fcn_name, + size_t *out_count +) { + char *targets_json = NULL; + RJson *root = NULL; + RJson *item; + ut64 *targets = NULL; + size_t count = 0; + size_t cap = 0; + + if (out_count) { + *out_count = 0; + } + if (!ctx || !blocks || !blocks->blocks || blocks->count == 0) { + return NULL; + } + targets_json = r2sleigh_get_direct_call_targets_json (ctx, + (const R2ILBlock **)blocks->blocks, blocks->count, fcn_addr, fcn_name); + if (!targets_json || !*targets_json) { + r2il_string_free (targets_json); + return NULL; + } + root = r_json_parse (targets_json); + if (!root || root->type != R_JSON_ARRAY) { + r_json_free (root); + r2il_string_free (targets_json); + return NULL; + } + for (item = root->children.first; item; item = item->next) { + if (!item || item->type != R_JSON_INTEGER) { + continue; + } + append_unique_ut64 (&targets, &count, &cap, item->num.u_value); + } + r_json_free (root); + r2il_string_free (targets_json); + if (out_count) { + *out_count = count; + } + return targets; +} + +static char *build_ut64_json_array(const ut64 *values, size_t count) { + PJ *pj; + size_t i; + char *out; + + if (!values || count == 0) { + return strdup ("[]"); + } + pj = pj_new (); + if (!pj) { + return strdup ("[]"); + } + pj_a (pj); + for (i = 0; i < count; i++) { + pj_n (pj, values[i]); + } + pj_end (pj); + out = strdup (pj_string (pj)); + pj_free (pj); + return out? out: strdup ("[]"); +} + +static const char *strip_runtime_scope_name_prefixes(const char *name) { + const char *normalized = name; + if (!normalized) { + return NULL; + } + for (;;) { + if (!strncasecmp (normalized, "sym.imp.", 8)) { + normalized += 8; + continue; + } + if (!strncasecmp (normalized, "sym.", 4)) { + normalized += 4; + continue; + } + if (!strncasecmp (normalized, "imp.", 4)) { + normalized += 4; + continue; + } + if (!strncasecmp (normalized, "reloc.", 6)) { + normalized += 6; + continue; + } + if (!strncasecmp (normalized, "dbg.", 4)) { + normalized += 4; + continue; + } + if (!strncasecmp (normalized, "fcn.", 4)) { + normalized += 4; + continue; + } + break; + } + return normalized; +} + +static bool function_name_is_windows_runtime_registration(const char *name) { + const char *normalized = strip_runtime_scope_name_prefixes (name); + if (!normalized || !*normalized) { + return false; + } + size_t len = strlen (normalized); + size_t suffix_len = strlen ("AddVectoredExceptionHandler"); + if (len < suffix_len) { + return false; + } + return !strcasecmp (normalized + (len - suffix_len), "AddVectoredExceptionHandler"); +} + +static bool function_name_is_import_like(const char *name) { + if (!name || !*name) { + return false; + } + return !strncasecmp (name, "sym.imp.", 8) + || !strncasecmp (name, "imp.", 4) + || !strncasecmp (name, "reloc.", 6); } -static ut64 *collect_type_interproc_direct_targets_from_blocks( +static bool function_name_is_runtime_copy(const char *name) { + const char *normalized = strip_runtime_scope_name_prefixes (name); + if (!normalized || !*normalized) { + return false; + } + return !strcasecmp (normalized, "memcpy") + || !strcasecmp (normalized, "__memcpy_chk") + || !strncasecmp (normalized, "memcpy", 6); +} + +static ut64 *collect_runtime_scope_targets_from_blocks( R2ILContext *ctx, const BlockArray *blocks, ut64 fcn_addr, const char *fcn_name, + const ut64 *registration_targets, + size_t registration_target_count, size_t *out_count ) { + char *request_json = NULL; char *targets_json = NULL; RJson *root = NULL; RJson *item; @@ -6572,11 +10085,17 @@ static ut64 *collect_type_interproc_direct_targets_from_blocks( if (out_count) { *out_count = 0; } - if (!ctx || !blocks || !blocks->blocks || blocks->count == 0) { + if (!ctx || !blocks || !blocks->blocks || blocks->count == 0 || !registration_target_count) { return NULL; } - targets_json = r2sleigh_get_direct_call_targets_json (ctx, - (const R2ILBlock **)blocks->blocks, blocks->count, fcn_addr, fcn_name); + request_json = build_ut64_json_array (registration_targets, registration_target_count); + if (!request_json || !*request_json) { + free (request_json); + return NULL; + } + targets_json = r2sleigh_get_symbolic_scope_targets_json (ctx, + (const R2ILBlock **)blocks->blocks, blocks->count, fcn_addr, fcn_name, request_json); + free (request_json); if (!targets_json || !*targets_json) { r2il_string_free (targets_json); return NULL; @@ -6601,11 +10120,554 @@ static ut64 *collect_type_interproc_direct_targets_from_blocks( return targets; } +static bool append_runtime_materialized_source( + RuntimeMaterializedSource **sources, + size_t *count, + size_t *cap, + ut64 addr, + ut64 size +) { + RuntimeMaterializedSource *next; + size_t i; + if (!sources || !count || !cap || !addr || !size) { + return false; + } + for (i = 0; i < *count; i++) { + if ((*sources)[i].addr == addr) { + if ((*sources)[i].size < size) { + (*sources)[i].size = size; + } + return true; + } + } + if (*count >= *cap) { + size_t new_cap = *cap? *cap * 2: 4; + next = realloc (*sources, new_cap * sizeof (**sources)); + if (!next) { + return false; + } + *sources = next; + *cap = new_cap; + } + (*sources)[*count].addr = addr; + (*sources)[*count].size = size; + (*count)++; + return true; +} + +static RuntimeMaterializedSource *collect_runtime_materialized_sources_from_blocks( + R2ILContext *ctx, + const BlockArray *blocks, + ut64 fcn_addr, + const char *fcn_name, + const ut64 *copy_targets, + size_t copy_target_count, + size_t *out_count +) { + char *request_json = NULL; + char *sources_json = NULL; + RJson *root = NULL; + RJson *item; + RuntimeMaterializedSource *sources = NULL; + size_t count = 0; + size_t cap = 0; + + if (out_count) { + *out_count = 0; + } + if (!ctx || !blocks || !blocks->blocks || blocks->count == 0 || !copy_target_count) { + return NULL; + } + request_json = build_ut64_json_array (copy_targets, copy_target_count); + if (!request_json || !*request_json) { + free (request_json); + return NULL; + } + sources_json = r2sleigh_get_runtime_materialized_sources_json (ctx, + (const R2ILBlock **)blocks->blocks, blocks->count, fcn_addr, fcn_name, request_json); + free (request_json); + if (!sources_json || !*sources_json) { + r2il_string_free (sources_json); + return NULL; + } + root = r_json_parse (sources_json); + if (!root || root->type != R_JSON_ARRAY) { + r_json_free (root); + r2il_string_free (sources_json); + return NULL; + } + for (item = root->children.first; item; item = item->next) { + const RJson *addr_json; + const RJson *size_json; + ut64 addr; + ut64 size; + if (!item || item->type != R_JSON_OBJECT) { + continue; + } + addr_json = r_json_get (item, "addr"); + size_json = r_json_get (item, "size"); + if (!addr_json || addr_json->type != R_JSON_INTEGER + || !size_json || size_json->type != R_JSON_INTEGER) { + continue; + } + addr = addr_json->num.u_value; + size = size_json->num.u_value; + if (addr && size) { + (void)append_runtime_materialized_source (&sources, &count, &cap, addr, size); + } + } + r_json_free (root); + r2il_string_free (sources_json); + if (out_count) { + *out_count = count; + } + return sources; +} + +static void sym_function_scope_init(SymFunctionScope *scope) { + if (!scope) { + return; + } + memset (scope, 0, sizeof (*scope)); +} + +static void sym_function_scope_free(SymFunctionScope *scope) { + size_t i; + if (!scope) { + return; + } + for (i = 0; i < scope->count; i++) { + block_array_free (&scope->owned_blocks[i]); + free (scope->owned_names[i]); + } + free (scope->functions); + free (scope->owned_blocks); + free (scope->owned_names); + memset (scope, 0, sizeof (*scope)); +} + +static bool sym_function_scope_ensure_capacity(SymFunctionScope *scope, size_t needed) { + R2ILFunctionBlocks *functions_next; + BlockArray *blocks_next; + char **names_next; + size_t new_cap; + if (!scope) { + return false; + } + if (needed <= scope->capacity) { + return true; + } + new_cap = scope->capacity? scope->capacity * 2: 4; + while (new_cap < needed) { + new_cap *= 2; + } + functions_next = realloc (scope->functions, new_cap * sizeof (*scope->functions)); + blocks_next = realloc (scope->owned_blocks, new_cap * sizeof (*scope->owned_blocks)); + names_next = realloc (scope->owned_names, new_cap * sizeof (*scope->owned_names)); + if (!functions_next || !blocks_next || !names_next) { + free (functions_next); + free (blocks_next); + free (names_next); + return false; + } + scope->functions = functions_next; + scope->owned_blocks = blocks_next; + scope->owned_names = names_next; + scope->capacity = new_cap; + return true; +} + +static bool sym_function_scope_append( + SymFunctionScope *scope, + RAnal *anal, + RAnalFunction *fcn, + R2ILContext *ctx, + bool include_linear_gap_blocks +) { + BlockArray blocks; + if (!scope || !anal || !fcn || !ctx) { + return false; + } + if (!sym_function_scope_ensure_capacity (scope, scope->count + 1)) { + return false; + } + sleigh_debug_scope_log ( + "scope_append addr=0x%"PFMT64x" name=%s include_gaps=%d", + fcn->addr, + fcn->name? fcn->name: "(null)", + include_linear_gap_blocks? 1: 0 + ); + if (!lift_function_blocks (anal, fcn, ctx, &blocks, include_linear_gap_blocks)) { + sleigh_debug_scope_log ("scope_append_failed addr=0x%"PFMT64x, fcn->addr); + return false; + } + scope->owned_blocks[scope->count] = blocks; + scope->owned_names[scope->count] = fcn->name? strdup (fcn->name): NULL; + scope->functions[scope->count].entry_addr = fcn->addr; + scope->functions[scope->count].name = scope->owned_names[scope->count]; + scope->functions[scope->count].blocks = (const R2ILBlock **)scope->owned_blocks[scope->count].blocks; + scope->functions[scope->count].num_blocks = scope->owned_blocks[scope->count].count; + scope->count++; + sleigh_debug_scope_log ( + "scope_appended addr=0x%"PFMT64x" blocks=%zu", + fcn->addr, + blocks.count + ); + return true; +} + +static bool lift_runtime_materialized_source_blocks( + RAnal *anal, + R2ILContext *ctx, + ut64 addr, + ut64 size, + BlockArray *out +) { + ut64 limit; + ut64 offset = 0; + + if (!anal || !ctx || !out || !addr || !size || !anal->iob.read_at) { + return false; + } + limit = R_MIN (size, (ut64)SLEIGH_RUNTIME_MATERIALIZED_MAX_BYTES); + block_array_init (out); + while (offset < limit) { + ut64 cur = addr + offset; + ut8 buf[SLEIGH_MIN_BYTES] = {0}; + ut64 remaining = limit - offset; + ut64 logical_size = R_MIN (remaining, (ut64)SLEIGH_RUNTIME_MATERIALIZED_SLOT_BYTES); + R2ILBlock *block; + + if (!anal->iob.read_at (anal->iob.io, cur, buf, sizeof (buf))) { + break; + } + block = r2il_lift_block (ctx, buf, sizeof (buf), cur, (unsigned int)logical_size); + if (block) { + if (r2il_block_validate (ctx, block)) { + block_array_push (out, block); + } else { + r2il_block_free (block); + } + } + offset += logical_size; + } + block_array_sort (out); + return out->count > 0; +} + +static bool sym_function_scope_append_runtime_source( + SymFunctionScope *scope, + RAnal *anal, + R2ILContext *ctx, + ut64 addr, + ut64 size +) { + BlockArray blocks; + char name[64]; + + if (!scope || !anal || !ctx || !addr || !size) { + return false; + } + if (!lift_runtime_materialized_source_blocks (anal, ctx, addr, size, &blocks)) { + return false; + } + if (!sym_function_scope_ensure_capacity (scope, scope->count + 1)) { + block_array_free (&blocks); + return false; + } + snprintf (name, sizeof (name), "runtime.materialized.%"PFMT64x, addr); + scope->owned_blocks[scope->count] = blocks; + scope->owned_names[scope->count] = strdup (name); + scope->functions[scope->count].entry_addr = addr; + scope->functions[scope->count].name = scope->owned_names[scope->count]; + scope->functions[scope->count].blocks = (const R2ILBlock **)scope->owned_blocks[scope->count].blocks; + scope->functions[scope->count].num_blocks = scope->owned_blocks[scope->count].count; + scope->count++; + sleigh_debug_scope_log ( + "scope_runtime_source addr=0x%"PFMT64x" size=%"PFMT64u" blocks=%zu", + addr, + size, + blocks.count + ); + return true; +} + +static bool build_symbolic_function_scope_with_target( + RAnal *anal, + RAnalFunction *root_fcn, + R2ILContext *ctx, + SymFunctionScope *scope, + ut64 target_hint +) { + size_t queue_count = 0; + size_t queue_cap = 0; + size_t queue_index = 0; + ut64 *queue = NULL; + ut64 *seen = NULL; + size_t seen_count = 0; + size_t seen_cap = 0; + ut64 target_entry = UT64_MAX; + + if (!anal || !root_fcn || !ctx || !scope) { + return false; + } + sym_function_scope_init (scope); + if (!append_unique_ut64 (&queue, &queue_count, &queue_cap, root_fcn->addr)) { + free (queue); + return false; + } + if (target_hint != UT64_MAX && target_hint && target_hint != root_fcn->addr) { + RAnalFunction *target_fcn = materialize_function_at (anal, target_hint); + if (target_fcn) { + target_entry = target_fcn->addr; + append_unique_ut64 (&queue, &queue_count, &queue_cap, target_fcn->addr); + sleigh_debug_scope_log ( + "scope_target_hint target=0x%"PFMT64x" entry=0x%"PFMT64x" name=%s", + target_hint, + target_fcn->addr, + target_fcn->name? target_fcn->name: "(null)" + ); + } else { + sleigh_debug_scope_log ( + "scope_target_hint_unmaterialized target=0x%"PFMT64x, + target_hint + ); + } + } + sleigh_debug_scope_log ( + "scope_build_start root=0x%"PFMT64x" name=%s", + root_fcn->addr, + root_fcn->name? root_fcn->name: "(null)" + ); + + while (queue_index < queue_count && scope->count < SLEIGH_SYM_HELPER_MAX_FUNCTIONS) { + RAnalFunction *fcn; + ut64 addr = queue[queue_index++]; + ut64 *direct_targets = NULL; + ut64 *queued_direct_targets = NULL; + ut64 *runtime_copy_targets = NULL; + ut64 *runtime_targets = NULL; + RuntimeMaterializedSource *runtime_sources = NULL; + size_t target_count = 0; + size_t queued_direct_target_count = 0; + size_t queued_direct_target_cap = 0; + size_t runtime_copy_target_count = 0; + size_t runtime_copy_target_cap = 0; + size_t runtime_target_count = 0; + size_t runtime_source_count = 0; + ut64 registration_targets[8] = {0}; + size_t registration_target_count = 0; + size_t i; + const BlockArray *blocks; + + fcn = materialize_function_at (anal, addr); + if (!fcn || !append_unique_ut64 (&seen, &seen_count, &seen_cap, fcn->addr)) { + continue; + } + bool target_hint_function = target_entry != UT64_MAX && fcn->addr == target_entry; + if (fcn->addr != root_fcn->addr + && !target_hint_function + && function_exceeds_helper_scope_budget (fcn)) { + sleigh_debug_scope_log ( + "scope_skip_budget addr=0x%"PFMT64x" name=%s", + fcn->addr, + fcn->name? fcn->name: "(null)" + ); + continue; + } + if (!sym_function_scope_append ( + scope, + anal, + fcn, + ctx, + scope->count == 0 || fcn->addr == root_fcn->addr + )) { + continue; + } + if (fcn->addr != root_fcn->addr) { + sleigh_debug_scope_log ( + "scope_expand_helper addr=0x%"PFMT64x" name=%s", + fcn->addr, + fcn->name? fcn->name: "(null)" + ); + } + if (target_hint_function && fcn->addr != root_fcn->addr) { + sleigh_debug_scope_log ( + "scope_target_terminal addr=0x%"PFMT64x" target=0x%"PFMT64x, + fcn->addr, + target_hint + ); + continue; + } + blocks = &scope->owned_blocks[scope->count - 1]; + direct_targets = collect_type_interproc_direct_targets_from_blocks ( + ctx, blocks, fcn->addr, fcn->name, &target_count); + sleigh_debug_scope_log ( + "scope_targets addr=0x%"PFMT64x" direct=%zu", + fcn->addr, + target_count + ); + for (i = 0; i < target_count; i++) { + char *target_name = resolve_interproc_seed_name (anal->coreb.core, anal, direct_targets[i]); + bool queue_direct_target = true; + RAnalFunction *target_fcn = NULL; + ut64 scope_target = direct_targets[i]; + const char *summary_alias = NULL; + if (target_name && registration_target_count < R_ARRAY_SIZE (registration_targets) + && function_name_is_windows_runtime_registration (target_name)) { + registration_targets[registration_target_count++] = direct_targets[i]; + } + if (target_name && function_name_is_runtime_copy (target_name)) { + append_unique_ut64 ( + &runtime_copy_targets, + &runtime_copy_target_count, + &runtime_copy_target_cap, + direct_targets[i] + ); + } + if (target_name && function_name_is_import_like (target_name)) { + queue_direct_target = false; + sleigh_debug_scope_log ( + "scope_skip_import_target addr=0x%"PFMT64x" name=%s", + direct_targets[i], + target_name + ); + } + if (queue_direct_target) { + summary_alias = detect_local_summary_alias (anal, direct_targets[i]); + if (summary_alias) { + queue_direct_target = false; + if (function_name_is_runtime_copy (summary_alias)) { + append_unique_ut64 ( + &runtime_copy_targets, + &runtime_copy_target_count, + &runtime_copy_target_cap, + direct_targets[i] + ); + } + sleigh_debug_scope_log ( + "scope_skip_summary_target addr=0x%"PFMT64x" summary=%s", + direct_targets[i], + summary_alias + ); + } + } + if (queue_direct_target) { + scope_target = resolve_local_direct_jump_thunk_target (anal, direct_targets[i]); + if (scope_target != direct_targets[i]) { + sleigh_debug_scope_log ( + "scope_resolved_thunk_target from=0x%"PFMT64x" to=0x%"PFMT64x" name=%s", + direct_targets[i], + scope_target, + target_name? target_name: "(null)" + ); + } + target_fcn = materialize_function_at (anal, scope_target); + if (!target_fcn) { + queue_direct_target = false; + sleigh_debug_scope_log ( + "scope_skip_unmaterialized_target addr=0x%"PFMT64x" resolved=0x%"PFMT64x" name=%s", + direct_targets[i], + scope_target, + target_name? target_name: "(null)" + ); + } else if (function_exceeds_helper_scope_budget (target_fcn)) { + queue_direct_target = false; + sleigh_debug_scope_log ( + "scope_skip_budget_target addr=0x%"PFMT64x" resolved=0x%"PFMT64x" name=%s", + direct_targets[i], + scope_target, + target_name? target_name: "(null)" + ); + } + } + free (target_name); + if (queue_direct_target) { + if (scope_target != direct_targets[i]) { + append_unique_ut64 ( + &queued_direct_targets, + &queued_direct_target_count, + &queued_direct_target_cap, + direct_targets[i] + ); + } + append_unique_ut64 ( + &queued_direct_targets, + &queued_direct_target_count, + &queued_direct_target_cap, + scope_target + ); + } + } + runtime_targets = collect_runtime_scope_targets_from_blocks ( + ctx, + blocks, + fcn->addr, + fcn->name, + registration_targets, + registration_target_count, + &runtime_target_count); + runtime_sources = collect_runtime_materialized_sources_from_blocks ( + ctx, + blocks, + fcn->addr, + fcn->name, + runtime_copy_targets, + runtime_copy_target_count, + &runtime_source_count); + sleigh_debug_scope_log ( + "scope_runtime_targets addr=0x%"PFMT64x" registrations=%zu runtime=%zu materialized=%zu", + fcn->addr, + registration_target_count, + runtime_target_count, + runtime_source_count + ); + for (i = 0; i < runtime_target_count; i++) { + append_unique_ut64 (&queue, &queue_count, &queue_cap, runtime_targets[i]); + } + for (i = 0; i < runtime_source_count && scope->count < SLEIGH_SYM_HELPER_MAX_FUNCTIONS; i++) { + if (append_unique_ut64 (&seen, &seen_count, &seen_cap, runtime_sources[i].addr)) { + (void)sym_function_scope_append_runtime_source ( + scope, + anal, + ctx, + runtime_sources[i].addr, + runtime_sources[i].size + ); + } + } + for (i = 0; i < queued_direct_target_count; i++) { + append_unique_ut64 (&queue, &queue_count, &queue_cap, queued_direct_targets[i]); + } + free (queued_direct_targets); + free (runtime_copy_targets); + free (runtime_targets); + free (runtime_sources); + free (direct_targets); + } + + free (queue); + free (seen); + sleigh_debug_scope_log ("scope_build_done count=%zu", scope->count); + return scope->count > 0; +} + +static bool build_symbolic_function_scope( + RAnal *anal, + RAnalFunction *root_fcn, + R2ILContext *ctx, + SymFunctionScope *scope +) { + return build_symbolic_function_scope_with_target (anal, root_fcn, ctx, scope, UT64_MAX); +} + static char *build_type_interproc_scope_json_from_targets( RCore *core, RAnal *anal, + R2ILContext *ctx, const ut64 *targets, - size_t target_count + size_t target_count, + int max_iters ) { PJ *pj; ut64 *seen_addrs = NULL; @@ -6614,7 +10676,7 @@ static char *build_type_interproc_scope_json_from_targets( size_t i; char *out; - if (!anal || !targets || target_count == 0) { + if (!anal || !ctx || !targets || target_count == 0) { return strdup ("{}"); } pj = pj_new (); @@ -6624,6 +10686,58 @@ static char *build_type_interproc_scope_json_from_targets( pj_o (pj); pj_ks (pj, "phase", "fixpoint"); + pj_k (pj, "summaries"); + pj_a (pj); + for (i = 0; i < target_count; i++) { + RAnalFunction *callee_fcn; + TypeWritebackCacheEntry *entry; + BlockArray callee_blocks; + char *external_context_json = NULL; + char *summary_json = NULL; + ut64 target = targets[i]; + if (!target) { + continue; + } + callee_fcn = materialize_function_at (anal, target); + if (!callee_fcn || function_exceeds_helper_scope_budget (callee_fcn) + || !append_unique_ut64 (&seen_addrs, &seen_count, &seen_cap, callee_fcn->addr)) { + continue; + } + entry = type_writeback_cache_get (callee_fcn->addr); + if (!entry || !entry->interproc_scope_json || !*entry->interproc_scope_json) { + continue; + } + if (!lift_function_blocks (anal, callee_fcn, ctx, &callee_blocks, true)) { + continue; + } + external_context_json = sleigh_collect_external_context_json (anal, callee_fcn); + if (!external_context_json || (external_context_json[0] != '{' && external_context_json[0] != '[')) { + free (external_context_json); + external_context_json = strdup ("{}"); + } + summary_json = r2sleigh_function_interproc_summary_json_ex ( + ctx, + (const R2ILBlock **)callee_blocks.blocks, + callee_blocks.count, + callee_fcn->addr, + callee_fcn->name, + external_context_json? external_context_json: "{}", + max_iters, + entry->interproc_scope_json); + if (summary_json && *summary_json) { + pj_j (pj, summary_json); + } + r2il_string_free (summary_json); + free (external_context_json); + block_array_free (&callee_blocks); + } + pj_end (pj); + + free (seen_addrs); + seen_addrs = NULL; + seen_count = 0; + seen_cap = 0; + pj_k (pj, "payloads"); pj_a (pj); for (i = 0; i < target_count; i++) { @@ -6633,8 +10747,9 @@ static char *build_type_interproc_scope_json_from_targets( if (!target) { continue; } - callee_fcn = r_anal_get_fcn_in (anal, target, 0); - if (!callee_fcn || !append_unique_ut64 (&seen_addrs, &seen_count, &seen_cap, callee_fcn->addr)) { + callee_fcn = materialize_function_at (anal, target); + if (!callee_fcn || function_exceeds_helper_scope_budget (callee_fcn) + || !append_unique_ut64 (&seen_addrs, &seen_count, &seen_cap, callee_fcn->addr)) { continue; } entry = type_writeback_cache_get (callee_fcn->addr); @@ -6652,18 +10767,24 @@ static char *build_type_interproc_scope_json_from_targets( pj_k (pj, "seeds"); pj_a (pj); for (i = 0; i < target_count; i++) { + RAnalFunction *seed_fcn; ut64 seed_addr = targets[i]; char *seed_name; - if (!seed_addr || !append_unique_ut64 (&seen_addrs, &seen_count, &seen_cap, seed_addr)) { + if (!seed_addr) { + continue; + } + seed_fcn = materialize_function_at (anal, seed_addr); + if (!seed_fcn || function_exceeds_helper_scope_budget (seed_fcn) + || !append_unique_ut64 (&seen_addrs, &seen_count, &seen_cap, seed_fcn->addr)) { continue; } - seed_name = resolve_interproc_seed_name (core, anal, seed_addr); + seed_name = resolve_interproc_seed_name (core, anal, seed_fcn->addr); if (!seed_name || !*seed_name) { free (seed_name); continue; } pj_o (pj); - pj_kn (pj, "id", seed_addr); + pj_kn (pj, "id", seed_fcn->addr); pj_ks (pj, "name", seed_name); pj_end (pj); free (seed_name); @@ -6694,9 +10815,9 @@ static bool warm_type_payload_cache_for_function( char *external_context_json = NULL; char *interproc_scope_json = NULL; char *payload_json = NULL; + ut64 artifact_key = 0; ut64 payload_hash = 0; bool ok = false; - TypeWritebackCacheEntry *entry; if (!core || !anal || !ctx || !fcn) { return false; @@ -6704,19 +10825,16 @@ static bool warm_type_payload_cache_for_function( if (!append_unique_ut64 (seen_addrs, seen_count, seen_cap, fcn->addr)) { return true; } - entry = type_writeback_cache_get (fcn->addr); - if (entry && entry->payload_json && *entry->payload_json) { - return true; - } - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { return false; } direct_targets = collect_type_interproc_direct_targets_from_blocks ( ctx, &blocks, fcn->addr, fcn->name, &direct_target_count); for (i = 0; i < direct_target_count; i++) { - RAnalFunction *callee_fcn = r_anal_get_fcn_in (anal, direct_targets[i], 0); - if (!callee_fcn || callee_fcn->addr == fcn->addr) { + RAnalFunction *callee_fcn = materialize_function_at (anal, direct_targets[i]); + if (!callee_fcn || callee_fcn->addr == fcn->addr + || function_exceeds_helper_scope_budget (callee_fcn)) { continue; } warm_type_payload_cache_for_function (core, anal, ctx, callee_fcn, max_iters, @@ -6728,14 +10846,30 @@ static bool warm_type_payload_cache_for_function( external_context_json = strdup ("{}"); } interproc_scope_json = build_type_interproc_scope_json_from_targets ( - core, anal, direct_targets, direct_target_count); + core, anal, ctx, direct_targets, direct_target_count, max_iters); + artifact_key = r2sleigh_function_analysis_artifact_cache_key ( + ctx, + (const R2ILBlock **)blocks.blocks, + blocks.count, + fcn->addr, + fcn->name, + external_context_json? external_context_json: "{}", + max_iters, + interproc_scope_json? interproc_scope_json: "{}"); payload_json = r2sleigh_infer_type_writeback_json_ex (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, external_context_json? external_context_json: "{}", 1, max_iters, 1, interproc_scope_json? interproc_scope_json: "{}"); if (payload_json && *payload_json) { payload_hash = r_str_hash64 (payload_json); - ok = type_writeback_cache_put (fcn->addr, 0, 0, payload_hash, 0, payload_json); + ok = type_writeback_cache_put ( + fcn->addr, + artifact_key, + 0, + payload_hash, + 0, + payload_json, + interproc_scope_json? interproc_scope_json: "{}"); } r2il_string_free (payload_json); @@ -6751,10 +10885,15 @@ static char *build_type_interproc_scope_json( RAnal *anal, R2ILContext *ctx, RAnalFunction *fcn, - const BlockArray *blocks + const BlockArray *blocks, + int max_iters ) { ut64 *direct_targets = NULL; size_t direct_target_count = 0; + ut64 *seen_addrs = NULL; + size_t seen_count = 0; + size_t seen_cap = 0; + size_t i; char *scope_json; if (!anal || !ctx || !fcn || !blocks) { @@ -6762,9 +10901,19 @@ static char *build_type_interproc_scope_json( } direct_targets = collect_type_interproc_direct_targets_from_blocks ( ctx, blocks, fcn->addr, fcn->name, &direct_target_count); + for (i = 0; i < direct_target_count; i++) { + RAnalFunction *callee_fcn = materialize_function_at (anal, direct_targets[i]); + if (!callee_fcn || callee_fcn->addr == fcn->addr + || function_exceeds_helper_scope_budget (callee_fcn)) { + continue; + } + warm_type_payload_cache_for_function ( + core, anal, ctx, callee_fcn, max_iters, &seen_addrs, &seen_count, &seen_cap); + } scope_json = build_type_interproc_scope_json_from_targets ( - core, anal, direct_targets, direct_target_count); + core, anal, ctx, direct_targets, direct_target_count, max_iters); free (direct_targets); + free (seen_addrs); return scope_json; } @@ -7548,6 +11697,11 @@ static bool sleigh_post_analysis(RAnal *anal) { bool sigwrite_enabled = post_mode != SLEIGH_MODE_FAST; bool type_writeback_enabled = sigwrite_enabled && type_wb_mode != SLEIGH_TYPE_WRITEBACK_OFF; bool sigverify_enabled = false; + ut64 post_start_us = r_time_now_mono (); + ut64 post_budget_us = post_mode == SLEIGH_MODE_FAST + ? SLEIGH_POST_ANALYSIS_FAST_BUDGET_USEC + : SLEIGH_POST_ANALYSIS_BALANCED_BUDGET_USEC; + bool post_budget_exhausted = false; ut64 *type_eligible_addrs = NULL; size_t type_eligible_count = 0; size_t type_eligible_cap = 0; @@ -7587,6 +11741,12 @@ static bool sleigh_post_analysis(RAnal *anal) { RListIter *iter; RAnalFunction *fcn; r_list_foreach (anal->fcns, iter, fcn) { + if (post_budget_us && r_time_now_mono () - post_start_us > post_budget_us) { + post_budget_exhausted = true; + R_LOG_WARN ("r2sleigh: post-analysis budget exhausted after %llu usec; refusing remaining automatic enrichment", + (unsigned long long)(r_time_now_mono () - post_start_us)); + break; + } int bb_count = (fcn && fcn->bbs) ? r_list_length (fcn->bbs) : 0; bool sig_scope_eligible = !sigwrite_focus_only || (focus_callee_addr && fcn && fcn->addr == focus_callee_addr); bool type_scope_eligible = !type_writeback_focus_only || (focus_callee_addr && fcn && fcn->addr == focus_callee_addr); @@ -7621,7 +11781,7 @@ static bool sleigh_post_analysis(RAnal *anal) { if (!fcn || !need_blocks) { continue; } - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { continue; } @@ -7953,6 +12113,7 @@ static bool sleigh_post_analysis(RAnal *anal) { bool signature_arch_eligible = sig_arch_supported; bool callconv_arch_eligible = cc_arch_supported; bool sig_metrics_eligible = signature_arch_eligible && signature_part_eligible; + ut64 artifact_key = 0; ut64 cache_key = 0; ut64 payload_hash = 0; ut64 dep_hash = 0; @@ -7972,16 +12133,28 @@ static bool sleigh_post_analysis(RAnal *anal) { free (external_context_json); external_context_json = strdup ("{}"); } + interproc_scope_json = build_type_interproc_scope_json ( + core, anal, ctx, fcn, &blocks, type_max_iters); + artifact_key = r2sleigh_function_analysis_artifact_cache_key ( + ctx, + (const R2ILBlock **)blocks.blocks, + blocks.count, + fcn->addr, + fcn->name, + external_context_json? external_context_json: "{}", + type_max_iters, + interproc_scope_json? interproc_scope_json: "{}"); if (type_cache_enabled && type_writeback_enabled) { TypeWritebackCacheEntry *cache_entry; dep_hash = compute_callee_dependency_hash (core, anal, fcn); - cache_key = compute_type_cache_key (fcn, external_context_json, + cache_key = compute_type_cache_key (artifact_key, dep_hash, type_wb_mode, type_min_conf, type_rename_min_conf, type_struct_min_conf, type_max_iters); cache_entry = type_writeback_cache_get (fcn->addr); if (cache_entry && cache_entry->key == cache_key) { type_wb.cache_hits++; + free (interproc_scope_json); free (external_context_json); block_array_free (&blocks); continue; @@ -7994,13 +12167,10 @@ static bool sleigh_post_analysis(RAnal *anal) { } } - interproc_scope_json = build_type_interproc_scope_json (core, anal, ctx, fcn, &blocks); payload_json = r2sleigh_infer_type_writeback_json_ex (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, external_context_json? external_context_json: "{}", 1, type_max_iters, 1, interproc_scope_json? interproc_scope_json: "{}"); - free (interproc_scope_json); - interproc_scope_json = NULL; if (!payload_json || !*payload_json) { if (sig_metrics_eligible) { sig_parse_failures++; @@ -8144,7 +12314,14 @@ static bool sleigh_post_analysis(RAnal *anal) { if (type_cache_enabled && type_writeback_enabled) { ut64 applied_hash = type_payload_changed? payload_hash: 0; - if (type_writeback_cache_put (fcn->addr, cache_key, dep_hash, payload_hash, applied_hash, payload_json)) { + if (type_writeback_cache_put ( + fcn->addr, + cache_key, + dep_hash, + payload_hash, + applied_hash, + payload_json, + interproc_scope_json? interproc_scope_json: "{}")) { type_wb.cache_updates++; } } @@ -8175,7 +12352,7 @@ static bool sleigh_post_analysis(RAnal *anal) { block_array_free (&blocks); } - if (xref_enabled) { + if (xref_enabled && !post_budget_exhausted) { ut64 *xref_queue = NULL; size_t xref_queue_count = 0; size_t xref_queue_cap = 0; @@ -8194,6 +12371,12 @@ static bool sleigh_post_analysis(RAnal *anal) { } while (xref_queue_count > 0) { + if (post_budget_us && r_time_now_mono () - post_start_us > post_budget_us) { + post_budget_exhausted = true; + R_LOG_WARN ("r2sleigh: post-analysis xref budget exhausted after %llu usec; refusing remaining automatic xrefs", + (unsigned long long)(r_time_now_mono () - post_start_us)); + break; + } ut64 faddr = xref_queue[--xref_queue_count]; RAnalFunction *xref_fcn_cur = r_anal_get_fcn_in (anal, faddr, 0); BlockArray xref_blocks; @@ -8204,7 +12387,7 @@ static bool sleigh_post_analysis(RAnal *anal) { if (!xref_fcn_cur) { continue; } - if (!lift_function_blocks (anal, xref_fcn_cur, ctx, &xref_blocks)) { + if (!lift_function_blocks (anal, xref_fcn_cur, ctx, &xref_blocks, true)) { continue; } cache_key = compute_xref_cache_key (xref_fcn_cur, &xref_blocks, post_mode); @@ -8230,7 +12413,7 @@ static bool sleigh_post_analysis(RAnal *anal) { qsort (type_eligible_addrs, type_eligible_count, sizeof (ut64), ut64_cmp_asc); } - if (type_writeback_enabled) { + if (type_writeback_enabled && !post_budget_exhausted) { ut64 *queue = NULL; size_t queue_count = 0; size_t queue_cap = 0; @@ -8249,6 +12432,12 @@ static bool sleigh_post_analysis(RAnal *anal) { } } while (queue_count > 0 && iter_idx < type_max_iters) { + if (post_budget_us && r_time_now_mono () - post_start_us > post_budget_us) { + post_budget_exhausted = true; + R_LOG_WARN ("r2sleigh: post-analysis type fixpoint budget exhausted after %llu usec; refusing remaining type propagation", + (unsigned long long)(r_time_now_mono () - post_start_us)); + break; + } ut64 *current = queue; size_t current_count = queue_count; iter_idx++; @@ -8270,6 +12459,7 @@ static bool sleigh_post_analysis(RAnal *anal) { bool type_changed = false; bool sig_or_cc_changed = false; bool summary_changed = false; + ut64 artifact_key = 0; ut64 payload_hash = 0; ut64 dep_hash = 0; ut64 cache_key = 0; @@ -8284,7 +12474,7 @@ static bool sleigh_post_analysis(RAnal *anal) { type_wb.type_fcns_skipped_size++; continue; } - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { continue; } @@ -8293,16 +12483,28 @@ static bool sleigh_post_analysis(RAnal *anal) { free (external_context_json); external_context_json = strdup ("{}"); } + interproc_scope_json = build_type_interproc_scope_json ( + core, anal, ctx, fcn, &blocks, type_max_iters); + artifact_key = r2sleigh_function_analysis_artifact_cache_key ( + ctx, + (const R2ILBlock **)blocks.blocks, + blocks.count, + fcn->addr, + fcn->name, + external_context_json? external_context_json: "{}", + type_max_iters, + interproc_scope_json? interproc_scope_json: "{}"); if (type_cache_enabled) { TypeWritebackCacheEntry *cache_entry; dep_hash = compute_callee_dependency_hash (core, anal, fcn); - cache_key = compute_type_cache_key (fcn, external_context_json, + cache_key = compute_type_cache_key (artifact_key, dep_hash, type_wb_mode, type_min_conf, type_rename_min_conf, type_struct_min_conf, type_max_iters); cache_entry = type_writeback_cache_get (fcn->addr); if (cache_entry && cache_entry->key == cache_key) { type_wb.cache_hits++; + free (interproc_scope_json); free (external_context_json); block_array_free (&blocks); continue; @@ -8315,14 +12517,11 @@ static bool sleigh_post_analysis(RAnal *anal) { } } - interproc_scope_json = build_type_interproc_scope_json (core, anal, ctx, fcn, &blocks); payload_json = r2sleigh_infer_type_writeback_json_ex (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, external_context_json? external_context_json: "{}", (size_t)iter_idx, (size_t)type_max_iters, 0, interproc_scope_json? interproc_scope_json: "{}"); - free (interproc_scope_json); - interproc_scope_json = NULL; if (!payload_json || !*payload_json) { type_wb.payload_missing++; r2il_string_free (payload_json); @@ -8389,7 +12588,14 @@ static bool sleigh_post_analysis(RAnal *anal) { } if (type_cache_enabled) { ut64 applied_hash = (type_changed || sig_or_cc_changed)? payload_hash: 0; - if (type_writeback_cache_put (fcn->addr, cache_key, dep_hash, payload_hash, applied_hash, payload_json)) { + if (type_writeback_cache_put ( + fcn->addr, + cache_key, + dep_hash, + payload_hash, + applied_hash, + payload_json, + interproc_scope_json? interproc_scope_json: "{}")) { type_wb.cache_updates++; } } @@ -8440,8 +12646,8 @@ static bool sleigh_post_analysis(RAnal *anal) { snprintf (type_wb.fixpoint_stop_reason, sizeof (type_wb.fixpoint_stop_reason), type_writeback_enabled? "queue_empty": "off"); } - R_LOG_INFO ("r2sleigh: post-analysis summary fcns=%d xref_cache_hits=%d xref_recomputes=%d xref_dirty_queued=%d type_queue_pops=%d type_fixpoint_converged=%d", - num_fcns, xref_cache_hits, xref_recomputes, xref_dirty_queued, + R_LOG_INFO ("r2sleigh: post-analysis summary fcns=%d budget_exhausted=%d xref_cache_hits=%d xref_recomputes=%d xref_dirty_queued=%d type_queue_pops=%d type_fixpoint_converged=%d", + num_fcns, post_budget_exhausted? 1: 0, xref_cache_hits, xref_recomputes, xref_dirty_queued, type_wb.fixpoint_queue_pops, type_wb.fixpoint_converged); R_LOG_INFO ("r2sleigh: type write-back enabled=%d mode=%d vars_considered=%d vars_applied=%d vars_hint_only=%d vars_low_conf=%d vars_conflict=%d vars_api_verify_fail=%d vars_cmd_fallback_attempted=%d vars_cmd_apply_fail=%d renames_considered=%d renames_applied=%d renames_low_conf=%d renames_conflict=%d rename_generated_guard_skips=%d structs_considered=%d structs_imported=%d structs_low_conf=%d structs_import_fail=%d global_links_considered=%d global_links_applied=%d global_links_low_conf=%d global_links_conflict_skip=%d global_links_existing_preserved=%d global_links_fail=%d payload_missing=%d payload_parse_failures=%d cache_hits=%d cache_misses=%d cache_invalidates=%d cache_updates=%d type_skipped_arch=%d type_skipped_size=%d fixpoint_iters=%d fixpoint_converged=%d fixpoint_queue_pushes=%d fixpoint_queue_pops=%d fixpoint_requeues=%d fixpoint_stop=%s", type_writeback_enabled? 1: 0, (int)type_wb_mode, diff --git a/r2plugin/src/analysis/sym.rs b/r2plugin/src/analysis/sym.rs index c61e0e5..c6bc574 100644 --- a/r2plugin/src/analysis/sym.rs +++ b/r2plugin/src/analysis/sym.rs @@ -1,17 +1,52 @@ use crate::blocks::BlockSlice; use crate::context::require_ctx_view; +use crate::helpers::effective_ptr_bits; use crate::{ArchSpec, R2ILBlock, R2ILContext, parse_addr_name_map}; use serde::Serialize; +use serde_json::json; +use std::any::Any; use std::collections::BTreeMap; use std::collections::HashMap; use std::ffi::{CStr, CString}; +use std::fs::OpenOptions; +use std::io::Write; use std::os::raw::c_char; use std::ptr; +use std::slice; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Mutex, OnceLock}; use z3::{Config, Context}; static MERGE_STATES: AtomicBool = AtomicBool::new(false); +const SYM_PATHS_LIMIT: usize = 32; +const SYM_PATHS_CALL_FREE_MAX_STATES: usize = 16; +const SYM_PATHS_CALL_FREE_MAX_DEPTH: usize = 64; +const SYM_PATHS_CALL_HEAVY_MAX_STATES: usize = 8; +const SYM_PATHS_CALL_HEAVY_MAX_DEPTH: usize = 32; +const SYM_PATHS_TIMEOUT_MS: u64 = 500; +const SYM_PATHS_SOLUTION_LIMIT: usize = 4; + +fn solve_pipeline_debug_enabled() -> bool { + std::env::var_os("R2SLEIGH_DEBUG_SOLVE_PIPELINE").is_some() +} + +fn solve_pipeline_debug_log(message: &str) { + if !solve_pipeline_debug_enabled() { + return; + } + let line = format!("{message}\n"); + if let Some(path) = std::env::var_os("R2SLEIGH_DEBUG_SOLVE_PIPELINE_LOG") { + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { + let _ = file.write_all(line.as_bytes()); + } + return; + } + let _ = std::io::stderr().write_all(line.as_bytes()); +} + +fn solve_pipeline_debug_stage(stage: &str) { + solve_pipeline_debug_log(stage); +} fn merge_states_enabled() -> bool { MERGE_STATES.load(Ordering::Relaxed) @@ -25,6 +60,58 @@ pub struct R2SymContext { error: Option, } +#[repr(C)] +pub struct R2ILFunctionBlocks { + entry_addr: u64, + name: *const c_char, + blocks: *const *const R2ILBlock, + num_blocks: usize, +} + +#[repr(C)] +pub struct R2SymReplayRegister { + name: *const c_char, + value: u64, +} + +#[repr(C)] +pub struct R2SymReplayMemoryWindow { + addr: u64, + bytes: *const u8, + size: usize, + label: *const c_char, +} + +#[repr(C)] +pub struct R2SymReplayRegisterOverlay { + name: *const c_char, + symbol: *const c_char, +} + +#[repr(C)] +pub struct R2SymReplayMemoryOverlay { + addr: u64, + size: u32, + name: *const c_char, +} + +#[repr(C)] +pub struct R2SymReplaySeed { + checkpoint_id: u64, + entry_addr: u64, + registers: *const R2SymReplayRegister, + num_registers: usize, + memory: *const R2SymReplayMemoryWindow, + num_memory: usize, + register_overlays: *const R2SymReplayRegisterOverlay, + num_register_overlays: usize, + memory_overlays: *const R2SymReplayMemoryOverlay, + num_memory_overlays: usize, + tty_fds: *const i32, + num_tty_fds: usize, + skip_sleep_calls: i32, +} + /// Initialize the symbolic execution engine. /// Returns 1 on success, 0 on failure. /// Note: This is an intentional no-op because contexts are created per-state. @@ -92,48 +179,88 @@ pub extern "C" fn r2sym_merge_set_enabled(enabled: i32) { fn sym_default_config() -> r2sym::ExploreConfig { r2sym::ExploreConfig { - max_states: 100, - max_depth: 200, + max_states: 200, + max_depth: 800, merge_states: merge_states_enabled(), - timeout: Some(std::time::Duration::from_secs(5)), + timeout: Some(std::time::Duration::from_secs(20)), ..Default::default() } } +fn sym_default_query_config() -> r2sym::SymQueryConfig { + r2sym::SymQueryConfig { + explore: sym_default_config(), + mode: r2sym::QueryMode::TargetGuided, + summary_profile: r2sym::SummaryProfile::Default, + solve_tactics: r2sym::SolveTacticConfig::default(), + } +} + +fn tune_query_config_for_state( + config: &mut r2sym::SymQueryConfig, + prepared: &r2ssa::SsaArtifact, + initial_state: &r2sym::SymState<'_>, + route: Option<&r2sym::TargetQueryRoutePlan>, +) -> r2sym::QueryExecutionPolicy { + let route = route + .cloned() + .unwrap_or_else(r2sym::TargetQueryRoutePlan::dynamic_fallback); + let policy = r2sym::QueryExecutionPolicy::for_route(config, prepared, initial_state, route); + r2sym::apply_query_execution_policy(config, &policy); + policy +} + +fn sym_paths_query_config(prepared: &r2ssa::SsaArtifact) -> r2sym::SymQueryConfig { + let mut config = sym_default_query_config(); + if prepared.call_sites().by_id.is_empty() { + config.explore.max_states = SYM_PATHS_CALL_FREE_MAX_STATES; + config.explore.max_depth = SYM_PATHS_CALL_FREE_MAX_DEPTH; + } else { + config.explore.max_states = SYM_PATHS_CALL_HEAVY_MAX_STATES; + config.explore.max_depth = SYM_PATHS_CALL_HEAVY_MAX_DEPTH; + } + config.explore.timeout = Some(std::time::Duration::from_millis(SYM_PATHS_TIMEOUT_MS)); + config.explore.max_completed_paths = Some(SYM_PATHS_LIMIT); + config.summary_profile = r2sym::SummaryProfile::PathListing; + config +} + +fn sym_paths_solution_limit(result_count: usize, prepared: &r2ssa::SsaArtifact) -> usize { + if !prepared.call_sites().by_id.is_empty() { + return 0; + } + if result_count <= SYM_PATHS_SOLUTION_LIMIT { + result_count + } else { + 0 + } +} + fn sym_error_json(message: &str) -> *mut c_char { - let payload = format!(r#"{{"error":"{}"}}"#, message); + let payload = json!({ "error": message }).to_string(); CString::new(payload).map_or(ptr::null_mut(), |c| c.into_raw()) } +fn panic_payload_message(payload: &(dyn Any + Send)) -> Option { + payload.downcast_ref::().cloned().or_else(|| { + payload + .downcast_ref::<&'static str>() + .map(|msg| (*msg).to_string()) + }) +} + +fn sym_panic_json(default: &str, payload: Box) -> *mut c_char { + let message = panic_payload_message(payload.as_ref()) + .map(|details| format!("{default}: {details}")) + .unwrap_or_else(|| default.to_string()); + sym_error_json(&message) +} + fn sym_symbol_map() -> &'static Mutex> { static MAP: OnceLock>> = OnceLock::new(); MAP.get_or_init(|| Mutex::new(HashMap::new())) } -fn build_seed_interproc_summary_set( - prepared: &r2ssa::SsaArtifact, - symbol_map: &HashMap, -) -> r2ssa::InterprocSummarySet { - let mut summaries = BTreeMap::new(); - for call in prepared.call_sites().by_id.values() { - let Some(target) = call.direct_target else { - continue; - }; - let Some(name) = symbol_map.get(&target) else { - continue; - }; - let id = r2ssa::InterprocFunctionId(target); - if let Some(summary) = r2ssa::FunctionSemanticSummary::seed_for_name(id, name) { - summaries.insert(id, summary); - } - } - r2ssa::InterprocSummarySet { - root: None, - summaries, - diagnostics: r2ssa::InterprocSummaryDiagnostics::default(), - } -} - #[unsafe(no_mangle)] pub extern "C" fn r2sym_set_symbol_map_json(json: *const c_char) -> i32 { if json.is_null() { @@ -160,9 +287,504 @@ struct SymExecSummary { paths_explored: usize, paths_feasible: usize, paths_pruned: usize, + #[serde(default, skip_serializing_if = "is_zero")] + target_pruned_cfg_unreachable: usize, + #[serde(default, skip_serializing_if = "is_zero")] + target_pruned_summary_contradiction: usize, + #[serde(default, skip_serializing_if = "is_zero")] + target_match_unsat: usize, + #[serde(default, skip_serializing_if = "is_zero")] + runtime_symbolic_breakpoint_forks: usize, + #[serde(default, skip_serializing_if = "is_zero")] + runtime_symbolic_breakpoint_pruned: usize, + #[serde(default, skip_serializing_if = "is_zero")] + runtime_breakpoint_loop_summaries: usize, + #[serde(default, skip_serializing_if = "is_zero")] + runtime_breakpoint_loop_exact_summaries: usize, + #[serde(default, skip_serializing_if = "is_zero")] + runtime_loop_exact_recurrence_summaries: usize, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + runtime_loop_exact_folds: Vec, + #[serde(default, skip_serializing_if = "is_zero")] + runtime_loop_refusals: usize, + #[serde(default, skip_serializing_if = "is_zero")] + runtime_loop_unknown_carried_state: usize, + #[serde(default, skip_serializing_if = "is_zero")] + runtime_loop_budget_residuals: usize, max_depth: usize, states_explored: usize, + sat_queries: usize, + sat_cache_hits: usize, + sat_cache_misses: usize, + solve_calls: usize, + solve_unsat_shortcuts: usize, time_ms: u64, + #[serde( + default, + skip_serializing_if = "r2ssa::AssumptionUsageReport::is_empty" + )] + assumption_usage: r2ssa::AssumptionUsageReport, + #[serde(default, skip_serializing_if = "is_false")] + assumption_conditioned: bool, + #[serde(default, skip_serializing_if = "is_false")] + summary_conditioned: bool, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + runtime_diagnostics: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + semantic: Option, +} + +fn is_zero(value: &usize) -> bool { + *value == 0 +} + +#[derive(Debug, Serialize, Clone)] +pub(crate) struct CompiledSemanticInfo { + pub(crate) schema_version: u32, + pub(crate) stage: String, + pub(crate) granularity: String, + pub(crate) execution: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) seed_mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) replay_seed_fingerprint: Option, + pub(crate) query_plan: r2sym::QueryPlan, + pub(crate) type_plan: r2sym::TypePlan, + pub(crate) decompile_plan: r2sym::DecompilePlan, + pub(crate) slice_class: String, + pub(crate) residual_reasons: Vec, + pub(crate) ambiguous_target_count: usize, + pub(crate) ambiguous_targets: Vec, + pub(crate) closure_functions: usize, + pub(crate) helper_functions: usize, + pub(crate) derived_summaries: usize, + pub(crate) summary_attempted: usize, + pub(crate) summary_budget_exhausted: usize, + pub(crate) summary_scc_count: usize, + pub(crate) region_count: usize, + pub(crate) control_region_count: usize, + pub(crate) memory_region_count: usize, + pub(crate) compiled_condition_count: usize, + pub(crate) exact_compiled_condition_count: usize, + pub(crate) actionable_compiled_condition_count: usize, + pub(crate) branches_pruned: usize, + pub(crate) branches_unknown: usize, + pub(crate) skipped_large_cfg: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) interpreter: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) vm_step: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) vm_transfer: Option, + pub(crate) cache_hit: bool, +} + +#[derive(Debug, Serialize, Clone)] +pub(crate) struct InterpreterDispatchInfo { + pub(crate) kind: String, + pub(crate) dispatch_header: String, + pub(crate) dispatch_targets: usize, + pub(crate) selector: Option, + pub(crate) back_edges: usize, + pub(crate) score: i32, +} + +#[derive(Debug, Serialize, Clone)] +pub(crate) struct VmStateUpdateInfo { + pub(crate) output: String, + pub(crate) expr: String, + pub(crate) value: String, + pub(crate) exact: bool, + pub(crate) confidence: String, +} + +#[derive(Debug, Serialize, Clone)] +pub(crate) struct VmGuardConditionInfo { + pub(crate) expr: String, + pub(crate) value: String, + pub(crate) expect_nonzero: bool, + pub(crate) exact: bool, + pub(crate) confidence: String, +} + +#[derive(Debug, Serialize, Clone)] +pub(crate) struct VmGuardedExitInfo { + pub(crate) target: String, + pub(crate) guard: VmGuardConditionInfo, +} + +#[derive(Debug, Serialize, Clone)] +pub(crate) struct VmMemoryConditionInfo { + pub(crate) region: String, + pub(crate) offset_lo: i64, + pub(crate) offset_hi: i64, + pub(crate) size: u32, + pub(crate) exact_offset: bool, + pub(crate) confidence: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) binding: Option, + pub(crate) expr: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) value_expr: Option, + pub(crate) exact_value: bool, +} + +#[derive(Debug, Serialize, Clone)] +pub(crate) struct VmTransferArmInfo { + pub(crate) handler_target: String, + pub(crate) case_values: Vec, + pub(crate) region_blocks: Vec, + pub(crate) exit_targets: Vec, + pub(crate) exit_guards: Vec, + pub(crate) state_updates: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) selector_update: Option, + pub(crate) memory_reads: Vec, + pub(crate) memory_writes: Vec, + pub(crate) residual_guards: bool, + pub(crate) residual_memory_effects: bool, + pub(crate) exact: bool, + pub(crate) confidence: String, + pub(crate) redispatch: bool, + pub(crate) may_return: bool, + pub(crate) truncated: bool, +} + +#[derive(Debug, Serialize, Clone)] +pub(crate) struct VmStepSummaryInfo { + pub(crate) kind: String, + pub(crate) loop_header: String, + pub(crate) dispatch_header: String, + pub(crate) selector: Option, + pub(crate) dispatch_targets: Vec, + pub(crate) default_target: Option, + pub(crate) case_values_by_target: BTreeMap>, + pub(crate) loop_latches: Vec, + pub(crate) state_inputs: Vec, + pub(crate) state_outputs: Vec, + pub(crate) step_blocks: Vec, + pub(crate) handler_regions: BTreeMap>, + pub(crate) handler_state_inputs: BTreeMap>, + pub(crate) handler_state_outputs: BTreeMap>, + pub(crate) handler_state_updates: BTreeMap>, + pub(crate) handler_exit_guards: BTreeMap>, + pub(crate) handler_memory_read_effects: BTreeMap>, + pub(crate) handler_memory_write_effects: BTreeMap>, + pub(crate) handler_memory_reads: BTreeMap, + pub(crate) handler_memory_writes: BTreeMap, + pub(crate) handler_calls: BTreeMap, + pub(crate) handler_conditional_branches: BTreeMap, + pub(crate) handler_exit_targets: BTreeMap>, + pub(crate) redispatch_handlers: Vec, + pub(crate) returning_handlers: Vec, + pub(crate) truncated_handlers: Vec, + pub(crate) transfers: Vec, +} + +fn render_vm_value_expr(value: &r2sym::VmValueExpr) -> String { + match value { + r2sym::VmValueExpr::Const(value) => format!("0x{value:x}"), + r2sym::VmValueExpr::Var(name) | r2sym::VmValueExpr::Expr(name) => name.clone(), + r2sym::VmValueExpr::Unary { op, arg } => { + let op = match op { + r2sym::VmUnaryOp::Neg => "-", + r2sym::VmUnaryOp::BitNot => "~", + r2sym::VmUnaryOp::BoolNot => "!", + }; + format!("({}{})", op, render_vm_value_expr(arg)) + } + r2sym::VmValueExpr::Binary { op, lhs, rhs } => { + let op = match op { + r2sym::VmBinaryOp::Add => "+", + r2sym::VmBinaryOp::Sub => "-", + r2sym::VmBinaryOp::Mul => "*", + r2sym::VmBinaryOp::Div => "/", + r2sym::VmBinaryOp::Rem => "%", + r2sym::VmBinaryOp::And => "&", + r2sym::VmBinaryOp::Or => "|", + r2sym::VmBinaryOp::Xor => "^", + r2sym::VmBinaryOp::Shl => "<<", + r2sym::VmBinaryOp::LShr | r2sym::VmBinaryOp::AShr => ">>", + r2sym::VmBinaryOp::Eq => "==", + r2sym::VmBinaryOp::Ne => "!=", + r2sym::VmBinaryOp::Lt | r2sym::VmBinaryOp::SLt => "<", + r2sym::VmBinaryOp::Le | r2sym::VmBinaryOp::SLe => "<=", + r2sym::VmBinaryOp::BoolAnd => "&&", + r2sym::VmBinaryOp::BoolOr => "||", + }; + format!( + "({} {} {})", + render_vm_value_expr(lhs), + op, + render_vm_value_expr(rhs) + ) + } + } +} + +fn render_semantic_confidence(confidence: r2sym::SemanticConfidence) -> String { + match confidence { + r2sym::SemanticConfidence::Exact => "exact", + r2sym::SemanticConfidence::Likely => "likely", + r2sym::SemanticConfidence::Heuristic => "heuristic", + r2sym::SemanticConfidence::Residual => "residual", + } + .to_string() +} + +fn vm_state_update_info_from_sym(update: &r2sym::VmStateUpdate) -> VmStateUpdateInfo { + VmStateUpdateInfo { + output: update.output.clone(), + expr: update.expr.clone(), + value: render_vm_value_expr(&update.value), + exact: update.exact, + confidence: render_semantic_confidence(update.confidence()), + } +} + +fn vm_guard_condition_info_from_sym(guard: &r2sym::VmGuardCondition) -> VmGuardConditionInfo { + VmGuardConditionInfo { + expr: guard.expr.clone(), + value: render_vm_value_expr(&guard.value), + expect_nonzero: guard.expect_nonzero, + exact: guard.exact, + confidence: render_semantic_confidence(guard.confidence()), + } +} + +fn vm_guarded_exit_info_from_sym(guarded: &r2sym::VmGuardedExit) -> VmGuardedExitInfo { + VmGuardedExitInfo { + target: format!("0x{:x}", guarded.target), + guard: vm_guard_condition_info_from_sym(&guarded.guard), + } +} + +fn vm_memory_condition_info_from_sym( + condition: &r2sym::VmMemoryCondition, +) -> VmMemoryConditionInfo { + VmMemoryConditionInfo { + region: format!( + "{}:{}#{}", + match condition.region.kind { + r2sym::MemoryRegionKind::Stack => "stack", + r2sym::MemoryRegionKind::Global => "global", + r2sym::MemoryRegionKind::Input => "input", + r2sym::MemoryRegionKind::Heap => "heap", + r2sym::MemoryRegionKind::Replay => "replay", + r2sym::MemoryRegionKind::EscapedUnknown => "unknown", + }, + condition.region.name, + condition.region.id + ), + offset_lo: condition.offset_lo, + offset_hi: condition.offset_hi, + size: condition.size, + exact_offset: condition.exact_offset, + confidence: render_semantic_confidence(condition.confidence()), + binding: condition.binding.clone(), + expr: condition.expr.clone(), + value_expr: condition.value_expr.clone(), + exact_value: condition.exact_value, + } +} + +fn vm_transfer_arm_info_from_sym(transfer: &r2sym::VmTransferArm) -> VmTransferArmInfo { + VmTransferArmInfo { + handler_target: format!("0x{:x}", transfer.handler_target), + case_values: transfer.case_values.clone(), + region_blocks: transfer + .region_blocks + .iter() + .map(|addr| format!("0x{addr:x}")) + .collect(), + exit_targets: transfer + .exit_targets + .iter() + .map(|addr| format!("0x{addr:x}")) + .collect(), + exit_guards: transfer + .exit_guards + .iter() + .map(vm_guarded_exit_info_from_sym) + .collect(), + state_updates: transfer + .state_updates + .iter() + .map(vm_state_update_info_from_sym) + .collect(), + selector_update: transfer + .selector_update + .as_ref() + .map(vm_state_update_info_from_sym), + memory_reads: transfer + .memory_reads + .iter() + .map(vm_memory_condition_info_from_sym) + .collect(), + memory_writes: transfer + .memory_writes + .iter() + .map(vm_memory_condition_info_from_sym) + .collect(), + residual_guards: transfer.residual_guards, + residual_memory_effects: transfer.residual_memory_effects, + exact: transfer.exact, + confidence: render_semantic_confidence(transfer.confidence()), + redispatch: transfer.redispatch, + may_return: transfer.may_return, + truncated: transfer.truncated, + } +} + +fn vm_step_summary_info_from_sym(vm_step: &r2sym::VmStepSummary) -> VmStepSummaryInfo { + VmStepSummaryInfo { + kind: match vm_step.kind { + r2sym::InterpreterKind::SwitchDispatch => "switch_dispatch".to_string(), + r2sym::InterpreterKind::IndirectDispatch => "indirect_dispatch".to_string(), + }, + loop_header: format!("0x{:x}", vm_step.loop_header), + dispatch_header: format!("0x{:x}", vm_step.dispatch_header), + selector: vm_step.selector.clone(), + dispatch_targets: vm_step + .dispatch_targets + .iter() + .map(|addr| format!("0x{addr:x}")) + .collect(), + default_target: vm_step.default_target.map(|addr| format!("0x{addr:x}")), + case_values_by_target: vm_step + .case_values_by_target + .iter() + .map(|(target, values)| (format!("0x{target:x}"), values.clone())) + .collect(), + loop_latches: vm_step + .loop_latches + .iter() + .map(|addr| format!("0x{addr:x}")) + .collect(), + state_inputs: vm_step.state_inputs.clone(), + state_outputs: vm_step.state_outputs.clone(), + step_blocks: vm_step + .step_blocks + .iter() + .map(|addr| format!("0x{addr:x}")) + .collect(), + handler_regions: vm_step + .handler_regions + .iter() + .map(|(target, blocks)| { + ( + format!("0x{target:x}"), + blocks.iter().map(|addr| format!("0x{addr:x}")).collect(), + ) + }) + .collect(), + handler_state_inputs: vm_step + .handler_state_inputs + .iter() + .map(|(target, inputs)| (format!("0x{target:x}"), inputs.clone())) + .collect(), + handler_state_outputs: vm_step + .handler_state_outputs + .iter() + .map(|(target, outputs)| (format!("0x{target:x}"), outputs.clone())) + .collect(), + handler_state_updates: vm_step + .handler_state_updates + .iter() + .map(|(target, updates)| { + ( + format!("0x{target:x}"), + updates.iter().map(vm_state_update_info_from_sym).collect(), + ) + }) + .collect(), + handler_exit_guards: vm_step + .handler_exit_guards + .iter() + .map(|(target, guards)| { + ( + format!("0x{target:x}"), + guards.iter().map(vm_guarded_exit_info_from_sym).collect(), + ) + }) + .collect(), + handler_memory_read_effects: vm_step + .handler_memory_read_effects + .iter() + .map(|(target, conditions)| { + ( + format!("0x{target:x}"), + conditions + .iter() + .map(vm_memory_condition_info_from_sym) + .collect(), + ) + }) + .collect(), + handler_memory_write_effects: vm_step + .handler_memory_write_effects + .iter() + .map(|(target, conditions)| { + ( + format!("0x{target:x}"), + conditions + .iter() + .map(vm_memory_condition_info_from_sym) + .collect(), + ) + }) + .collect(), + handler_memory_reads: vm_step + .handler_memory_reads + .iter() + .map(|(target, count)| (format!("0x{target:x}"), *count)) + .collect(), + handler_memory_writes: vm_step + .handler_memory_writes + .iter() + .map(|(target, count)| (format!("0x{target:x}"), *count)) + .collect(), + handler_calls: vm_step + .handler_calls + .iter() + .map(|(target, count)| (format!("0x{target:x}"), *count)) + .collect(), + handler_conditional_branches: vm_step + .handler_conditional_branches + .iter() + .map(|(target, count)| (format!("0x{target:x}"), *count)) + .collect(), + handler_exit_targets: vm_step + .handler_exit_targets + .iter() + .map(|(target, exits)| { + ( + format!("0x{target:x}"), + exits.iter().map(|addr| format!("0x{addr:x}")).collect(), + ) + }) + .collect(), + redispatch_handlers: vm_step + .redispatch_handlers + .iter() + .map(|addr| format!("0x{addr:x}")) + .collect(), + returning_handlers: vm_step + .returning_handlers + .iter() + .map(|addr| format!("0x{addr:x}")) + .collect(), + truncated_handlers: vm_step + .truncated_handlers + .iter() + .map(|addr| format!("0x{addr:x}")) + .collect(), + transfers: vm_step + .transfers + .iter() + .map(vm_transfer_arm_info_from_sym) + .collect(), + } } #[derive(Serialize)] @@ -170,7 +792,7 @@ struct SymStateInfo { pc: u64, depth: usize, num_constraints: usize, - registers: std::collections::HashMap, + registers: BTreeMap, } #[derive(Serialize)] @@ -187,8 +809,21 @@ struct PathInfo { #[derive(Serialize)] struct PathSolution { - inputs: std::collections::HashMap, - registers: std::collections::HashMap, + inputs: BTreeMap, + registers: BTreeMap, +} + +#[derive(Serialize)] +struct RunPathSolution { + inputs: BTreeMap, + input_buffers: BTreeMap, + registers: BTreeMap, +} + +#[derive(Serialize)] +struct BufferSolution { + hex: String, + ascii: String, } #[derive(Serialize)] @@ -197,6 +832,8 @@ struct SymTargetExploreResult { target: String, matched_paths: usize, stats: SymExecSummary, + #[serde(skip_serializing_if = "Option::is_none")] + target_query: Option, paths: Vec, } @@ -204,203 +841,1888 @@ struct SymTargetExploreResult { struct SymTargetSolveResult { entry: String, target: String, + status: String, matched_paths: usize, found: bool, + target_reached_under_model: bool, + candidate_solution_verified: bool, + residual_reasons: Vec, + model_validation: ModelValidationInfo, + witness: SolveWitnessInfo, stats: SymExecSummary, #[serde(skip_serializing_if = "Option::is_none")] + target_query: Option, + #[serde(skip_serializing_if = "Option::is_none")] selected_path: Option, } -fn path_solution_from_result<'ctx>( - explorer: &r2sym::PathExplorer<'ctx>, - result: &r2sym::PathResult<'ctx>, -) -> Option { - if !result.feasible { - return None; - } - explorer.solve_path(result).map(|solved| PathSolution { - inputs: solved - .inputs - .into_iter() - .map(|(k, v)| (k, format!("0x{:x}", v))) - .collect(), - registers: solved - .registers - .into_iter() - .filter(|(name, _)| !name.starts_with("tmp:") && !name.contains("_0")) - .map(|(k, v)| (k, format!("0x{:x}", v))) - .collect(), - }) +#[derive(Serialize)] +struct ModelValidationInfo { + status: String, + #[serde(skip_serializing_if = "Option::is_none")] + reason: Option, } -fn path_info_from_result<'ctx>( - path_id: usize, - result: &r2sym::PathResult<'ctx>, - explorer: &r2sym::PathExplorer<'ctx>, -) -> PathInfo { - PathInfo { - path_id, - feasible: result.feasible, - depth: result.depth, - exit_status: format!("{:?}", result.exit_status), - final_pc: format!("0x{:x}", result.final_pc()), - num_constraints: result.num_constraints(), - solution: path_solution_from_result(explorer, result), - } +#[derive(Serialize)] +struct SolveWitnessInfo { + target: String, + #[serde(skip_serializing_if = "Option::is_none")] + selected_path_index: Option, + #[serde(skip_serializing_if = "Option::is_none")] + final_pc: Option, + #[serde(skip_serializing_if = "Option::is_none")] + candidate: Option, + evidence: EvidenceSummaryInfo, + verification_requirement: VerificationRequirementInfo, + proven: bool, } -fn build_symbolic_prepared( - blocks: &[R2ILBlock], - arch: Option<&ArchSpec>, -) -> Option { - r2ssa::SsaArtifact::for_symbolic(blocks, arch) +#[derive(Serialize)] +struct SolveCandidateShapeInfo { + input_scalars: usize, + input_buffers: usize, + registers: usize, + memory_regions: usize, + concrete_assignments: usize, + final_pc: String, + num_constraints: usize, + #[serde(skip_serializing_if = "Option::is_none")] + generation: Option, } -fn symbol_map_snapshot() -> HashMap { - sym_symbol_map() - .lock() - .map(|map| map.clone()) - .unwrap_or_default() +#[derive(Serialize)] +struct SolveCandidateGenerationInfo { + kind: String, + reason: String, + constrained_bytes: usize, } -#[unsafe(no_mangle)] -pub extern "C" fn r2sym_function( - ctx: *const R2ILContext, - blocks: *const *const R2ILBlock, - num_blocks: usize, - entry_addr: u64, -) -> *mut c_char { - let Some(ctx_view) = require_ctx_view(ctx) else { - return ptr::null_mut(); - }; - let Some(blocks) = (unsafe { BlockSlice::from_ffi(blocks, num_blocks) }) else { - return ptr::null_mut(); - }; - - let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch) { - Some(prepared) => prepared, - None => return ptr::null_mut(), - }; - let symbol_map = symbol_map_snapshot(); - let z3_ctx = Context::thread_local(); +#[derive(Serialize)] +struct EvidenceSummaryInfo { + precision: String, + requires_replay: bool, + reasons: Vec, + final_constraint_precision: String, + exact_final_constraints: usize, + model_conditioned_final_constraints: usize, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + exact_loop_recurrences: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + exact_loop_folds: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + tactics: Vec, +} - let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); - r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); - let mut explorer = r2sym::PathExplorer::with_config(&z3_ctx, sym_default_config()); - if let Some(arch) = ctx_view.arch - && let Some(registry) = r2sym::SummaryRegistry::with_core_for_arch(arch) - { - let interproc = build_seed_interproc_summary_set(&prepared, &symbol_map); - let _ = - registry.install_known_symbols_for_function(&mut explorer, &prepared, &symbol_map); - let _ = registry.install_interproc_summaries_for_function( - &mut explorer, - &prepared, - &interproc, - &symbol_map, - ); - } - let results = explorer.explore(&prepared, initial_state); - let stats = explorer.stats().clone(); - (results, stats) - })); +#[derive(Serialize)] +struct VerificationRequirementInfo { + status: String, + reasons: Vec, +} - let (results, stats) = match explore_result { - Ok(r) => r, - Err(_) => { - let error_msg = r#"{"error": "symbolic execution failed (z3 context error)"}"#; - return CString::new(error_msg).map_or(ptr::null_mut(), |c| c.into_raw()); - } - }; +#[derive(Serialize, Clone)] +struct ExactLoopFoldInfo { + header: String, + exit_target: String, + iterations: u64, + accumulator: String, + bits: u32, + operation: String, + memory: LoopMemoryTermInfo, +} - let feasible_count = results.iter().filter(|r| r.feasible).count(); - let summary = SymExecSummary { - paths_explored: stats.paths_completed, - paths_feasible: feasible_count, - paths_pruned: stats.paths_pruned, - max_depth: stats.max_depth_reached, +#[derive(Serialize, Clone)] +struct ExactLoopRecurrenceInfo { + header: String, + exit_target: String, + iterations: u64, + accumulator: String, + bits: u32, + kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + constant: Option, + #[serde(skip_serializing_if = "Option::is_none")] + multiplier: Option, + #[serde(skip_serializing_if = "Option::is_none")] + addend: Option, + #[serde(skip_serializing_if = "Option::is_none")] + direction: Option, + #[serde(skip_serializing_if = "Option::is_none")] + amount: Option, + #[serde(skip_serializing_if = "Option::is_none")] + operation: Option, + #[serde(skip_serializing_if = "Option::is_none")] + memory: Option, +} + +#[derive(Serialize, Clone)] +struct LoopMemoryTermInfo { + kind: String, + addr: String, + bytes: u32, + #[serde(skip_serializing_if = "Option::is_none")] + base: Option, + #[serde(skip_serializing_if = "Option::is_none")] + stride: Option, + #[serde(skip_serializing_if = "Option::is_none")] + region: Option, + #[serde(skip_serializing_if = "Option::is_none")] + region_base: Option, + #[serde(skip_serializing_if = "Option::is_none")] + region_size: Option, +} + +#[derive(Serialize, Clone)] +struct SolveTacticInfo { + kind: String, + status: String, + reason: String, + recurrence: ExactLoopRecurrenceInfo, +} + +#[derive(Serialize, Clone)] +struct TargetQueryInfo { + plan: r2sym::TargetQueryPlan, + route: r2sym::TargetQueryRoutePlan, +} + +#[derive(Serialize)] +struct SymRunPathInfo { + path_id: usize, + feasible: bool, + depth: usize, + exit_status: String, + final_pc: String, + num_constraints: usize, + #[serde(skip_serializing_if = "Option::is_none")] + solution: Option, +} + +#[derive(Serialize)] +struct SymRunStashCounts { + found: usize, + avoided: usize, + unsat: usize, + errored: usize, + completed: usize, +} + +#[derive(Serialize)] +struct SymRunResult { + entry: String, + spec: r2sym::ExplorationSpec, + stats: SymExecSummary, + stash_counts: SymRunStashCounts, + found_paths: Vec, + diagnostics: Vec, +} + +fn is_false(value: &bool) -> bool { + !*value +} + +fn build_sym_exec_summary( + stats: &r2sym::path::ExploreStats, + solver_stats: &r2sym::SolverStats, + paths_feasible: usize, + assumption_usage: r2ssa::AssumptionUsageReport, + assumption_conditioned: bool, + summary_conditioned: bool, + semantic: Option<&r2sym::SemanticArtifact>, +) -> SymExecSummary { + build_sym_exec_summary_with_semantic_info( + stats, + solver_stats, + paths_feasible, + assumption_usage, + assumption_conditioned, + summary_conditioned, + semantic.map(compiled_semantic_info), + ) +} + +fn build_sym_exec_summary_with_semantic_info( + stats: &r2sym::path::ExploreStats, + solver_stats: &r2sym::SolverStats, + paths_feasible: usize, + assumption_usage: r2ssa::AssumptionUsageReport, + assumption_conditioned: bool, + summary_conditioned: bool, + semantic: Option, +) -> SymExecSummary { + let mut runtime_diagnostics = Vec::new(); + if stats.runtime_missing_exception_handler > 0 { + runtime_diagnostics.push("missing_exception_handler".to_string()); + } + if stats.runtime_missing_materialized_code > 0 { + runtime_diagnostics.push("missing_runtime_materialized_code".to_string()); + } + if stats.runtime_missing_continuation_seed > 0 { + runtime_diagnostics.push("missing_continuation_seed".to_string()); + } + if stats.runtime_region_provenance_unknown > 0 { + runtime_diagnostics.push("runtime_region_provenance_unknown".to_string()); + } + if stats.timed_out && stats.runtime_symbolic_breakpoint_forks > 0 { + runtime_diagnostics.push("runtime_symbolic_breakpoint_budget_exhausted".to_string()); + } + if stats.runtime_breakpoint_loop_summaries > 0 { + runtime_diagnostics.push("runtime_breakpoint_loop_summary_residual".to_string()); + } + if stats.runtime_breakpoint_loop_exact_summaries > 0 { + runtime_diagnostics.push("runtime_breakpoint_loop_summary_exact".to_string()); + } + if stats.runtime_loop_exact_recurrence_summaries > 0 { + runtime_diagnostics.push("runtime_loop_exact_recurrence_summary".to_string()); + } + if stats.runtime_loop_unknown_carried_state > 0 { + runtime_diagnostics.push("runtime_loop_unknown_carried_state".to_string()); + } + if stats.runtime_loop_budget_residuals > 0 { + runtime_diagnostics.push("runtime_loop_iteration_budget".to_string()); + } + if stats.runtime_loop_refusals > 0 { + runtime_diagnostics.push("runtime_loop_refused".to_string()); + } + SymExecSummary { + paths_explored: stats.paths_completed, + paths_feasible, + paths_pruned: stats.paths_pruned, + target_pruned_cfg_unreachable: stats.target_pruned_cfg_unreachable, + target_pruned_summary_contradiction: stats.target_pruned_summary_contradiction, + target_match_unsat: stats.target_match_unsat, + runtime_symbolic_breakpoint_forks: stats.runtime_symbolic_breakpoint_forks, + runtime_symbolic_breakpoint_pruned: stats.runtime_symbolic_breakpoint_pruned, + runtime_breakpoint_loop_summaries: stats.runtime_breakpoint_loop_summaries, + runtime_breakpoint_loop_exact_summaries: stats.runtime_breakpoint_loop_exact_summaries, + runtime_loop_exact_recurrence_summaries: stats.runtime_loop_exact_recurrence_summaries, + runtime_loop_exact_folds: stats + .runtime_loop_exact_folds + .iter() + .map(exact_loop_fold_info) + .collect(), + runtime_loop_refusals: stats.runtime_loop_refusals, + runtime_loop_unknown_carried_state: stats.runtime_loop_unknown_carried_state, + runtime_loop_budget_residuals: stats.runtime_loop_budget_residuals, + max_depth: stats.max_depth_reached, states_explored: stats.states_explored, + sat_queries: solver_stats.sat_queries, + sat_cache_hits: solver_stats.sat_cache_hits, + sat_cache_misses: solver_stats.sat_cache_misses, + solve_calls: solver_stats.solve_calls, + solve_unsat_shortcuts: solver_stats.solve_unsat_shortcuts, time_ms: stats.total_time.as_millis() as u64, + assumption_usage, + assumption_conditioned, + summary_conditioned, + runtime_diagnostics, + semantic, + } +} + +fn target_query_info(route: &r2sym::TargetQueryRoutePlan) -> TargetQueryInfo { + TargetQueryInfo { + plan: route.target_plan.clone(), + route: route.clone(), + } +} + +fn solve_status_string(status: r2sym::SolveStatus) -> String { + match status { + r2sym::SolveStatus::Solved => "solved", + r2sym::SolveStatus::Candidate => "candidate", + r2sym::SolveStatus::ResidualReachable => "residual_reachable", + r2sym::SolveStatus::Unverified => "unverified", + r2sym::SolveStatus::Unsat => "unsat", + r2sym::SolveStatus::Unknown => "unknown", + r2sym::SolveStatus::BudgetExhausted => "budget_exhausted", + } + .to_string() +} + +fn solve_found(status: r2sym::SolveStatus) -> bool { + matches!(status, r2sym::SolveStatus::Solved) +} + +fn model_validation_info(validation: &r2sym::ModelValidation) -> ModelValidationInfo { + match validation { + r2sym::ModelValidation::NotRequired => ModelValidationInfo { + status: "not_required".to_string(), + reason: None, + }, + r2sym::ModelValidation::Verified => ModelValidationInfo { + status: "verified".to_string(), + reason: None, + }, + r2sym::ModelValidation::Failed { reason } => ModelValidationInfo { + status: "failed".to_string(), + reason: Some(reason.clone()), + }, + r2sym::ModelValidation::Unavailable { reason } => ModelValidationInfo { + status: "unavailable".to_string(), + reason: Some(reason.clone()), + }, + } +} + +fn fact_precision_string(precision: r2sym::FactPrecision) -> String { + match precision { + r2sym::FactPrecision::Unknown => "unknown", + r2sym::FactPrecision::UnderApprox => "under_approx", + r2sym::FactPrecision::Residual => "residual", + r2sym::FactPrecision::OverApprox => "over_approx", + r2sym::FactPrecision::Exact => "exact", + } + .to_string() +} + +fn verification_requirement_info( + requirement: &r2sym::VerificationRequirement, +) -> VerificationRequirementInfo { + match requirement { + r2sym::VerificationRequirement::NotRequired => VerificationRequirementInfo { + status: "not_required".to_string(), + reasons: Vec::new(), + }, + r2sym::VerificationRequirement::Required { reasons } => VerificationRequirementInfo { + status: "required".to_string(), + reasons: reasons.clone(), + }, + r2sym::VerificationRequirement::Refused { reasons } => VerificationRequirementInfo { + status: "refused".to_string(), + reasons: reasons.clone(), + }, + } +} + +fn loop_memory_term_kind_string(kind: r2sym::LoopMemoryTermKind) -> String { + match kind { + r2sym::LoopMemoryTermKind::TableRead => "table_read", + r2sym::LoopMemoryTermKind::InputRead => "input_read", + r2sym::LoopMemoryTermKind::RuntimeBlobRead => "runtime_blob_read", + r2sym::LoopMemoryTermKind::Unknown => "unknown", + } + .to_string() +} + +fn loop_fold_operation_string(operation: r2sym::LoopFoldOperation) -> String { + match operation { + r2sym::LoopFoldOperation::Add => "add", + r2sym::LoopFoldOperation::Xor => "xor", + } + .to_string() +} + +fn loop_rotate_direction_string(direction: r2sym::LoopRotateDirection) -> String { + match direction { + r2sym::LoopRotateDirection::Left => "left", + r2sym::LoopRotateDirection::Right => "right", + } + .to_string() +} + +fn solve_tactic_kind_string(kind: r2sym::SolveTacticKind) -> String { + match kind { + r2sym::SolveTacticKind::XorFoldPreimage => "xor_fold_preimage", + r2sym::SolveTacticKind::AddFoldPreimage => "add_fold_preimage", + r2sym::SolveTacticKind::RotateXorRecurrence => "rotate_xor_recurrence", + r2sym::SolveTacticKind::RotateAddRecurrence => "rotate_add_recurrence", + r2sym::SolveTacticKind::ConcreteTableFold => "concrete_table_fold", + r2sym::SolveTacticKind::RuntimeBlobFold => "runtime_blob_fold", + } + .to_string() +} + +fn solve_tactic_status_string(status: r2sym::SolveTacticStatus) -> String { + match status { + r2sym::SolveTacticStatus::Available => "available", + r2sym::SolveTacticStatus::EvidenceOnly => "evidence_only", + } + .to_string() +} + +fn loop_memory_term_info(term: &r2sym::LoopMemoryTerm) -> LoopMemoryTermInfo { + LoopMemoryTermInfo { + kind: loop_memory_term_kind_string(term.kind), + addr: term.addr.clone(), + bytes: term.bytes, + base: term.base.map(|addr| format!("0x{addr:x}")), + stride: term.stride, + region: term.region.clone(), + region_base: term.region_base.map(|addr| format!("0x{addr:x}")), + region_size: term.region_size, + } +} + +fn exact_loop_fold_info(fold: &r2sym::ExactLoopFoldEvidence) -> ExactLoopFoldInfo { + ExactLoopFoldInfo { + header: format!("0x{:x}", fold.header), + exit_target: format!("0x{:x}", fold.exit_target), + iterations: fold.iterations, + accumulator: fold.accumulator.clone(), + bits: fold.bits, + operation: loop_fold_operation_string(fold.operation), + memory: loop_memory_term_info(&fold.term), + } +} + +fn exact_loop_recurrence_info( + recurrence: &r2sym::ExactLoopRecurrenceEvidence, +) -> ExactLoopRecurrenceInfo { + let (kind, constant, multiplier, addend, direction, amount, operation, memory) = + match &recurrence.kind { + r2sym::ExactLoopRecurrenceKind::AddConst(value) => ( + "add_const".to_string(), + Some(format!("0x{value:x}")), + None, + None, + None, + None, + None, + None, + ), + r2sym::ExactLoopRecurrenceKind::SubConst(value) => ( + "sub_const".to_string(), + Some(format!("0x{value:x}")), + None, + None, + None, + None, + None, + None, + ), + r2sym::ExactLoopRecurrenceKind::AffineConst { multiplier, addend } => ( + "affine_const".to_string(), + None, + Some(format!("0x{multiplier:x}")), + Some(format!("0x{addend:x}")), + None, + None, + None, + None, + ), + r2sym::ExactLoopRecurrenceKind::XorConst(value) => ( + "xor_const".to_string(), + Some(format!("0x{value:x}")), + None, + None, + None, + None, + None, + None, + ), + r2sym::ExactLoopRecurrenceKind::Fold { operation, term } => ( + "fold".to_string(), + None, + None, + None, + None, + None, + Some(loop_fold_operation_string(*operation)), + Some(loop_memory_term_info(term)), + ), + r2sym::ExactLoopRecurrenceKind::RotateMix { + direction, + amount, + operation, + term, + } => ( + "rotate_mix".to_string(), + None, + None, + None, + Some(loop_rotate_direction_string(*direction)), + Some(*amount), + Some(loop_fold_operation_string(*operation)), + Some(loop_memory_term_info(term)), + ), + }; + ExactLoopRecurrenceInfo { + header: format!("0x{:x}", recurrence.header), + exit_target: format!("0x{:x}", recurrence.exit_target), + iterations: recurrence.iterations, + accumulator: recurrence.accumulator.clone(), + bits: recurrence.bits, + kind, + constant, + multiplier, + addend, + direction, + amount, + operation, + memory, + } +} + +fn solve_tactic_info(tactic: &r2sym::SolveTacticEvidence) -> SolveTacticInfo { + SolveTacticInfo { + kind: solve_tactic_kind_string(tactic.kind), + status: solve_tactic_status_string(tactic.status), + reason: tactic.reason.clone(), + recurrence: exact_loop_recurrence_info(&tactic.recurrence), + } +} + +fn solve_candidate_shape_info(candidate: &r2sym::SolveCandidateShape) -> SolveCandidateShapeInfo { + SolveCandidateShapeInfo { + input_scalars: candidate.input_scalars, + input_buffers: candidate.input_buffers, + registers: candidate.registers, + memory_regions: candidate.memory_regions, + concrete_assignments: candidate.concrete_assignments, + final_pc: format!("0x{:x}", candidate.final_pc), + num_constraints: candidate.num_constraints, + generation: candidate + .generation + .as_ref() + .map(solve_candidate_generation_info), + } +} + +fn solve_candidate_generation_info( + generation: &r2sym::SolvedPathGeneration, +) -> SolveCandidateGenerationInfo { + SolveCandidateGenerationInfo { + kind: match generation.kind { + r2sym::SolvedPathGenerationKind::ExactRecurrenceConstraintTactic => { + "exact_recurrence_constraint_tactic" + } + r2sym::SolvedPathGenerationKind::MitmConstraintTactic => "mitm_constraint_tactic", + r2sym::SolvedPathGenerationKind::DomainConstraintTactic => "domain_constraint_tactic", + } + .to_string(), + reason: generation.reason.clone(), + constrained_bytes: generation.constrained_bytes, + } +} + +fn solve_witness_info(witness: &r2sym::SolveWitness) -> SolveWitnessInfo { + SolveWitnessInfo { + target: format!("0x{:x}", witness.target_addr), + selected_path_index: witness.selected_path_index, + final_pc: witness.final_pc.map(|pc| format!("0x{pc:x}")), + candidate: witness.candidate.as_ref().map(solve_candidate_shape_info), + evidence: EvidenceSummaryInfo { + precision: fact_precision_string(witness.evidence.precision), + requires_replay: witness.evidence.requires_replay, + reasons: witness.evidence.reasons.clone(), + final_constraint_precision: final_constraint_precision_string( + witness.evidence.final_constraint_precision, + ), + exact_final_constraints: witness.evidence.exact_final_constraints, + model_conditioned_final_constraints: witness + .evidence + .model_conditioned_final_constraints, + exact_loop_recurrences: witness + .evidence + .exact_loop_recurrences + .iter() + .map(exact_loop_recurrence_info) + .collect(), + exact_loop_folds: witness + .evidence + .exact_loop_folds + .iter() + .map(exact_loop_fold_info) + .collect(), + tactics: witness + .evidence + .tactics + .iter() + .map(solve_tactic_info) + .collect(), + }, + verification_requirement: verification_requirement_info(&witness.verification_requirement), + proven: witness.is_proven(), + } +} + +fn final_constraint_precision_string(precision: r2sym::FinalConstraintPrecision) -> String { + match precision { + r2sym::FinalConstraintPrecision::Exact => "exact", + r2sym::FinalConstraintPrecision::ModelConditioned => "model_conditioned", + r2sym::FinalConstraintPrecision::Residual => "residual", + r2sym::FinalConstraintPrecision::Unknown => "unknown", + } + .to_string() +} + +fn empty_symbolic_summary<'ctx>() -> r2sym::SymbolicFunctionSummary<'ctx> { + r2sym::SymbolicFunctionSummary { + completion: r2sym::QueryCompletion::Complete, + paths: Vec::new(), + feasible_paths: 0, + stats: r2sym::path::ExploreStats::default(), + solver_stats: r2sym::SolverStats::default(), + } +} + +fn should_skip_expensive_symbolic_summary( + compiled: &r2sym::SemanticArtifact, + prepared: &r2ssa::SsaArtifact, +) -> bool { + compiled.diagnostics.skipped_large_cfg + || prepared.function().cfg_risk_summary().block_count > 96 +} + +pub(crate) fn compiled_semantic_info(compiled: &r2sym::SemanticArtifact) -> CompiledSemanticInfo { + compiled_semantic_info_with_seed(compiled, None) +} + +fn compiled_semantic_info_with_replay_seed( + compiled: &r2sym::SemanticArtifact, + replay_seed: &r2sym::ReplaySeed, +) -> CompiledSemanticInfo { + compiled_semantic_info_with_seed( + compiled, + Some(r2sym::stable_replay_seed_fingerprint(replay_seed)), + ) +} + +fn compiled_semantic_info_with_seed( + compiled: &r2sym::SemanticArtifact, + replay_seed_fingerprint: Option, +) -> CompiledSemanticInfo { + let native = compiled.native_body(); + CompiledSemanticInfo { + schema_version: r2sym::SEMANTIC_ARTIFACT_SCHEMA_VERSION, + stage: match compiled.stage { + r2sym::RefinementStage::Raw => "raw", + r2sym::RefinementStage::Compiled => "compiled", + r2sym::RefinementStage::Residual => "residual", + } + .to_string(), + granularity: match compiled.granularity { + r2sym::ArtifactGranularity::WholeFunction => "whole_function", + r2sym::ArtifactGranularity::Regioned => "regioned", + r2sym::ArtifactGranularity::SummaryOnly => "summary_only", + } + .to_string(), + execution: match compiled.execution { + r2sym::ExecutionModel::Native => "native", + r2sym::ExecutionModel::Vm => "vm", + } + .to_string(), + seed_mode: replay_seed_fingerprint.map(|_| "replay".to_string()), + replay_seed_fingerprint: replay_seed_fingerprint + .map(|fingerprint| format!("0x{fingerprint:x}")), + query_plan: compiled.query_plan(), + type_plan: compiled.type_plan(), + decompile_plan: compiled.decompile_plan(), + slice_class: compiled + .slice_class() + .map(|slice_class| match slice_class { + r2sym::SliceClass::Wrapper => "wrapper", + r2sym::SliceClass::Worker => "worker", + r2sym::SliceClass::RecursiveGroup => "recursive_group", + r2sym::SliceClass::InterpreterSwitch => "interpreter_switch", + r2sym::SliceClass::InterpreterIndirect => "interpreter_indirect", + r2sym::SliceClass::GenericLarge => "generic_large", + }) + .unwrap_or("worker") + .to_string(), + residual_reasons: compiled + .diagnostics + .residual_reasons + .iter() + .map(|reason| { + match reason { + r2sym::ResidualReason::MissingArch => "missing_arch", + r2sym::ResidualReason::LargeCfg => "large_cfg", + r2sym::ResidualReason::SummaryBudgetExhausted => "summary_budget_exhausted", + r2sym::ResidualReason::SccBudgetExhausted => "scc_budget_exhausted", + r2sym::ResidualReason::InterpreterRequiresStepSummary => { + "interpreter_requires_step_summary" + } + } + .to_string() + }) + .collect(), + ambiguous_target_count: compiled.ambiguous_targets().len(), + ambiguous_targets: compiled + .ambiguous_targets() + .into_iter() + .map(|target| format!("0x{target:x}")) + .collect(), + closure_functions: compiled + .native_body() + .map(|body| body.summary.closure_functions) + .unwrap_or(0), + helper_functions: compiled + .native_body() + .map(|body| body.summary.helper_functions) + .unwrap_or(0), + derived_summaries: compiled + .native_body() + .map(|body| body.summary.derived_summaries) + .unwrap_or(0), + summary_attempted: compiled + .native_body() + .map(|body| body.summary.derived_diagnostics.attempted) + .unwrap_or(0), + summary_budget_exhausted: compiled + .native_body() + .map(|body| { + body.summary.derived_diagnostics.budget_exhausted + + body.summary.derived_diagnostics.scc_budget_exhausted + }) + .unwrap_or(0), + summary_scc_count: compiled + .native_body() + .map(|body| body.summary.derived_diagnostics.scc_count) + .unwrap_or(0), + region_count: native.map(|body| body.regions.len()).unwrap_or(0), + control_region_count: native + .map(|body| { + body.regions + .values() + .filter(|region| !region.control.is_empty()) + .count() + }) + .unwrap_or(0), + memory_region_count: native + .map(|body| { + body.regions + .values() + .filter(|region| !region.memory.is_empty()) + .count() + }) + .unwrap_or(0), + compiled_condition_count: compiled.actionable_control_count(), + exact_compiled_condition_count: compiled.exact_control_count(), + actionable_compiled_condition_count: compiled.actionable_control_count(), + branches_pruned: compiled.diagnostics.branches_pruned, + branches_unknown: compiled.diagnostics.branches_unknown, + skipped_large_cfg: compiled.diagnostics.skipped_large_cfg, + interpreter: compiled + .vm_body() + .and_then(|body| body.interpreter.as_ref()) + .as_ref() + .map(|interpreter| InterpreterDispatchInfo { + kind: match interpreter.kind { + r2sym::InterpreterKind::SwitchDispatch => "switch_dispatch".to_string(), + r2sym::InterpreterKind::IndirectDispatch => "indirect_dispatch".to_string(), + }, + dispatch_header: format!("0x{:x}", interpreter.dispatch_header), + dispatch_targets: interpreter.dispatch_targets, + selector: interpreter.selector.clone(), + back_edges: interpreter.back_edges, + score: interpreter.score, + }), + vm_step: compiled + .vm_body() + .and_then(|body| body.step_summary.as_ref()) + .map(vm_step_summary_info_from_sym), + vm_transfer: compiled + .vm_body() + .and_then(|body| body.transfer_summary.as_ref()) + .map(vm_step_summary_info_from_sym), + cache_hit: compiled.diagnostics.cache_hit, + } +} + +fn path_solution_from_result<'ctx>( + explorer: &r2sym::PathExplorer<'ctx>, + result: &r2sym::PathResult<'ctx>, +) -> Option { + if !result.feasible { + return None; + } + explorer.solve_path(result).map(|solved| PathSolution { + inputs: solved + .inputs + .into_iter() + .map(|(k, v)| (k, format!("0x{:x}", v))) + .collect(), + registers: solved + .registers + .into_iter() + .filter(|(name, _)| !name.starts_with("tmp:") && !name.contains("_0")) + .map(|(k, v)| (k, format!("0x{:x}", v))) + .collect(), + }) +} + +fn buffer_solution(bytes: Vec) -> BufferSolution { + let hex = bytes + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + let ascii = bytes + .iter() + .map(|byte| { + if byte.is_ascii_graphic() || *byte == b' ' { + *byte as char + } else { + '.' + } + }) + .collect(); + BufferSolution { hex, ascii } +} + +fn run_path_solution_from_result<'ctx>( + explorer: &r2sym::PathExplorer<'ctx>, + result: &r2sym::PathResult<'ctx>, +) -> Option { + if !result.feasible { + return None; + } + explorer.solve_path(result).map(|solved| RunPathSolution { + inputs: solved + .inputs + .into_iter() + .map(|(k, v)| (k, format!("0x{:x}", v))) + .collect(), + input_buffers: solved + .input_buffers + .into_iter() + .map(|(k, v)| (k, buffer_solution(v))) + .collect(), + registers: solved + .registers + .into_iter() + .filter(|(name, _)| !name.starts_with("tmp:") && !name.contains("_0")) + .map(|(k, v)| (k, format!("0x{:x}", v))) + .collect(), + }) +} + +fn path_info_from_result_with_solution<'ctx>( + path_id: usize, + result: &r2sym::PathResult<'ctx>, + explorer: &r2sym::PathExplorer<'ctx>, + include_solution: bool, +) -> PathInfo { + PathInfo { + path_id, + feasible: result.feasible, + depth: result.depth, + exit_status: format!("{:?}", result.exit_status), + final_pc: format!("0x{:x}", result.final_pc()), + num_constraints: result.num_constraints(), + solution: include_solution + .then(|| path_solution_from_result(explorer, result)) + .flatten(), + } +} + +fn path_info_from_result<'ctx>( + path_id: usize, + result: &r2sym::PathResult<'ctx>, + explorer: &r2sym::PathExplorer<'ctx>, +) -> PathInfo { + path_info_from_result_with_solution(path_id, result, explorer, true) +} + +fn run_path_info_from_result<'ctx>( + path_id: usize, + result: &r2sym::PathResult<'ctx>, + explorer: &r2sym::PathExplorer<'ctx>, +) -> SymRunPathInfo { + SymRunPathInfo { + path_id, + feasible: result.feasible, + depth: result.depth, + exit_status: format!("{:?}", result.exit_status), + final_pc: format!("0x{:x}", result.final_pc()), + num_constraints: result.num_constraints(), + solution: run_path_solution_from_result(explorer, result), + } +} + +pub(crate) fn build_symbolic_prepared( + blocks: &[R2ILBlock], + arch: Option<&ArchSpec>, + name: Option<&str>, +) -> Option { + let prepared = if name.is_some_and(|name| name.starts_with("runtime.materialized.")) { + r2ssa::SsaArtifact::raw(blocks, arch)? + } else { + r2ssa::SsaArtifact::for_symbolic(blocks, arch)? + }; + Some(match name { + Some(name) if !name.is_empty() => prepared.with_name(name.to_string()), + _ => prepared, + }) +} + +pub(crate) fn build_single_function_scope( + prepared: r2ssa::SsaArtifact, + entry_addr: u64, + name: Option, +) -> Option { + r2sym::PreparedFunctionScope::new( + entry_addr, + vec![r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(entry_addr), + name, + prepared, + }], + ) +} + +pub(crate) unsafe fn build_symbolic_scope_from_ffi( + functions: *const R2ILFunctionBlocks, + num_functions: usize, + arch: Option<&ArchSpec>, + root_entry_addr: u64, +) -> Option { + if functions.is_null() || num_functions == 0 { + return None; + } + + let mut scope_functions = Vec::new(); + for index in 0..num_functions { + let function = unsafe { &*functions.add(index) }; + let blocks = unsafe { BlockSlice::from_ffi(function.blocks, function.num_blocks) }?; + let name = if function.name.is_null() { + None + } else { + unsafe { CStr::from_ptr(function.name).to_str().ok() }.map(str::to_string) + }; + let prepared = build_symbolic_prepared(blocks.as_slice(), arch, name.as_deref())?; + scope_functions.push(r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(function.entry_addr), + name, + prepared, + }); + } + r2sym::PreparedFunctionScope::new(root_entry_addr, scope_functions) +} + +fn symbol_map_snapshot() -> HashMap { + sym_symbol_map() + .lock() + .map(|map| map.clone()) + .unwrap_or_default() +} + +fn scope_root_prepared(scope: &r2sym::PreparedFunctionScope) -> Option<&r2ssa::SsaArtifact> { + scope.root().map(|function| &function.prepared) +} + +fn prepared_assumption_conflicted(prepared: &r2ssa::SsaArtifact) -> bool { + !prepared.facts().assumption_usage.conflicts.is_empty() +} + +fn prepared_assumption_conditioning( + prepared: &r2ssa::SsaArtifact, +) -> (r2ssa::AssumptionUsageReport, bool) { + let usage = prepared.facts().assumption_usage.clone(); + let conditioned = !usage.applied.is_empty() || !usage.conflicts.is_empty(); + (usage, conditioned) +} + +struct PredictedTargetQueryRouteInput<'ctx, 'a> { + z3_ctx: &'ctx Context, + prepared: &'a r2ssa::SsaArtifact, + scope: Option<&'a r2sym::PreparedFunctionScope>, + compiled: &'a r2sym::SemanticArtifact, + target_addr: u64, + arch: Option<&'a ArchSpec>, + symbol_map: &'a HashMap, + summary_profile: r2sym::SummaryProfile, + assumption_conflicted: bool, +} + +fn predicted_target_query_route( + input: PredictedTargetQueryRouteInput<'_, '_>, +) -> r2sym::TargetQueryRoutePlan { + let PredictedTargetQueryRouteInput { + z3_ctx, + prepared, + scope, + compiled, + target_addr, + arch, + symbol_map, + summary_profile, + assumption_conflicted, + } = input; + let probe_config = r2sym::SymQueryConfig { + explore: sym_default_config(), + mode: r2sym::QueryMode::TargetGuided, + summary_profile, + solve_tactics: r2sym::SolveTacticConfig::default(), + }; + let mut explorer = probe_config.make_explorer(z3_ctx); + if let Some(scope) = scope { + r2sym::install_runtime_hooks_for_scope(&mut explorer, scope, arch, symbol_map); + } + r2sym::selected_target_query_route_in_scope( + &mut explorer, + prepared, + scope, + Some(compiled), + target_addr, + assumption_conflicted, + ) +} + +fn parse_scope_assumptions( + external_context_json: *const c_char, + arch: Option<&ArchSpec>, +) -> Result { + if external_context_json.is_null() { + return Ok(r2ssa::AssumptionSet::default()); + } + let text = unsafe { CStr::from_ptr(external_context_json) } + .to_str() + .map_err(|_| "external context is not valid utf-8")?; + let ptr_bits = arch.map(effective_ptr_bits).unwrap_or(64); + Ok(r2types::parse_external_context_json(text, ptr_bits).assumptions) +} + +fn scope_with_external_assumptions( + scope: &r2sym::PreparedFunctionScope, + arch: Option<&ArchSpec>, + external_context_json: *const c_char, +) -> Result { + let assumptions = parse_scope_assumptions(external_context_json, arch)?; + let prepared = scope_root_prepared(scope).ok_or("failed to build root SSA function")?; + let prepared = if assumptions.is_empty() { + prepared.clone() + } else { + prepared.with_assumptions(&assumptions) + }; + scope + .with_prepared_root(&prepared) + .ok_or("failed to build symbolic scope") +} + +unsafe fn ffi_slice<'a, T>(ptr: *const T, len: usize) -> Result<&'a [T], &'static str> { + if len == 0 { + return Ok(&[]); + } + if ptr.is_null() { + return Err("missing replay seed array"); + } + Ok(unsafe { slice::from_raw_parts(ptr, len) }) +} + +unsafe fn ffi_string(ptr: *const c_char) -> Result { + if ptr.is_null() { + return Err("missing replay seed string"); + } + unsafe { CStr::from_ptr(ptr) } + .to_str() + .map(str::to_string) + .map_err(|_| "replay seed string is not valid utf-8") +} + +unsafe fn replay_seed_from_ffi( + seed: *const R2SymReplaySeed, +) -> Result { + if seed.is_null() { + return Err("missing replay seed"); + } + let seed = unsafe { &*seed }; + + let registers = unsafe { ffi_slice(seed.registers, seed.num_registers) }? + .iter() + .map(|register| { + Ok(r2sym::ReplayRegisterValue { + name: unsafe { ffi_string(register.name) }?, + value: register.value, + }) + }) + .collect::, _>>()?; + + let memory = unsafe { ffi_slice(seed.memory, seed.num_memory) }? + .iter() + .map(|window| { + let bytes = if window.size == 0 { + Vec::new() + } else if window.bytes.is_null() { + return Err("missing replay memory bytes"); + } else { + unsafe { slice::from_raw_parts(window.bytes, window.size) }.to_vec() + }; + let label = if window.label.is_null() { + None + } else { + Some(unsafe { ffi_string(window.label) }?) + }; + Ok(r2sym::ReplayMemoryWindow { + addr: window.addr, + bytes, + label, + }) + }) + .collect::, _>>()?; + + let register_overlays = + unsafe { ffi_slice(seed.register_overlays, seed.num_register_overlays) }? + .iter() + .map(|overlay| { + Ok(r2sym::ReplayRegisterOverlay { + name: unsafe { ffi_string(overlay.name) }?, + symbol: unsafe { ffi_string(overlay.symbol) }?, + }) + }) + .collect::, _>>()?; + + let memory_overlays = unsafe { ffi_slice(seed.memory_overlays, seed.num_memory_overlays) }? + .iter() + .map(|overlay| { + Ok(r2sym::ReplayMemoryOverlay { + addr: overlay.addr, + size: overlay.size, + name: unsafe { ffi_string(overlay.name) }?, + }) + }) + .collect::, _>>()?; + + let tty_fds = unsafe { ffi_slice(seed.tty_fds, seed.num_tty_fds) }?.to_vec(); + + Ok(r2sym::ReplaySeed { + checkpoint_id: (seed.checkpoint_id != 0).then_some(seed.checkpoint_id), + entry_pc: (seed.entry_addr != 0).then_some(seed.entry_addr), + registers, + memory, + register_overlays, + memory_overlays, + tty_fds, + skip_sleep_calls: seed.skip_sleep_calls != 0, + }) +} + +fn build_replay_seeded_state<'ctx>( + z3_ctx: &'ctx Context, + entry_addr: u64, + prepared: &r2ssa::SsaArtifact, + arch: Option<&ArchSpec>, + replay_seed: &r2sym::ReplaySeed, +) -> r2sym::SymState<'ctx> { + let mut initial_state = + r2sym::SymState::new(z3_ctx, replay_seed.entry_pc.unwrap_or(entry_addr)); + r2sym::seed_replay_state_for_arch(&mut initial_state, Some(prepared), arch, replay_seed); + initial_state +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_function( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + entry_addr: u64, +) -> *mut c_char { + let Some(ctx_view) = require_ctx_view(ctx) else { + return ptr::null_mut(); + }; + let Some(blocks) = (unsafe { BlockSlice::from_ffi(blocks, num_blocks) }) else { + return ptr::null_mut(); + }; + + let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch, None) { + Some(prepared) => prepared, + None => return ptr::null_mut(), + }; + let Some(scope) = build_single_function_scope(prepared.clone(), entry_addr, None) else { + return ptr::null_mut(); + }; + let symbol_map = symbol_map_snapshot(); + let z3_ctx = Context::thread_local(); + let mut query_config = sym_paths_query_config(&prepared); + + let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let compiled = r2sym::compile_semantic_artifact_with_scope( + &z3_ctx, + &prepared, + Some(&scope), + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + ); + if should_skip_expensive_symbolic_summary(&compiled, &prepared) { + return (empty_symbolic_summary(), compiled); + } + let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); + r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); + let query_policy = + tune_query_config_for_state(&mut query_config, &prepared, &initial_state, None); + let mut explorer = query_config.make_explorer(&z3_ctx); + r2sym::install_symbolic_hooks_for_query_policy( + &mut explorer, + &z3_ctx, + &scope, + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + &query_policy, + ); + ( + explorer.summarize_function(&prepared, initial_state), + compiled, + ) + })); + + let (summary, compiled) = match explore_result { + Ok(r) => r, + Err(err) => { + return sym_panic_json("symbolic execution failed", err); + } + }; + + let output = build_sym_exec_summary( + &summary.stats, + &summary.solver_stats, + summary.feasible_paths, + r2ssa::AssumptionUsageReport::default(), + false, + false, + Some(&compiled), + ); + match serde_json::to_string_pretty(&output) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => ptr::null_mut(), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_state_json(state: *const R2SymContext) -> *mut c_char { + if state.is_null() { + return ptr::null_mut(); + } + let state_ref = unsafe { &*state }; + let info = SymStateInfo { + pc: state_ref.entry_pc, + depth: 0, + num_constraints: 0, + registers: BTreeMap::new(), + }; + match serde_json::to_string_pretty(&info) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => ptr::null_mut(), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_paths( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + entry_addr: u64, +) -> *mut c_char { + let Some(ctx_view) = require_ctx_view(ctx) else { + return ptr::null_mut(); + }; + let Some(blocks) = (unsafe { BlockSlice::from_ffi(blocks, num_blocks) }) else { + return ptr::null_mut(); + }; + + let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch, None) { + Some(prepared) => prepared, + None => return ptr::null_mut(), + }; + let Some(scope) = build_single_function_scope(prepared.clone(), entry_addr, None) else { + return ptr::null_mut(); + }; + let symbol_map = symbol_map_snapshot(); + let z3_ctx = Context::thread_local(); + let mut query_config = sym_paths_query_config(&prepared); + + let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); + r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); + let query_policy = + tune_query_config_for_state(&mut query_config, &prepared, &initial_state, None); + let mut explorer = query_config.make_explorer(&z3_ctx); + r2sym::install_symbolic_hooks_for_query_policy( + &mut explorer, + &z3_ctx, + &scope, + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + &query_policy, + ); + let summary = explorer.summarize_function(&prepared, initial_state); + (summary, explorer) + })); + + let (summary, explorer) = match explore_result { + Ok(r) => r, + Err(err) => { + return sym_panic_json("symbolic execution failed", err); + } + }; + + let solution_limit = sym_paths_solution_limit(summary.paths.len(), &prepared); + let paths: Vec = summary + .paths + .iter() + .enumerate() + .map(|(i, r)| path_info_from_result_with_solution(i, r, &explorer, i < solution_limit)) + .collect(); + match serde_json::to_string_pretty(&paths) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => ptr::null_mut(), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_explore_to( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + entry_addr: u64, + target_addr: u64, +) -> *mut c_char { + let Some(ctx_view) = require_ctx_view(ctx) else { + return sym_error_json("missing disassembler context"); + }; + let Some(blocks) = (unsafe { BlockSlice::from_ffi(blocks, num_blocks) }) else { + return sym_error_json("no blocks to explore"); + }; + + let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch, None) { + Some(prepared) => prepared, + None => return sym_error_json("failed to build SSA function"), + }; + let Some(scope) = build_single_function_scope(prepared.clone(), entry_addr, None) else { + return sym_error_json("failed to build symbolic scope"); + }; + let symbol_map = symbol_map_snapshot(); + let mut query_config = sym_default_query_config(); + + let z3_ctx = Context::thread_local(); + let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let compiled = r2sym::compile_query_semantic_artifact_with_scope( + &z3_ctx, + &prepared, + Some(&scope), + target_addr, + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + ); + let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); + r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); + let selected_route = predicted_target_query_route(PredictedTargetQueryRouteInput { + z3_ctx: &z3_ctx, + prepared: &prepared, + scope: Some(&scope), + compiled: &compiled, + target_addr, + arch: ctx_view.arch, + symbol_map: &symbol_map, + summary_profile: query_config.summary_profile, + assumption_conflicted: prepared_assumption_conflicted(&prepared), + }); + let query_policy = tune_query_config_for_state( + &mut query_config, + &prepared, + &initial_state, + Some(&selected_route), + ); + let mut explorer = query_config.make_explorer(&z3_ctx); + r2sym::install_symbolic_hooks_for_query_policy( + &mut explorer, + &z3_ctx, + &scope, + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + &query_policy, + ); + let reach = explorer.can_reach_with_artifact_in_scope( + &prepared, + Some(&scope), + Some(&compiled), + initial_state, + target_addr, + ); + let paths: Vec = reach + .paths + .iter() + .enumerate() + .map(|(i, r)| path_info_from_result(i, r, &explorer)) + .collect(); + ( + paths, + reach.stats, + reach.solver_stats, + reach.assumption_usage, + reach.assumption_conditioned, + reach.summary_conditioned, + reach.selected_route, + compiled.clone(), + ) + })); + + let ( + paths, + stats, + solver_stats, + assumption_usage, + assumption_conditioned, + summary_conditioned, + selected_route, + compiled, + ) = match explore_result { + Ok(value) => value, + Err(err) => return sym_panic_json("symbolic execution failed", err), + }; + let output = SymTargetExploreResult { + entry: format!("0x{:x}", entry_addr), + target: format!("0x{:x}", target_addr), + matched_paths: paths.len(), + stats: build_sym_exec_summary( + &stats, + &solver_stats, + paths.len(), + assumption_usage, + assumption_conditioned, + summary_conditioned, + Some(&compiled), + ), + target_query: Some(target_query_info(&selected_route)), + paths, + }; + + match serde_json::to_string(&output) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => sym_error_json("failed to serialize symbolic exploration output"), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_solve_to( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + entry_addr: u64, + target_addr: u64, +) -> *mut c_char { + let Some(ctx_view) = require_ctx_view(ctx) else { + return sym_error_json("missing disassembler context"); + }; + let Some(blocks) = (unsafe { BlockSlice::from_ffi(blocks, num_blocks) }) else { + return sym_error_json("no blocks to solve"); + }; + + let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch, None) { + Some(prepared) => prepared, + None => return sym_error_json("failed to build SSA function"), + }; + let Some(scope) = build_single_function_scope(prepared.clone(), entry_addr, None) else { + return sym_error_json("failed to build symbolic scope"); + }; + let symbol_map = symbol_map_snapshot(); + let mut query_config = sym_default_query_config(); + let z3_ctx = Context::thread_local(); + + let solve_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let compiled = r2sym::compile_query_semantic_artifact_with_scope( + &z3_ctx, + &prepared, + Some(&scope), + target_addr, + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + ); + let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); + r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); + let selected_route = predicted_target_query_route(PredictedTargetQueryRouteInput { + z3_ctx: &z3_ctx, + prepared: &prepared, + scope: Some(&scope), + compiled: &compiled, + target_addr, + arch: ctx_view.arch, + symbol_map: &symbol_map, + summary_profile: query_config.summary_profile, + assumption_conflicted: prepared_assumption_conflicted(&prepared), + }); + let query_policy = tune_query_config_for_state( + &mut query_config, + &prepared, + &initial_state, + Some(&selected_route), + ); + let mut explorer = query_config.make_explorer(&z3_ctx); + r2sym::install_symbolic_hooks_for_query_policy( + &mut explorer, + &z3_ctx, + &scope, + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + &query_policy, + ); + let solve = explorer.solve_for_target_with_artifact_in_scope( + &prepared, + Some(&scope), + Some(&compiled), + initial_state, + target_addr, + ); + let selected = solve + .selected_path_index + .and_then(|idx| solve.matched_paths.get(idx).map(|path| (idx, path))) + .map(|(idx, path)| { + path_info_from_result_with_solution(idx, path, &explorer, solve_found(solve.status)) + }); + ( + solve.status, + solve.matched_paths.len(), + selected, + solve.verification, + solve.witness, + solve.stats, + solve.solver_stats, + solve.assumption_usage, + solve.assumption_conditioned, + solve.summary_conditioned, + solve.selected_route, + compiled.clone(), + ) + })); + + let ( + status, + matched_paths, + selected_path, + verification, + witness, + stats, + solver_stats, + assumption_usage, + assumption_conditioned, + summary_conditioned, + selected_route, + compiled, + ) = match solve_result { + Ok(value) => value, + Err(err) => return sym_panic_json("symbolic execution failed", err), + }; + let output = SymTargetSolveResult { + entry: format!("0x{:x}", entry_addr), + target: format!("0x{:x}", target_addr), + status: solve_status_string(status), + matched_paths, + found: solve_found(status), + target_reached_under_model: verification.target_reached_under_model, + candidate_solution_verified: verification.candidate_solution_verified, + residual_reasons: verification.residual_reasons.clone(), + model_validation: model_validation_info(&verification.model_validation), + witness: solve_witness_info(&witness), + stats: build_sym_exec_summary( + &stats, + &solver_stats, + matched_paths, + assumption_usage, + assumption_conditioned, + summary_conditioned, + Some(&compiled), + ), + target_query: Some(target_query_info(&selected_route)), + selected_path, }; - match serde_json::to_string_pretty(&summary) { + + match serde_json::to_string(&output) { Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), - Err(_) => ptr::null_mut(), + Err(_) => sym_error_json("failed to serialize symbolic solve output"), } } #[unsafe(no_mangle)] -pub extern "C" fn r2sym_state_json(state: *const R2SymContext) -> *mut c_char { - if state.is_null() { - return ptr::null_mut(); +pub extern "C" fn r2sym_run_spec_json( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + entry_addr: u64, + spec_json: *const c_char, +) -> *mut c_char { + if spec_json.is_null() { + return sym_error_json("missing exploration spec json"); } - let state_ref = unsafe { &*state }; - let info = SymStateInfo { - pc: state_ref.entry_pc, - depth: 0, - num_constraints: 0, - registers: std::collections::HashMap::new(), + let spec_text = unsafe { + match CStr::from_ptr(spec_json).to_str() { + Ok(text) => text, + Err(_) => return sym_error_json("exploration spec is not valid utf-8"), + } }; - match serde_json::to_string_pretty(&info) { + let spec = match serde_json::from_str::(spec_text) { + Ok(spec) => spec, + Err(err) => return sym_error_json(&format!("failed to parse exploration spec: {err}")), + }; + if let Err(err) = spec.validate() { + return sym_error_json(&err); + } + + let Some(ctx_view) = require_ctx_view(ctx) else { + return sym_error_json("missing disassembler context"); + }; + let Some(blocks) = (unsafe { BlockSlice::from_ffi(blocks, num_blocks) }) else { + return sym_error_json("no blocks to explore"); + }; + + let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch, None) { + Some(prepared) => prepared, + None => return sym_error_json("failed to build SSA function"), + }; + let Some(scope) = build_single_function_scope(prepared.clone(), entry_addr, None) else { + return sym_error_json("failed to build symbolic scope"); + }; + let symbol_map = symbol_map_snapshot(); + let z3_ctx = Context::thread_local(); + let mut default_config = sym_default_query_config(); + + let run_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let start_pc = spec.start_pc(entry_addr)?; + let mut initial_state = r2sym::SymState::new(&z3_ctx, start_pc); + r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); + spec.apply_to_state(&mut initial_state); + let query_policy = + tune_query_config_for_state(&mut default_config, &prepared, &initial_state, None); + + let mut explorer = r2sym::PathExplorer::with_config( + &z3_ctx, + spec.to_explore_config(&default_config.explore), + ); + r2sym::install_symbolic_hooks_for_query_policy( + &mut explorer, + &z3_ctx, + &scope, + ctx_view.arch, + &symbol_map, + default_config.summary_profile, + &query_policy, + ); + let result = explorer.run_spec(&prepared, initial_state, &spec)?; + let stats = explorer.stats().clone(); + let solver_stats = explorer.solver().stats(); + let found_paths = result + .found_paths + .iter() + .enumerate() + .map(|(idx, path)| run_path_info_from_result(idx, path, &explorer)) + .collect::>(); + Ok::<_, String>((result, stats, solver_stats, found_paths)) + })); + + let (result, stats, solver_stats, found_paths) = match run_result { + Ok(Ok(value)) => value, + Ok(Err(err)) => return sym_error_json(&err), + Err(err) => return sym_panic_json("symbolic execution failed", err), + }; + + let output = SymRunResult { + entry: format!("0x{:x}", entry_addr), + spec, + stats: build_sym_exec_summary( + &stats, + &solver_stats, + found_paths.len(), + r2ssa::AssumptionUsageReport::default(), + false, + false, + None, + ), + stash_counts: SymRunStashCounts { + found: found_paths.len(), + avoided: result.avoided_states, + unsat: result.unsat_states, + errored: result.errored_states, + completed: result.completed_states, + }, + found_paths, + diagnostics: result.diagnostics, + }; + + match serde_json::to_string(&output) { Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), - Err(_) => ptr::null_mut(), + Err(_) => sym_error_json("failed to serialize symbolic run output"), } } #[unsafe(no_mangle)] -pub extern "C" fn r2sym_paths( +pub extern "C" fn r2sym_function_scope( ctx: *const R2ILContext, - blocks: *const *const R2ILBlock, - num_blocks: usize, + functions: *const R2ILFunctionBlocks, + num_functions: usize, entry_addr: u64, + external_context_json: *const c_char, ) -> *mut c_char { let Some(ctx_view) = require_ctx_view(ctx) else { return ptr::null_mut(); }; - let Some(blocks) = (unsafe { BlockSlice::from_ffi(blocks, num_blocks) }) else { + let Some(scope) = (unsafe { + build_symbolic_scope_from_ffi(functions, num_functions, ctx_view.arch, entry_addr) + }) else { return ptr::null_mut(); }; - - let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch) { - Some(prepared) => prepared, - None => return ptr::null_mut(), + let scope = match scope_with_external_assumptions(&scope, ctx_view.arch, external_context_json) + { + Ok(scope) => scope, + Err(err) => return sym_error_json(err), + }; + let Some(prepared) = scope_root_prepared(&scope) else { + return ptr::null_mut(); }; + let (assumption_usage, assumption_conditioned) = prepared_assumption_conditioning(prepared); let symbol_map = symbol_map_snapshot(); let z3_ctx = Context::thread_local(); + let mut query_config = sym_paths_query_config(prepared); let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let compiled = r2sym::compile_semantic_artifact_with_scope( + &z3_ctx, + prepared, + Some(&scope), + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + ); + if should_skip_expensive_symbolic_summary(&compiled, prepared) { + return (empty_symbolic_summary(), compiled); + } let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); - r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); - let mut explorer = r2sym::PathExplorer::with_config(&z3_ctx, sym_default_config()); - if let Some(arch) = ctx_view.arch - && let Some(registry) = r2sym::SummaryRegistry::with_core_for_arch(arch) - { - let interproc = build_seed_interproc_summary_set(&prepared, &symbol_map); - let _ = - registry.install_known_symbols_for_function(&mut explorer, &prepared, &symbol_map); - let _ = registry.install_interproc_summaries_for_function( - &mut explorer, - &prepared, - &interproc, - &symbol_map, - ); + r2sym::seed_scope_state_for_arch(&mut initial_state, prepared, &scope, ctx_view.arch); + let query_policy = + tune_query_config_for_state(&mut query_config, prepared, &initial_state, None); + let mut explorer = query_config.make_explorer(&z3_ctx); + r2sym::install_symbolic_hooks_for_query_policy( + &mut explorer, + &z3_ctx, + &scope, + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + &query_policy, + ); + ( + explorer.summarize_function(prepared, initial_state), + compiled, + ) + })); + + let (summary, compiled) = match explore_result { + Ok(r) => r, + Err(err) => { + return sym_panic_json("symbolic execution failed", err); } - let results = explorer.explore(&prepared, initial_state); - (results, explorer) + }; + + let output = build_sym_exec_summary( + &summary.stats, + &summary.solver_stats, + summary.feasible_paths, + assumption_usage, + assumption_conditioned, + false, + Some(&compiled), + ); + match serde_json::to_string_pretty(&output) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => ptr::null_mut(), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_compile_semantics_scope( + ctx: *const R2ILContext, + functions: *const R2ILFunctionBlocks, + num_functions: usize, + entry_addr: u64, + external_context_json: *const c_char, +) -> *mut c_char { + let Some(ctx_view) = require_ctx_view(ctx) else { + return sym_error_json("missing disassembler context"); + }; + let Some(scope) = (unsafe { + build_symbolic_scope_from_ffi(functions, num_functions, ctx_view.arch, entry_addr) + }) else { + return sym_error_json("failed to build symbolic scope"); + }; + let scope = match scope_with_external_assumptions(&scope, ctx_view.arch, external_context_json) + { + Ok(scope) => scope, + Err(err) => return sym_error_json(err), + }; + let Some(prepared) = scope_root_prepared(&scope) else { + return sym_error_json("failed to build root SSA function"); + }; + let symbol_map = symbol_map_snapshot(); + let z3_ctx = Context::thread_local(); + let query_config = sym_default_query_config(); + let compiled = r2sym::compile_semantic_artifact_with_scope( + &z3_ctx, + prepared, + Some(&scope), + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + ); + match serde_json::to_string(&compiled_semantic_info(&compiled)) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => sym_error_json("failed to serialize compiled semantics output"), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_paths_scope( + ctx: *const R2ILContext, + functions: *const R2ILFunctionBlocks, + num_functions: usize, + entry_addr: u64, + external_context_json: *const c_char, +) -> *mut c_char { + let Some(ctx_view) = require_ctx_view(ctx) else { + return ptr::null_mut(); + }; + let Some(scope) = (unsafe { + build_symbolic_scope_from_ffi(functions, num_functions, ctx_view.arch, entry_addr) + }) else { + return ptr::null_mut(); + }; + let scope = match scope_with_external_assumptions(&scope, ctx_view.arch, external_context_json) + { + Ok(scope) => scope, + Err(err) => return sym_error_json(err), + }; + let Some(prepared) = scope_root_prepared(&scope) else { + return ptr::null_mut(); + }; + let symbol_map = symbol_map_snapshot(); + let z3_ctx = Context::thread_local(); + let mut query_config = sym_paths_query_config(prepared); + + let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); + r2sym::seed_scope_state_for_arch(&mut initial_state, prepared, &scope, ctx_view.arch); + let query_policy = + tune_query_config_for_state(&mut query_config, prepared, &initial_state, None); + let mut explorer = query_config.make_explorer(&z3_ctx); + r2sym::install_symbolic_hooks_for_query_policy( + &mut explorer, + &z3_ctx, + &scope, + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + &query_policy, + ); + let summary = explorer.summarize_function(prepared, initial_state); + (summary, explorer) })); - let (results, explorer) = match explore_result { + let (summary, explorer) = match explore_result { Ok(r) => r, - Err(_) => { - let error_msg = r#"[{"error": "symbolic execution failed (z3 context error)"}]"#; - return CString::new(error_msg).map_or(ptr::null_mut(), |c| c.into_raw()); + Err(err) => { + return sym_panic_json("symbolic execution failed", err); } }; - let paths: Vec = results + let solution_limit = sym_paths_solution_limit(summary.paths.len(), prepared); + let paths: Vec = summary + .paths .iter() .enumerate() - .map(|(i, r)| path_info_from_result(i, r, &explorer)) + .map(|(i, r)| path_info_from_result_with_solution(i, r, &explorer, i < solution_limit)) .collect(); match serde_json::to_string_pretty(&paths) { Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), @@ -409,70 +2731,125 @@ pub extern "C" fn r2sym_paths( } #[unsafe(no_mangle)] -pub extern "C" fn r2sym_explore_to( +pub extern "C" fn r2sym_explore_to_scope( ctx: *const R2ILContext, - blocks: *const *const R2ILBlock, - num_blocks: usize, + functions: *const R2ILFunctionBlocks, + num_functions: usize, entry_addr: u64, target_addr: u64, + external_context_json: *const c_char, ) -> *mut c_char { let Some(ctx_view) = require_ctx_view(ctx) else { return sym_error_json("missing disassembler context"); }; - let Some(blocks) = (unsafe { BlockSlice::from_ffi(blocks, num_blocks) }) else { - return sym_error_json("no blocks to explore"); + let Some(scope) = (unsafe { + build_symbolic_scope_from_ffi(functions, num_functions, ctx_view.arch, entry_addr) + }) else { + return sym_error_json("failed to build symbolic scope"); }; - - let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch) { - Some(prepared) => prepared, - None => return sym_error_json("failed to build SSA function"), + let scope = match scope_with_external_assumptions(&scope, ctx_view.arch, external_context_json) + { + Ok(scope) => scope, + Err(err) => return sym_error_json(err), + }; + let Some(prepared) = scope_root_prepared(&scope) else { + return sym_error_json("failed to build root SSA function"); }; let symbol_map = symbol_map_snapshot(); + let mut query_config = sym_default_query_config(); let z3_ctx = Context::thread_local(); let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let compiled = r2sym::compile_query_semantic_artifact_with_scope( + &z3_ctx, + prepared, + Some(&scope), + target_addr, + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + ); let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); - r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); - let mut explorer = r2sym::PathExplorer::with_config(&z3_ctx, sym_default_config()); - if let Some(arch) = ctx_view.arch - && let Some(registry) = r2sym::SummaryRegistry::with_core_for_arch(arch) - { - let interproc = build_seed_interproc_summary_set(&prepared, &symbol_map); - let _ = - registry.install_known_symbols_for_function(&mut explorer, &prepared, &symbol_map); - let _ = registry.install_interproc_summaries_for_function( - &mut explorer, - &prepared, - &interproc, - &symbol_map, - ); - } - let matched = explorer.find_paths_to(&prepared, initial_state, target_addr); - let stats = explorer.stats().clone(); - let paths: Vec = matched + r2sym::seed_scope_state_for_arch(&mut initial_state, prepared, &scope, ctx_view.arch); + let selected_route = predicted_target_query_route(PredictedTargetQueryRouteInput { + z3_ctx: &z3_ctx, + prepared, + scope: Some(&scope), + compiled: &compiled, + target_addr, + arch: ctx_view.arch, + symbol_map: &symbol_map, + summary_profile: query_config.summary_profile, + assumption_conflicted: prepared_assumption_conflicted(prepared), + }); + let query_policy = tune_query_config_for_state( + &mut query_config, + prepared, + &initial_state, + Some(&selected_route), + ); + let mut explorer = query_config.make_explorer(&z3_ctx); + r2sym::install_symbolic_hooks_for_query_policy( + &mut explorer, + &z3_ctx, + &scope, + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + &query_policy, + ); + let reach = explorer.can_reach_with_artifact_in_scope( + prepared, + Some(&scope), + Some(&compiled), + initial_state, + target_addr, + ); + let paths: Vec = reach + .paths .iter() .enumerate() .map(|(i, r)| path_info_from_result(i, r, &explorer)) .collect(); - (paths, stats) + ( + paths, + reach.stats, + reach.solver_stats, + reach.assumption_usage, + reach.assumption_conditioned, + reach.summary_conditioned, + reach.selected_route, + compiled.clone(), + ) })); - let (paths, stats) = match explore_result { + let ( + paths, + stats, + solver_stats, + assumption_usage, + assumption_conditioned, + summary_conditioned, + selected_route, + compiled, + ) = match explore_result { Ok(value) => value, - Err(_) => return sym_error_json("symbolic execution failed (z3 context error)"), + Err(err) => return sym_panic_json("symbolic execution failed", err), }; let output = SymTargetExploreResult { entry: format!("0x{:x}", entry_addr), target: format!("0x{:x}", target_addr), matched_paths: paths.len(), - stats: SymExecSummary { - paths_explored: stats.paths_completed, - paths_feasible: paths.len(), - paths_pruned: stats.paths_pruned, - max_depth: stats.max_depth_reached, - states_explored: stats.states_explored, - time_ms: stats.total_time.as_millis() as u64, - }, + stats: build_sym_exec_summary( + &stats, + &solver_stats, + paths.len(), + assumption_usage, + assumption_conditioned, + summary_conditioned, + Some(&compiled), + ), + target_query: Some(target_query_info(&selected_route)), paths, }; @@ -483,76 +2860,663 @@ pub extern "C" fn r2sym_explore_to( } #[unsafe(no_mangle)] -pub extern "C" fn r2sym_solve_to( +pub extern "C" fn r2sym_solve_to_scope( ctx: *const R2ILContext, - blocks: *const *const R2ILBlock, - num_blocks: usize, + functions: *const R2ILFunctionBlocks, + num_functions: usize, entry_addr: u64, target_addr: u64, + external_context_json: *const c_char, ) -> *mut c_char { + solve_pipeline_debug_stage("solve_to_scope:start"); let Some(ctx_view) = require_ctx_view(ctx) else { return sym_error_json("missing disassembler context"); }; - let Some(blocks) = (unsafe { BlockSlice::from_ffi(blocks, num_blocks) }) else { - return sym_error_json("no blocks to solve"); + solve_pipeline_debug_stage("solve_to_scope:ctx_ready"); + let Some(scope) = (unsafe { + build_symbolic_scope_from_ffi(functions, num_functions, ctx_view.arch, entry_addr) + }) else { + return sym_error_json("failed to build symbolic scope"); }; - - let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch) { - Some(prepared) => prepared, - None => return sym_error_json("failed to build SSA function"), + solve_pipeline_debug_log(&format!( + "solve_to_scope:scope_ready functions={}", + scope.functions().len() + )); + let scope = match scope_with_external_assumptions(&scope, ctx_view.arch, external_context_json) + { + Ok(scope) => scope, + Err(err) => return sym_error_json(err), + }; + solve_pipeline_debug_stage("solve_to_scope:assumptions_ready"); + let Some(prepared) = scope_root_prepared(&scope) else { + return sym_error_json("failed to build root SSA function"); }; + solve_pipeline_debug_log(&format!( + "solve_to_scope:prepared_ready blocks={} call_sites={}", + prepared.blocks().count(), + prepared.call_sites().by_id.len() + )); let symbol_map = symbol_map_snapshot(); + let mut query_config = sym_default_query_config(); let z3_ctx = Context::thread_local(); + solve_pipeline_debug_log(&format!( + "solve_to_scope:compile_begin target={:#x}", + target_addr + )); let solve_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let compiled = r2sym::compile_query_semantic_artifact_with_scope( + &z3_ctx, + prepared, + Some(&scope), + target_addr, + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + ); + solve_pipeline_debug_log(&format!( + "solve_to_scope:compile_done stage={:?} route={:?}", + compiled.stage, + compiled.target_query_route_plan(target_addr) + )); let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); - r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); - let mut explorer = r2sym::PathExplorer::with_config(&z3_ctx, sym_default_config()); - if let Some(arch) = ctx_view.arch - && let Some(registry) = r2sym::SummaryRegistry::with_core_for_arch(arch) - { - let interproc = build_seed_interproc_summary_set(&prepared, &symbol_map); - let _ = - registry.install_known_symbols_for_function(&mut explorer, &prepared, &symbol_map); - let _ = registry.install_interproc_summaries_for_function( - &mut explorer, - &prepared, - &interproc, - &symbol_map, - ); + r2sym::seed_scope_state_for_arch(&mut initial_state, prepared, &scope, ctx_view.arch); + solve_pipeline_debug_stage("solve_to_scope:seed_ready"); + let selected_route = predicted_target_query_route(PredictedTargetQueryRouteInput { + z3_ctx: &z3_ctx, + prepared, + scope: Some(&scope), + compiled: &compiled, + target_addr, + arch: ctx_view.arch, + symbol_map: &symbol_map, + summary_profile: query_config.summary_profile, + assumption_conflicted: prepared_assumption_conflicted(prepared), + }); + let query_policy = tune_query_config_for_state( + &mut query_config, + prepared, + &initial_state, + Some(&selected_route), + ); + solve_pipeline_debug_log(&format!( + "solve_to_scope:query_config max_states={} max_depth={} timeout_ms={}", + query_config.explore.max_states, + query_config.explore.max_depth, + query_config + .explore + .timeout + .map(|timeout| timeout.as_millis()) + .unwrap_or(0) + )); + let mut explorer = query_config.make_explorer(&z3_ctx); + r2sym::install_symbolic_hooks_for_query_policy( + &mut explorer, + &z3_ctx, + &scope, + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + &query_policy, + ); + solve_pipeline_debug_stage("solve_to_scope:hooks_ready"); + let solve = explorer.solve_for_target_with_artifact_in_scope( + prepared, + Some(&scope), + Some(&compiled), + initial_state, + target_addr, + ); + solve_pipeline_debug_log(&format!( + "solve_to_scope:solve_done status={:?} matched={} explored={} route={:?}", + solve.status, + solve.matched_paths.len(), + solve.stats.states_explored, + solve.selected_route + )); + let selected = solve + .selected_path_index + .and_then(|idx| solve.matched_paths.get(idx).map(|path| (idx, path))) + .map(|(idx, path)| { + path_info_from_result_with_solution(idx, path, &explorer, solve_found(solve.status)) + }); + ( + solve.status, + solve.matched_paths.len(), + selected, + solve.verification, + solve.witness, + solve.stats, + solve.solver_stats, + solve.assumption_usage, + solve.assumption_conditioned, + solve.summary_conditioned, + solve.selected_route, + compiled.clone(), + ) + })); + + let ( + status, + matched_paths, + selected_path, + verification, + witness, + stats, + solver_stats, + assumption_usage, + assumption_conditioned, + summary_conditioned, + selected_route, + compiled, + ) = match solve_result { + Ok(value) => value, + Err(err) => return sym_panic_json("symbolic execution failed", err), + }; + let output = SymTargetSolveResult { + entry: format!("0x{:x}", entry_addr), + target: format!("0x{:x}", target_addr), + status: solve_status_string(status), + matched_paths, + found: solve_found(status), + target_reached_under_model: verification.target_reached_under_model, + candidate_solution_verified: verification.candidate_solution_verified, + residual_reasons: verification.residual_reasons.clone(), + model_validation: model_validation_info(&verification.model_validation), + witness: solve_witness_info(&witness), + stats: build_sym_exec_summary( + &stats, + &solver_stats, + matched_paths, + assumption_usage, + assumption_conditioned, + summary_conditioned, + Some(&compiled), + ), + target_query: Some(target_query_info(&selected_route)), + selected_path, + }; + + match serde_json::to_string(&output) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => sym_error_json("failed to serialize symbolic solve output"), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_run_spec_json_scope( + ctx: *const R2ILContext, + functions: *const R2ILFunctionBlocks, + num_functions: usize, + entry_addr: u64, + spec_json: *const c_char, + external_context_json: *const c_char, +) -> *mut c_char { + if spec_json.is_null() { + return sym_error_json("missing exploration spec json"); + } + let spec_text = unsafe { + match CStr::from_ptr(spec_json).to_str() { + Ok(text) => text, + Err(_) => return sym_error_json("exploration spec is not valid utf-8"), } - let matched = explorer.find_paths_to(&prepared, initial_state, target_addr); + }; + let spec = match serde_json::from_str::(spec_text) { + Ok(spec) => spec, + Err(err) => return sym_error_json(&format!("failed to parse exploration spec: {err}")), + }; + if let Err(err) = spec.validate() { + return sym_error_json(&err); + } + + let Some(ctx_view) = require_ctx_view(ctx) else { + return sym_error_json("missing disassembler context"); + }; + let Some(scope) = (unsafe { + build_symbolic_scope_from_ffi(functions, num_functions, ctx_view.arch, entry_addr) + }) else { + return sym_error_json("failed to build symbolic scope"); + }; + let scope = match scope_with_external_assumptions(&scope, ctx_view.arch, external_context_json) + { + Ok(scope) => scope, + Err(err) => return sym_error_json(err), + }; + let Some(prepared) = scope_root_prepared(&scope) else { + return sym_error_json("failed to build root SSA function"); + }; + let (assumption_usage, assumption_conditioned) = prepared_assumption_conditioning(prepared); + let symbol_map = symbol_map_snapshot(); + let z3_ctx = Context::thread_local(); + let mut default_config = sym_default_query_config(); + + let run_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let start_pc = spec.start_pc(entry_addr)?; + let mut initial_state = r2sym::SymState::new(&z3_ctx, start_pc); + r2sym::seed_scope_state_for_arch(&mut initial_state, prepared, &scope, ctx_view.arch); + spec.apply_to_state(&mut initial_state); + let query_policy = + tune_query_config_for_state(&mut default_config, prepared, &initial_state, None); + + let mut explorer = r2sym::PathExplorer::with_config( + &z3_ctx, + spec.to_explore_config(&default_config.explore), + ); + r2sym::install_symbolic_hooks_for_query_policy( + &mut explorer, + &z3_ctx, + &scope, + ctx_view.arch, + &symbol_map, + default_config.summary_profile, + &query_policy, + ); + let result = explorer.run_spec(prepared, initial_state, &spec)?; let stats = explorer.stats().clone(); - let selected = matched + let solver_stats = explorer.solver().stats(); + let found_paths = result + .found_paths + .iter() + .enumerate() + .map(|(idx, path)| run_path_info_from_result(idx, path, &explorer)) + .collect::>(); + Ok::<_, String>((result, stats, solver_stats, found_paths)) + })); + + let (result, stats, solver_stats, found_paths) = match run_result { + Ok(Ok(value)) => value, + Ok(Err(err)) => return sym_error_json(&err), + Err(err) => return sym_panic_json("symbolic execution failed", err), + }; + + let output = SymRunResult { + entry: format!("0x{:x}", entry_addr), + spec, + stats: build_sym_exec_summary( + &stats, + &solver_stats, + found_paths.len(), + assumption_usage, + assumption_conditioned, + false, + None, + ), + stash_counts: SymRunStashCounts { + found: found_paths.len(), + avoided: result.avoided_states, + unsat: result.unsat_states, + errored: result.errored_states, + completed: result.completed_states, + }, + found_paths, + diagnostics: result.diagnostics, + }; + + match serde_json::to_string(&output) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => sym_error_json("failed to serialize symbolic run output"), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_explore_to_replay_scope( + ctx: *const R2ILContext, + functions: *const R2ILFunctionBlocks, + num_functions: usize, + entry_addr: u64, + target_addr: u64, + replay_seed: *const R2SymReplaySeed, + external_context_json: *const c_char, +) -> *mut c_char { + let Some(ctx_view) = require_ctx_view(ctx) else { + return sym_error_json("missing disassembler context"); + }; + let replay_seed = unsafe { + match replay_seed_from_ffi(replay_seed) { + Ok(seed) => seed, + Err(err) => return sym_error_json(err), + } + }; + let Some(scope) = (unsafe { + build_symbolic_scope_from_ffi(functions, num_functions, ctx_view.arch, entry_addr) + }) else { + return sym_error_json("failed to build symbolic scope"); + }; + let scope = match scope_with_external_assumptions(&scope, ctx_view.arch, external_context_json) + { + Ok(scope) => scope, + Err(err) => return sym_error_json(err), + }; + let Some(prepared) = scope_root_prepared(&scope) else { + return sym_error_json("failed to build root SSA function"); + }; + let symbol_map = symbol_map_snapshot(); + let mut query_config = sym_default_query_config(); + let start_pc = replay_seed.entry_pc.unwrap_or(entry_addr); + let z3_ctx = Context::thread_local(); + + let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let compiled = r2sym::compile_query_semantic_artifact_with_scope( + &z3_ctx, + prepared, + Some(&scope), + target_addr, + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + ); + let initial_state = + build_replay_seeded_state(&z3_ctx, entry_addr, prepared, ctx_view.arch, &replay_seed); + let selected_route = predicted_target_query_route(PredictedTargetQueryRouteInput { + z3_ctx: &z3_ctx, + prepared, + scope: Some(&scope), + compiled: &compiled, + target_addr, + arch: ctx_view.arch, + symbol_map: &symbol_map, + summary_profile: query_config.summary_profile, + assumption_conflicted: prepared_assumption_conflicted(prepared), + }); + let query_policy = tune_query_config_for_state( + &mut query_config, + prepared, + &initial_state, + Some(&selected_route), + ); + let mut explorer = query_config.make_explorer(&z3_ctx); + r2sym::install_symbolic_hooks_for_query_policy( + &mut explorer, + &z3_ctx, + &scope, + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + &query_policy, + ); + let reach = explorer.can_reach_with_artifact_in_scope( + prepared, + Some(&scope), + Some(&compiled), + initial_state, + target_addr, + ); + let paths: Vec = reach + .paths .iter() .enumerate() - .min_by_key(|(idx, path)| (path.num_constraints(), path.depth, *idx)) - .map(|(idx, path)| path_info_from_result(idx, path, &explorer)); - (matched.len(), selected, stats) + .map(|(i, r)| path_info_from_result(i, r, &explorer)) + .collect(); + ( + paths, + reach.stats, + reach.solver_stats, + reach.assumption_usage, + reach.assumption_conditioned, + reach.summary_conditioned, + reach.selected_route, + compiled.clone(), + ) + })); + + let ( + paths, + stats, + solver_stats, + assumption_usage, + assumption_conditioned, + summary_conditioned, + selected_route, + compiled, + ) = match explore_result { + Ok(value) => value, + Err(err) => { + return sym_panic_json("symbolic replay exploration failed", err); + } + }; + let output = SymTargetExploreResult { + entry: format!("0x{:x}", start_pc), + target: format!("0x{:x}", target_addr), + matched_paths: paths.len(), + stats: build_sym_exec_summary_with_semantic_info( + &stats, + &solver_stats, + paths.len(), + assumption_usage, + assumption_conditioned, + summary_conditioned, + Some(compiled_semantic_info_with_replay_seed( + &compiled, + &replay_seed, + )), + ), + target_query: Some(target_query_info(&selected_route)), + paths, + }; + + match serde_json::to_string(&output) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => sym_error_json("failed to serialize replay symbolic exploration output"), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_solve_to_replay_scope( + ctx: *const R2ILContext, + functions: *const R2ILFunctionBlocks, + num_functions: usize, + entry_addr: u64, + target_addr: u64, + replay_seed: *const R2SymReplaySeed, + external_context_json: *const c_char, +) -> *mut c_char { + let Some(ctx_view) = require_ctx_view(ctx) else { + return sym_error_json("missing disassembler context"); + }; + let replay_seed = unsafe { + match replay_seed_from_ffi(replay_seed) { + Ok(seed) => seed, + Err(err) => return sym_error_json(err), + } + }; + let Some(scope) = (unsafe { + build_symbolic_scope_from_ffi(functions, num_functions, ctx_view.arch, entry_addr) + }) else { + return sym_error_json("failed to build symbolic scope"); + }; + let scope = match scope_with_external_assumptions(&scope, ctx_view.arch, external_context_json) + { + Ok(scope) => scope, + Err(err) => return sym_error_json(err), + }; + let Some(prepared) = scope_root_prepared(&scope) else { + return sym_error_json("failed to build root SSA function"); + }; + let symbol_map = symbol_map_snapshot(); + let mut query_config = sym_default_query_config(); + let start_pc = replay_seed.entry_pc.unwrap_or(entry_addr); + let z3_ctx = Context::thread_local(); + + let solve_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let compiled = r2sym::compile_query_semantic_artifact_with_scope_and_replay_seed( + &z3_ctx, + prepared, + Some(&scope), + target_addr, + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + Some(&replay_seed), + ); + let initial_state = + build_replay_seeded_state(&z3_ctx, entry_addr, prepared, ctx_view.arch, &replay_seed); + let selected_route = predicted_target_query_route(PredictedTargetQueryRouteInput { + z3_ctx: &z3_ctx, + prepared, + scope: Some(&scope), + compiled: &compiled, + target_addr, + arch: ctx_view.arch, + symbol_map: &symbol_map, + summary_profile: query_config.summary_profile, + assumption_conflicted: prepared_assumption_conflicted(prepared), + }); + let query_policy = tune_query_config_for_state( + &mut query_config, + prepared, + &initial_state, + Some(&selected_route), + ); + let mut explorer = query_config.make_explorer(&z3_ctx); + r2sym::install_symbolic_hooks_for_query_policy( + &mut explorer, + &z3_ctx, + &scope, + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + &query_policy, + ); + let solve = explorer.solve_for_target_with_artifact_in_scope( + prepared, + Some(&scope), + Some(&compiled), + initial_state, + target_addr, + ); + let selected = solve + .selected_path_index + .and_then(|idx| solve.matched_paths.get(idx).map(|path| (idx, path))) + .map(|(idx, path)| { + path_info_from_result_with_solution(idx, path, &explorer, solve_found(solve.status)) + }); + ( + solve.status, + solve.matched_paths.len(), + selected, + solve.verification, + solve.witness, + solve.stats, + solve.solver_stats, + solve.assumption_usage, + solve.assumption_conditioned, + solve.summary_conditioned, + solve.selected_route, + compiled.clone(), + ) })); - let (matched_paths, selected_path, stats) = match solve_result { + let ( + status, + matched_paths, + selected_path, + verification, + witness, + stats, + solver_stats, + assumption_usage, + assumption_conditioned, + summary_conditioned, + selected_route, + compiled, + ) = match solve_result { Ok(value) => value, - Err(_) => return sym_error_json("symbolic execution failed (z3 context error)"), + Err(err) => return sym_panic_json("symbolic replay solve failed", err), }; let output = SymTargetSolveResult { - entry: format!("0x{:x}", entry_addr), + entry: format!("0x{:x}", start_pc), target: format!("0x{:x}", target_addr), + status: solve_status_string(status), matched_paths, - found: selected_path.is_some(), - stats: SymExecSummary { - paths_explored: stats.paths_completed, - paths_feasible: matched_paths, - paths_pruned: stats.paths_pruned, - max_depth: stats.max_depth_reached, - states_explored: stats.states_explored, - time_ms: stats.total_time.as_millis() as u64, - }, + found: solve_found(status), + target_reached_under_model: verification.target_reached_under_model, + candidate_solution_verified: verification.candidate_solution_verified, + residual_reasons: verification.residual_reasons.clone(), + model_validation: model_validation_info(&verification.model_validation), + witness: solve_witness_info(&witness), + stats: build_sym_exec_summary_with_semantic_info( + &stats, + &solver_stats, + matched_paths, + assumption_usage, + assumption_conditioned, + summary_conditioned, + Some(compiled_semantic_info_with_replay_seed( + &compiled, + &replay_seed, + )), + ), + target_query: Some(target_query_info(&selected_route)), selected_path, }; match serde_json::to_string(&output) { Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), - Err(_) => sym_error_json("failed to serialize symbolic solve output"), + Err(_) => sym_error_json("failed to serialize replay symbolic solve output"), + } +} + +#[cfg(test)] +mod tests { + use super::{build_sym_exec_summary, parse_scope_assumptions}; + use std::ffi::CString; + + #[test] + fn parse_scope_assumptions_reads_external_context_payload() { + let json = CString::new( + r#"{"assumptions":[{"subject":{"register":{"name":"rdi"}},"value":{"constant":{"value":4660}}}]}"#, + ) + .expect("json"); + let assumptions = parse_scope_assumptions(json.as_ptr(), None).expect("assumptions"); + assert_eq!(assumptions.items.len(), 1); + } + + #[test] + fn sym_exec_summary_serializes_assumption_usage_when_present() { + let summary = build_sym_exec_summary( + &r2sym::path::ExploreStats::default(), + &r2sym::SolverStats::default(), + 0, + r2ssa::AssumptionUsageReport { + applied: vec![r2ssa::AnalysisAssumption { + id: Some("arg0".to_string()), + subject: r2ssa::AssumptionSubject::Parameter { index: 0 }, + value: r2ssa::AssumptionValue::Constant { value: 7 }, + scope: r2ssa::AssumptionScope::Query, + provenance: r2ssa::AssumptionProvenance::User, + }], + ..r2ssa::AssumptionUsageReport::default() + }, + true, + false, + None, + ); + let json = serde_json::to_value(summary).expect("summary json"); + assert!(json.get("assumption_usage").is_some()); + assert_eq!( + json.get("assumption_conditioned") + .and_then(serde_json::Value::as_bool), + Some(true) + ); + assert_eq!(json.get("summary_conditioned"), None); + } + + #[test] + fn sym_exec_summary_serializes_summary_conditioning_when_present() { + let summary = build_sym_exec_summary( + &r2sym::path::ExploreStats::default(), + &r2sym::SolverStats::default(), + 0, + r2ssa::AssumptionUsageReport::default(), + false, + true, + None, + ); + let json = serde_json::to_value(summary).expect("summary json"); + assert_eq!( + json.get("summary_conditioned") + .and_then(serde_json::Value::as_bool), + Some(true) + ); } } diff --git a/r2plugin/src/bin/r2sleigh-plugin-install.rs b/r2plugin/src/bin/r2sleigh-plugin-install.rs index f1a81ac..d9db8c5 100644 --- a/r2plugin/src/bin/r2sleigh-plugin-install.rs +++ b/r2plugin/src/bin/r2sleigh-plugin-install.rs @@ -90,6 +90,6 @@ fn print_help() { println!( "r2sleigh-plugin-install\n\n\ Usage:\n r2sleigh-plugin-install [--features ] [--target ]\n\n\ -Options:\n --features Sleigh features (x86, arm[ARM32+AArch64], riscv, all-archs)\n --target RUST_TARGET for Makefile (release or debug)\n -h, --help Show this help message" +Options:\n --features Sleigh features (x86, arm[ARM32+AArch64], mips, riscv, all-archs)\n --target RUST_TARGET for Makefile (release or debug)\n -h, --help Show this help message" ); } diff --git a/r2plugin/src/decompiler.rs b/r2plugin/src/decompiler.rs index 966f88c..2678088 100644 --- a/r2plugin/src/decompiler.rs +++ b/r2plugin/src/decompiler.rs @@ -1,7 +1,7 @@ use crate::context::PluginCtxView; +use crate::parse_addr_name_map; use crate::types::FunctionAnalysisArtifact; -use crate::{decompile_artifact_guard_fallback, parse_addr_name_map}; -use r2il::{ArchSpec, R2ILBlock}; +use r2il::R2ILBlock; use std::collections::HashMap; pub(crate) struct DecompilerEnv { @@ -10,44 +10,8 @@ pub(crate) struct DecompilerEnv { pub(crate) cfg: r2dec::DecompilerConfig, } -pub(crate) fn normalize_sig_arch_name(arch: Option<&ArchSpec>) -> Option { - let arch = arch?; - let lower = arch.name.to_ascii_lowercase(); - if matches!(lower.as_str(), "x86-64" | "x86_64" | "x64" | "amd64") { - return Some("x86-64".to_string()); - } - if matches!(lower.as_str(), "x86" | "x86-32" | "i386" | "i686") { - return Some("x86".to_string()); - } - Some(arch.name.clone()) -} - -pub(crate) fn decompiler_config_for_arch_name( - arch_name: &str, - ptr_bits: u32, -) -> r2dec::DecompilerConfig { - match (arch_name, ptr_bits) { - ("x86", 32) | ("x86-32", _) => r2dec::DecompilerConfig::x86(), - ("x86-64", _) | ("x86_64", _) | ("x64", _) | ("amd64", _) => { - r2dec::DecompilerConfig::x86_64() - } - ("arm", _) | ("ARM", _) if ptr_bits == 32 => r2dec::DecompilerConfig::arm(), - ("aarch64", _) | ("arm64", _) | ("ARM64", _) => r2dec::DecompilerConfig::aarch64(), - ("riscv32", _) | ("rv32", _) | ("rv32gc", _) => r2dec::DecompilerConfig::riscv32(), - ("riscv64", _) | ("rv64", _) | ("rv64gc", _) => r2dec::DecompilerConfig::riscv64(), - ("riscv", _) if ptr_bits == 32 => r2dec::DecompilerConfig::riscv32(), - ("riscv", _) => r2dec::DecompilerConfig::riscv64(), - _ => r2dec::DecompilerConfig { - ptr_size: ptr_bits, - ..r2dec::DecompilerConfig::default() - }, - } -} - pub(crate) fn build_decompiler_env(ctx: &PluginCtxView<'_>) -> DecompilerEnv { - let arch_name = normalize_sig_arch_name(ctx.arch).unwrap_or_else(|| "unknown".to_string()); - let ptr_bits = ctx.arch.map(|arch| arch.addr_size * 8).unwrap_or(64); - let cfg = decompiler_config_for_arch_name(&arch_name, ptr_bits); + let (arch_name, ptr_bits, cfg) = r2dec::DecompilerConfig::for_arch(ctx.arch); DecompilerEnv { arch_name, ptr_bits, @@ -56,17 +20,19 @@ pub(crate) fn build_decompiler_env(ctx: &PluginCtxView<'_>) -> DecompilerEnv { } pub(crate) fn build_decompiler_context( - type_facts: r2types::FunctionTypeFacts, + function_facts: r2types::FunctionFacts, function_names: HashMap, strings: HashMap, symbols: HashMap, + ptr_bits: u32, ) -> r2dec::DecompilerContext { - r2dec::DecompilerContext { + r2dec::DecompilerContext::from_function_facts( + function_facts, function_names, strings, symbols, - type_facts, - } + ptr_bits, + ) } pub(crate) fn decompiler_input_from_artifact( @@ -74,23 +40,42 @@ pub(crate) fn decompiler_input_from_artifact( function_names: HashMap, strings: HashMap, symbols: HashMap, + ptr_bits: u32, ) -> r2dec::DecompilerInput { let FunctionAnalysisArtifact { ssa_func, - type_facts, - interproc_summary_set, + function_facts, .. } = artifact; r2dec::DecompilerInput::new( ssa_func, - build_decompiler_context(type_facts, function_names, strings, symbols), + build_decompiler_context(function_facts, function_names, strings, symbols, ptr_bits), ) - .with_interproc_summary_set(interproc_summary_set) +} + +fn rename_function_artifact_for_display( + artifact: FunctionAnalysisArtifact, + function_name: &str, +) -> FunctionAnalysisArtifact { + let FunctionAnalysisArtifact { + ssa_func, + pattern_ssa_func, + function_facts, + writeback_plan, + .. + } = artifact; + FunctionAnalysisArtifact { + ssa_func: ssa_func.with_name(function_name), + pattern_ssa_func: pattern_ssa_func.with_name(function_name), + function_facts, + writeback_plan, + } } #[allow(clippy::too_many_arguments)] pub(crate) fn run_full_decompile_on_large_stack( r2il_blocks: Vec, + fcn_addr: u64, func_name_str: String, arch: Option, ptr_bits: u32, @@ -101,59 +86,136 @@ pub(crate) fn run_full_decompile_on_large_stack( symbols_str: String, external_context_json: String, cached_artifact: Option, + symbolic_scope: Option, ) -> String { const STACK_SIZE: usize = 512 * 1024 * 1024; let handle = std::thread::Builder::new() .stack_size(STACK_SIZE) .spawn(move || { - if let Some(reason) = crate::decompiler_cfg_guard_reason(&r2il_blocks) { - return decompile_artifact_guard_fallback(&func_name_str, &reason); - } - let arch_name = - normalize_sig_arch_name(arch.as_ref()).unwrap_or_else(|| "unknown".to_string()); - let config = decompiler_config_for_arch_name(&arch_name, ptr_bits); + let cfg_guard_reason = r2dec::cfg_guard_reason(&r2il_blocks); + let (_, _, config) = r2dec::DecompilerConfig::for_arch(arch.as_ref()); let function_names = parse_addr_name_map(&func_names_str); let symbols = parse_addr_name_map(&symbols_str); + let display_func_name = crate::helpers::resolve_decompiler_display_name( + fcn_addr, + &func_name_str, + &function_names, + &symbols, + ); let mut artifact = if let Some(artifact) = cached_artifact { + if let Some(route) = r2dec::detached_semantic_route_plan( + &display_func_name, + &r2il_blocks, + &artifact.function_facts, + ) { + if let r2dec::SemanticRoutePlan::FallbackComment { comment } = route { + return comment; + } + if let Some(reason) = cfg_guard_reason.as_ref() + && matches!(route, r2dec::SemanticRoutePlan::Standard) + { + return r2dec::artifact_guard_fallback_comment(&func_name_str, reason); + } + } artifact } else { - let Some(artifact) = crate::types::build_detached_function_analysis_artifact( + let precomputed_semantic_artifact = cfg_guard_reason.as_ref().map_or_else( + || None, + |_| { + crate::types::collect_detached_semantic_artifact( + &r2il_blocks, + &func_name_str, + arch.as_ref(), + symbolic_scope.as_ref(), + ) + }, + ); + if let Some(route) = r2dec::detached_semantic_route_plan( + &display_func_name, &r2il_blocks, - &func_name_str, - arch.as_ref(), - ptr_bits, - semantic_metadata_enabled, - ®_type_hints, - &external_context_json, - ) else { - return decompile_artifact_guard_fallback( + &r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + precomputed_semantic_artifact.clone(), + ), + ) { + if let r2dec::SemanticRoutePlan::FallbackComment { comment } = route { + return comment; + } + if let Some(reason) = cfg_guard_reason.as_ref() + && matches!(route, r2dec::SemanticRoutePlan::Standard) + { + return r2dec::artifact_guard_fallback_comment(&func_name_str, reason); + } + } + let Some(artifact) = + crate::types::build_detached_function_analysis_artifact_with_scope_and_semantics( + &r2il_blocks, + &func_name_str, + arch.as_ref(), + ptr_bits, + semantic_metadata_enabled, + ®_type_hints, + &external_context_json, + symbolic_scope.as_ref(), + precomputed_semantic_artifact, + ) + else { + return r2dec::artifact_guard_fallback_comment( &func_name_str, "failed to build detached analysis artifact", ); }; artifact }; - crate::types::enrich_known_function_signatures_from_names( - &mut artifact.type_facts, - &function_names, - ptr_bits, - ); - crate::types::enrich_known_function_signatures_from_names( - &mut artifact.type_facts, - &symbols, - ptr_bits, - ); + artifact = rename_function_artifact_for_display(artifact, &display_func_name); + + if let Some(route) = r2dec::detached_semantic_route_plan( + &display_func_name, + &r2il_blocks, + &artifact.function_facts, + ) { + match route { + r2dec::SemanticRoutePlan::VmSummary { .. } => { + if let Some(output) = + r2dec::render_vm_semantic_summary(&display_func_name, &artifact.function_facts) + { + return output; + } + } + r2dec::SemanticRoutePlan::FallbackComment { comment } => return comment, + _ => {} + } + } let decompiler = r2dec::Decompiler::new(config); + let semantic_fallback_output = r2dec::semantic_fallback_comment( + &display_func_name, + artifact.function_facts.semantics.as_ref(), + ); let input = decompiler_input_from_artifact( artifact, function_names, parse_addr_name_map(&strings_str), symbols, + ptr_bits, ); - decompiler.decompile_input(&input) + let output = decompiler.decompile_input(&input); + if output.trim().is_empty() { + return semantic_fallback_output.unwrap_or_else(|| { + cfg_guard_reason + .as_ref() + .map(|reason| r2dec::artifact_guard_fallback_comment(&func_name_str, reason)) + .unwrap_or_else(|| { + format!( + "/* r2dec fallback: skipped decompilation for {} (empty output) */", + display_func_name + ) + }) + }); + } + output }); match handle { diff --git a/r2plugin/src/helpers.rs b/r2plugin/src/helpers.rs index b8cfe1b..b186dd9 100644 --- a/r2plugin/src/helpers.rs +++ b/r2plugin/src/helpers.rs @@ -1,4 +1,5 @@ use crate::ArchSpec; +use std::collections::HashMap; use std::ffi::CStr; use std::os::raw::c_char; @@ -31,70 +32,67 @@ pub(crate) fn cstr_or_default(ptr: *const c_char, default: &str) -> String { unsafe { CStr::from_ptr(ptr).to_string_lossy().to_string() } } -pub(crate) fn normalize_sim_name(name: &str) -> Option<&'static str> { - let normalized_owned = name.trim().to_ascii_lowercase(); - let mut normalized = normalized_owned.as_str(); - - for prefix in ["sym.imp.", "sym.", "imp.", "reloc.", "dbg."] { +fn strip_display_name_prefixes(name: &str) -> &str { + let mut normalized = name.trim(); + for prefix in ["sym.imp.", "sym.", "imp.", "reloc.", "dbg.", "fcn."] { while let Some(rest) = normalized.strip_prefix(prefix) { normalized = rest; } } + normalized +} - while let Some(rest) = normalized.strip_suffix("@plt") { - normalized = rest; - } - while let Some(rest) = normalized.strip_suffix(".plt") { - normalized = rest; - } - if let Some((base, _)) = normalized.split_once('@') { - normalized = base; +fn sanitize_c_identifier(name: &str) -> Option { + let trimmed = name.trim(); + if trimmed.is_empty() { + return None; } - if let Some(rest) = normalized.strip_prefix("__isoc99_") { - normalized = rest; + let mut out = String::new(); + for (idx, ch) in trimmed.chars().enumerate() { + let normalized = if ch.is_ascii_alphanumeric() || ch == '_' { + ch + } else { + '_' + }; + if idx == 0 && normalized.is_ascii_digit() { + out.push('_'); + } + out.push(normalized); } - if let Some(rest) = normalized.strip_prefix("__gi_") { - normalized = rest; + + if out.chars().all(|c| c == '_') { + None + } else { + Some(out) } +} - match normalized { - "strlen" | "__strlen_chk" => Some("strlen"), - "strcmp" => Some("strcmp"), - "memcmp" => Some("memcmp"), - "memcpy" | "__memcpy_chk" => Some("memcpy"), - "memset" => Some("memset"), - "malloc" | "__libc_malloc" | "__gi___libc_malloc" => Some("malloc"), - "free" => Some("free"), - "puts" => Some("puts"), - "printf" | "__printf_chk" => Some("printf"), - "exit" | "_exit" => Some("exit"), - _ => { - if normalized.starts_with("strlen") { - Some("strlen") - } else if normalized.starts_with("strcmp") { - Some("strcmp") - } else if normalized.starts_with("memcmp") { - Some("memcmp") - } else if normalized.starts_with("memcpy") { - Some("memcpy") - } else if normalized.starts_with("memset") { - Some("memset") - } else if normalized.starts_with("printf") || normalized == "__printf_chk" { - Some("printf") - } else if normalized.starts_with("puts") { - Some("puts") - } else if normalized == "malloc" || normalized.ends_with("malloc") { - Some("malloc") - } else if normalized == "free" || normalized.ends_with("free") { - Some("free") - } else if normalized.starts_with("exit") { - Some("exit") - } else { - None - } +pub(crate) fn resolve_decompiler_display_name( + fcn_addr: u64, + raw_name: &str, + function_names: &HashMap, + symbols: &HashMap, +) -> String { + for candidate in [ + symbols.get(&fcn_addr).map(String::as_str), + function_names.get(&fcn_addr).map(String::as_str), + Some(raw_name), + ] { + let Some(candidate) = candidate else { + continue; + }; + let stripped = strip_display_name_prefixes(candidate); + if let Some(clean) = sanitize_c_identifier(stripped) { + return clean; } } + + if fcn_addr == 0 { + "func".to_string() + } else { + format!("sub_{fcn_addr:x}") + } } pub(crate) fn effective_addr_size_bytes(arch: &ArchSpec) -> u32 { @@ -137,3 +135,36 @@ pub(crate) fn effective_addr_size_bytes(arch: &ArchSpec) -> u32 { pub(crate) fn effective_ptr_bits(arch: &ArchSpec) -> u32 { effective_addr_size_bytes(arch).saturating_mul(8) } + +#[cfg(test)] +mod tests { + use super::resolve_decompiler_display_name; + use std::collections::HashMap; + + #[test] + fn decompiler_display_name_prefers_symbol_name() { + let function_names = HashMap::from([(0x401617, "dbg.test_symbolic_xor_guard".to_string())]); + let symbols = HashMap::from([(0x401617, "test_symbolic_xor_guard".to_string())]); + + let display = resolve_decompiler_display_name( + 0x401617, + "dbg.test_symbolic_xor_guard", + &function_names, + &symbols, + ); + + assert_eq!(display, "test_symbolic_xor_guard"); + } + + #[test] + fn decompiler_display_name_strips_debug_prefix_without_symbol_hint() { + let display = resolve_decompiler_display_name( + 0x401617, + "dbg.test_symbolic_xor_guard", + &HashMap::new(), + &HashMap::new(), + ); + + assert_eq!(display, "test_symbolic_xor_guard"); + } +} diff --git a/r2plugin/src/lib.rs b/r2plugin/src/lib.rs index 927479d..e6d3b2d 100644 --- a/r2plugin/src/lib.rs +++ b/r2plugin/src/lib.rs @@ -17,6 +17,8 @@ mod decompiler; mod helpers; mod types; +#[cfg(test)] +use analysis::ssa::{r2il_block_defuse_json, r2il_block_to_ssa_json}; use r2il::serialize::UserOpDef; use r2il::{ArchSpec, R2ILBlock, R2ILOp, Varnode, serialize, validate_block_full}; use r2sleigh_export::{ @@ -24,16 +26,16 @@ use r2sleigh_export::{ }; use r2sleigh_lift::{Disassembler, SemanticMetadataOptions, build_arch_spec, userop_map_for_arch}; use std::ffi::{CStr, CString}; +use std::fs::OpenOptions; +use std::io::Write; use std::os::raw::c_char; use std::path::Path; use std::ptr; use std::slice; -use types::{recover_vars_arch_profile, size_to_type, ssa_var_block_key}; - -#[cfg(test)] -use analysis::ssa::{r2il_block_defuse_json, r2il_block_to_ssa_json}; #[cfg(test)] use types::parse_const_value; +#[cfg(test)] +use types::{recover_vars_arch_profile, size_to_type, ssa_var_block_key}; /// Opaque context handle for C API. pub struct R2ILContext { @@ -305,6 +307,20 @@ pub extern "C" fn r2il_get_reg_profile(ctx: *const R2ILContext) -> *mut c_char { profile.push_str(&format!("gpr\t{}\t.{}\t{}\t0\n", name_lower, bits, offset)); } + let mut stripped_aliases = Vec::new(); + for (original, (bits, offset, _)) in ®_meta { + if let Some(stripped) = original.strip_prefix('$') + && !stripped.is_empty() + && !reg_meta.contains_key(stripped) + { + stripped_aliases.push((stripped.to_string(), *bits, *offset)); + } + } + for (alias, bits, offset) in stripped_aliases { + profile.push_str(&format!("gpr\t{}\t.{}\t{}\t0\n", alias, bits, offset)); + reg_meta.insert(alias.clone(), (bits, offset, alias)); + } + // Synthesize missing aliases expected by radare2/ESIL for specific arches. let mut add_gpr_alias = |alias_name: &str, source_name: &str| { let alias_lower = alias_name.to_ascii_lowercase(); @@ -336,19 +352,19 @@ pub extern "C" fn r2il_get_reg_profile(ctx: *const R2ILContext) -> *mut c_char { .find_map(|name| reg_meta.get(*name).map(|(_, _, original)| original.clone())) }; - let pc = first_existing(&["pc", "rip", "eip", "ip"]); - let sp = first_existing(&["sp", "rsp", "esp"]); - let bp = first_existing(&["bp", "rbp", "ebp", "fp", "x29"]); + let pc = first_existing(&["pc", "$pc", "rip", "eip", "ip"]); + let sp = first_existing(&["sp", "$sp", "rsp", "esp"]); + let bp = first_existing(&["bp", "rbp", "ebp", "fp", "$fp", "s8", "$s8", "x29"]); let mut a_roles: [Option; 8] = std::array::from_fn(|_| None); - a_roles[0] = first_existing(&["rdi", "a0", "x0", "w0", "r0"]); - a_roles[1] = first_existing(&["rsi", "a1", "x1", "w1", "r1"]); - a_roles[2] = first_existing(&["rdx", "a2", "x2", "w2", "r2"]); - a_roles[3] = first_existing(&["rcx", "a3", "x3", "w3", "r3"]); + a_roles[0] = first_existing(&["rdi", "a0", "$a0", "x0", "w0", "r0"]); + a_roles[1] = first_existing(&["rsi", "a1", "$a1", "x1", "w1", "r1"]); + a_roles[2] = first_existing(&["rdx", "a2", "$a2", "x2", "w2", "r2"]); + a_roles[3] = first_existing(&["rcx", "a3", "$a3", "x3", "w3", "r3"]); let mut r_roles: [Option; 4] = std::array::from_fn(|_| None); - r_roles[0] = first_existing(&["r0", "rax", "eax", "v0", "x0", "w0"]); - r_roles[1] = first_existing(&["r1", "x1", "w1"]); + r_roles[0] = first_existing(&["r0", "rax", "eax", "v0", "$v0", "x0", "w0"]); + r_roles[1] = first_existing(&["r1", "v1", "$v1", "x1", "w1"]); r_roles[2] = first_existing(&["r2", "x2", "w2"]); r_roles[3] = first_existing(&["r3", "x3", "w3"]); @@ -2285,6 +2301,74 @@ fn create_disassembler_for_arch(arch: &str) -> Result<(ArchSpec, Disassembler), let (spec, dis) = apply_userop_map(spec, dis, "arm64"); Ok((spec, dis)) } + #[cfg(feature = "mips")] + "mips" | "mips32" | "mips32be" | "mipsbe" | "mipseb" => { + let spec = build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS32BE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32be", + ) + .map_err(|e| e.to_string())?; + let dis = Disassembler::from_sla( + sleigh_config::processor_mips::SLA_MIPS32BE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32be", + ) + .map_err(|e| e.to_string())?; + let (spec, dis) = apply_userop_map(spec, dis, "mips32be"); + Ok((spec, dis)) + } + #[cfg(feature = "mips")] + "mipsel" | "mips32le" | "mips32el" => { + let spec = build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS32LE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32le", + ) + .map_err(|e| e.to_string())?; + let dis = Disassembler::from_sla( + sleigh_config::processor_mips::SLA_MIPS32LE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32le", + ) + .map_err(|e| e.to_string())?; + let (spec, dis) = apply_userop_map(spec, dis, "mips32le"); + Ok((spec, dis)) + } + #[cfg(feature = "mips")] + "mips64" | "mips64be" => { + let spec = build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS64BE, + sleigh_config::processor_mips::PSPEC_MIPS64, + "mips64be", + ) + .map_err(|e| e.to_string())?; + let dis = Disassembler::from_sla( + sleigh_config::processor_mips::SLA_MIPS64BE, + sleigh_config::processor_mips::PSPEC_MIPS64, + "mips64be", + ) + .map_err(|e| e.to_string())?; + let (spec, dis) = apply_userop_map(spec, dis, "mips64be"); + Ok((spec, dis)) + } + #[cfg(feature = "mips")] + "mips64el" | "mips64le" => { + let spec = build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS64LE, + sleigh_config::processor_mips::PSPEC_MIPS64, + "mips64le", + ) + .map_err(|e| e.to_string())?; + let dis = Disassembler::from_sla( + sleigh_config::processor_mips::SLA_MIPS64LE, + sleigh_config::processor_mips::PSPEC_MIPS64, + "mips64le", + ) + .map_err(|e| e.to_string())?; + let (spec, dis) = apply_userop_map(spec, dis, "mips64le"); + Ok((spec, dis)) + } #[cfg(feature = "riscv")] "riscv64" | "rv64" | "rv64gc" => { let spec = build_arch_spec( @@ -2325,11 +2409,16 @@ fn create_disassembler_for_arch(arch: &str) -> Result<(ArchSpec, Disassembler), supported.extend(["x86-64", "x86"]); #[cfg(feature = "arm")] supported.extend(["arm", "arm64", "aarch64"]); + #[cfg(feature = "mips")] + supported.extend(["mips32be", "mips32le", "mips64be", "mips64le"]); #[cfg(feature = "riscv")] supported.extend(["riscv64", "riscv32"]); if supported.is_empty() { - Err("No architectures enabled; build with feature x86, arm, or riscv".to_string()) + Err( + "No architectures enabled; build with feature x86, arm, mips, or riscv" + .to_string(), + ) } else { Err(format!( "Unknown architecture '{}'. Supported: {}", @@ -2375,41 +2464,54 @@ fn decompiler_max_blocks() -> usize { .unwrap_or(200) } -fn decompiler_cfg_guard_reason_from_summary(summary: &r2ssa::CFGRiskSummary) -> Option { - if summary.loop_count > 8 || summary.back_edge_count > 16 { - return Some(format!( - "complex loop graph (loops={}, back_edges={})", - summary.loop_count, summary.back_edge_count - )); - } - - if summary.loop_count > 4 && summary.block_count >= 96 && summary.max_switch_cases >= 32 { - return Some(format!( - "large dense switch in looped CFG (blocks={}, loops={}, max_switch_cases={})", - summary.block_count, summary.loop_count, summary.max_switch_cases - )); - } - - None -} - -fn decompiler_cfg_guard_reason(blocks: &[R2ILBlock]) -> Option { - let ssa_func = r2ssa::SSAFunction::from_blocks_raw_no_arch(blocks)?; - decompiler_cfg_guard_reason_from_summary(&ssa_func.cfg_risk_summary()) -} - -fn decompile_block_guard_fallback(func_name: &str, blocks: usize, max_blocks: usize) -> String { - format!( - "/* r2dec fallback: skipped decompilation for {} ({} blocks > limit {}). Set SLEIGH_DEC_MAX_BLOCKS to override. */", - func_name, blocks, max_blocks - ) +#[unsafe(no_mangle)] +pub extern "C" fn r2dec_block_guard_comment_ffi( + func_name: *const c_char, + blocks: usize, + max_blocks: usize, +) -> *mut c_char { + let func_name = if func_name.is_null() { + "unknown".to_string() + } else { + unsafe { CStr::from_ptr(func_name) } + .to_str() + .unwrap_or("unknown") + .to_string() + }; + CString::new(r2dec::block_guard_fallback_comment( + &func_name, blocks, max_blocks, + )) + .map_or(ptr::null_mut(), |c| c.into_raw()) } -fn decompile_artifact_guard_fallback(func_name: &str, reason: &str) -> String { - format!( - "/* r2dec fallback: skipped decompilation for {} ({}) */", - func_name, reason - ) +#[unsafe(no_mangle)] +pub extern "C" fn r2dec_cfg_guard_comment_ffi( + func_name: *const c_char, + block_count: usize, + loop_count: usize, + back_edge_count: usize, + max_switch_cases: usize, +) -> *mut c_char { + let func_name = if func_name.is_null() { + "unknown".to_string() + } else { + unsafe { CStr::from_ptr(func_name) } + .to_str() + .unwrap_or("unknown") + .to_string() + }; + let summary = r2ssa::CFGRiskSummary { + block_count, + loop_count, + back_edge_count, + switch_block_count: usize::from(max_switch_cases > 0), + max_switch_cases, + }; + let Some(reason) = r2dec::cfg_guard_reason_from_summary(&summary) else { + return ptr::null_mut(); + }; + CString::new(r2dec::artifact_guard_fallback_comment(&func_name, &reason)) + .map_or(ptr::null_mut(), |c| c.into_raw()) } #[cfg(test)] @@ -2564,6 +2666,7 @@ fn parse_addr_name_map(json_str: &str) -> std::collections::HashMap .unwrap_or_default() } +#[cfg(test)] fn sanitize_c_identifier(name: &str) -> Option { let trimmed = name.trim(); if trimmed.is_empty() { @@ -2590,6 +2693,7 @@ fn sanitize_c_identifier(name: &str) -> Option { } } +#[cfg(test)] fn uniquify_name(base: String, used: &mut std::collections::HashSet) -> String { if used.insert(base.clone()) { return base; @@ -2623,6 +2727,7 @@ fn is_low_quality_stack_name(name: &str) -> bool { || is_generic_arg_name(&lower) } +#[cfg(test)] fn parse_external_type(raw_ty: &str, ptr_bits: u32) -> Option { let normalized = normalize_external_type_name(raw_ty); let parsed = r2types::parse_type_like_spec(&normalized, ptr_bits)?; @@ -2905,6 +3010,35 @@ pub extern "C" fn r2dec_function_with_context( symbols_json: *const c_char, external_context_json: *const c_char, ) -> *mut c_char { + r2dec_function_with_context_impl(R2DecFunctionWithContextInputs { + ctx, + blocks, + num_blocks, + fcn_addr: 0, + func_name, + func_names_json, + strings_json, + symbols_json, + external_context_json, + scope_functions: ptr::null(), + scope_num_functions: 0, + }) +} + +fn r2dec_function_with_context_impl(inputs: R2DecFunctionWithContextInputs) -> *mut c_char { + let R2DecFunctionWithContextInputs { + ctx, + blocks, + num_blocks, + fcn_addr, + func_name, + func_names_json, + strings_json, + symbols_json, + external_context_json, + scope_functions, + scope_num_functions, + } = inputs; let Some(ctx_view) = context::require_ctx_view(ctx) else { return ptr::null_mut(); }; @@ -2916,7 +3050,8 @@ pub extern "C" fn r2dec_function_with_context( let ptr_bits = ctx_view.arch.map(helpers::effective_ptr_bits).unwrap_or(64); let max_blocks = decompiler_max_blocks(); if block_slice.len() > max_blocks { - let output = decompile_block_guard_fallback(&func_name_str, block_slice.len(), max_blocks); + let output = + r2dec::block_guard_fallback_comment(&func_name_str, block_slice.len(), max_blocks); return CString::new(output).map_or(ptr::null_mut(), |c| c.into_raw()); } @@ -2926,9 +3061,25 @@ pub extern "C" fn r2dec_function_with_context( let strings_str = helpers::cstr_or_default(strings_json, "{}"); let symbols_str = helpers::cstr_or_default(symbols_json, "{}"); let external_context_str = helpers::cstr_or_default(external_context_json, "{}"); - let cached_artifact = types::build_function_input(ctx, blocks, num_blocks, 0, func_name) + let symbolic_scope = if scope_functions.is_null() || scope_num_functions == 0 { + None + } else { + unsafe { + analysis::sym::build_symbolic_scope_from_ffi( + scope_functions, + scope_num_functions, + ctx_view.arch, + fcn_addr, + ) + } + }; + let cached_artifact = types::build_function_input(ctx, blocks, num_blocks, fcn_addr, func_name) .and_then(|input| { - types::get_cached_function_analysis_artifact(&input, &external_context_str) + types::get_cached_function_analysis_artifact_with_scope( + &input, + &external_context_str, + symbolic_scope.as_ref(), + ) }); let semantic_metadata_enabled = ctx_view.semantic_metadata_enabled; let reg_type_hints = if semantic_metadata_enabled { @@ -2943,6 +3094,7 @@ pub extern "C" fn r2dec_function_with_context( // stack to prevent stack overflow on complex O2-optimized CFGs. let output = decompiler::run_full_decompile_on_large_stack( block_slice.into_inner(), + fcn_addr, func_name_str, arch_clone, ptr_bits, @@ -2953,11 +3105,57 @@ pub extern "C" fn r2dec_function_with_context( symbols_str, external_context_str, cached_artifact, + symbolic_scope, ); CString::new(output).map_or(ptr::null_mut(), |c| c.into_raw()) } +struct R2DecFunctionWithContextInputs { + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + func_name: *const c_char, + func_names_json: *const c_char, + strings_json: *const c_char, + symbols_json: *const c_char, + external_context_json: *const c_char, + scope_functions: *const analysis::sym::R2ILFunctionBlocks, + scope_num_functions: usize, +} + +/// Decompile a function with external context and a prepared symbolic helper scope. +/// Returns C code as a string. Caller must free with r2il_string_free(). +#[unsafe(no_mangle)] +pub extern "C" fn r2dec_function_with_context_scope( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + func_name: *const c_char, + func_names_json: *const c_char, + strings_json: *const c_char, + symbols_json: *const c_char, + external_context_json: *const c_char, + scope_functions: *const analysis::sym::R2ILFunctionBlocks, + scope_num_functions: usize, +) -> *mut c_char { + r2dec_function_with_context_impl(R2DecFunctionWithContextInputs { + ctx, + blocks, + num_blocks, + fcn_addr, + func_name, + func_names_json, + strings_json, + symbols_json, + external_context_json, + scope_functions, + scope_num_functions, + }) +} + /// Decompile a single basic block to C code. /// Returns C code as a string. Caller must free with r2il_string_free(). #[unsafe(no_mangle)] @@ -3034,6 +3232,7 @@ pub extern "C" fn r2dec_block_ast_json( // ============================================================================ #[derive(Debug, Clone)] +#[cfg(test)] pub(crate) struct InferredParam { name: String, ty: r2dec::CType, @@ -3042,39 +3241,8 @@ pub(crate) struct InferredParam { evidence: TypeEvidence, } -#[derive(Debug, Clone, Default, PartialEq, Eq)] -struct TypeEvidence { - pointer_proven: u8, - pointer_likely: u8, - scalar_proven: u8, - scalar_likely: u8, - bool_like: u8, - width_bits: u32, -} - -impl TypeEvidence { - fn pointer_score(&self) -> u16 { - (self.pointer_proven as u16) * 4 + (self.pointer_likely as u16) * 2 - } - - fn scalar_score(&self) -> u16 { - (self.scalar_proven as u16) * 4 - + (self.scalar_likely as u16) * 2 - + (self.bool_like as u16) * 3 - } - - fn has_pointer_signal(&self) -> bool { - self.pointer_proven > 0 || self.pointer_likely > 0 - } - - fn has_scalar_signal(&self) -> bool { - self.scalar_proven > 0 || self.scalar_likely > 0 || self.bool_like > 0 - } - - fn has_conflict(&self) -> bool { - self.has_pointer_signal() && self.has_scalar_signal() - } -} +#[cfg(test)] +type TypeEvidence = r2types::SignatureTypeEvidence; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct InferredParamJson { @@ -3183,9 +3351,116 @@ struct InferredTypeWritebackJson { struct_decls: Vec, global_type_links: Vec, interproc: InterprocSummaryJson, + plans: r2types::AnalysisPlans, + #[serde(skip_serializing_if = "r2ssa::AssumptionSet::is_empty")] + assumptions: r2ssa::AssumptionSet, + #[serde(skip_serializing_if = "r2types::AssumptionUsageReport::is_empty")] + assumption_usage: r2types::AssumptionUsageReport, + #[serde(skip_serializing_if = "Option::is_none")] + semantics: Option, + #[serde(skip_serializing_if = "Option::is_none")] + compiled_semantics: Option, diagnostics: TypeWritebackDiagnosticsJson, } +#[derive(Debug, serde::Serialize)] +struct CfgRiskSummaryJson { + block_count: usize, + loop_count: usize, + back_edge_count: usize, + switch_block_count: usize, + max_switch_cases: usize, +} + +#[derive(Debug, serde::Serialize)] +struct SemanticRoutePlanJson { + kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + reason: Option, + #[serde(skip_serializing_if = "Option::is_none")] + comment: Option, +} + +#[derive(Debug, serde::Serialize)] +struct FunctionFactsReportJson { + function_name: String, + function_addr: u64, + cfg_risk: CfgRiskSummaryJson, + #[serde(skip_serializing_if = "Option::is_none")] + semantic_build_plan: Option, + plans: r2types::AnalysisPlans, + #[serde(skip_serializing_if = "Option::is_none")] + semantic_route: Option, + #[serde(skip_serializing_if = "r2ssa::AssumptionSet::is_empty")] + assumptions: r2ssa::AssumptionSet, + #[serde(skip_serializing_if = "r2types::AssumptionUsageReport::is_empty")] + assumption_usage: r2types::AssumptionUsageReport, + #[serde(skip_serializing_if = "Option::is_none")] + summary_diagnostics: Option, + type_writeback: InferredTypeWritebackJson, +} + +#[derive(Debug, serde::Serialize)] +struct FunctionPlanReportJson { + function_name: String, + function_addr: u64, + cfg_risk: CfgRiskSummaryJson, + #[serde(skip_serializing_if = "Option::is_none")] + semantic: Option, + #[serde(skip_serializing_if = "Option::is_none")] + semantic_build_plan: Option, + plans: r2types::AnalysisPlans, + #[serde(skip_serializing_if = "Option::is_none")] + semantic_route: Option, + #[serde(skip_serializing_if = "r2ssa::AssumptionSet::is_empty")] + assumptions: r2ssa::AssumptionSet, + #[serde(skip_serializing_if = "r2types::AssumptionUsageReport::is_empty")] + assumption_usage: r2types::AssumptionUsageReport, + #[serde(skip_serializing_if = "Option::is_none")] + summary_diagnostics: Option, + prefer_bounded_type_plan: bool, +} + +fn cfg_risk_summary_json(summary: r2ssa::CFGRiskSummary) -> CfgRiskSummaryJson { + CfgRiskSummaryJson { + block_count: summary.block_count, + loop_count: summary.loop_count, + back_edge_count: summary.back_edge_count, + switch_block_count: summary.switch_block_count, + max_switch_cases: summary.max_switch_cases, + } +} + +fn semantic_route_plan_json(route: r2dec::SemanticRoutePlan) -> SemanticRoutePlanJson { + match route { + r2dec::SemanticRoutePlan::Standard => SemanticRoutePlanJson { + kind: "standard".to_string(), + reason: None, + comment: None, + }, + r2dec::SemanticRoutePlan::StructuredWorker { reason } => SemanticRoutePlanJson { + kind: "structured_worker".to_string(), + reason: Some(reason), + comment: None, + }, + r2dec::SemanticRoutePlan::LinearWorker { reason } => SemanticRoutePlanJson { + kind: "linear_worker".to_string(), + reason: Some(reason), + comment: None, + }, + r2dec::SemanticRoutePlan::VmSummary { reason } => SemanticRoutePlanJson { + kind: "vm_summary".to_string(), + reason: Some(reason), + comment: None, + }, + r2dec::SemanticRoutePlan::FallbackComment { comment } => SemanticRoutePlanJson { + kind: "fallback_comment".to_string(), + reason: None, + comment: Some(comment), + }, + } +} + fn evidence_json(evidence: &[r2types::WritebackEvidence]) -> Vec { evidence .iter() @@ -3208,6 +3483,9 @@ fn struct_fields_json(fields: &[r2types::StructFieldCandidate]) -> Vec, + compiled_semantics: Option, ) -> InferredTypeWritebackJson { InferredTypeWritebackJson { function_name: plan.signature.function_name, @@ -3275,6 +3553,11 @@ fn writeback_plan_json( }) .collect(), interproc, + plans: function_facts.plans.clone(), + assumptions: function_facts.assumptions.clone(), + assumption_usage: function_facts.assumption_usage.clone(), + semantics, + compiled_semantics, diagnostics: TypeWritebackDiagnosticsJson { conflicts: plan.diagnostics.conflicts, warnings: plan.diagnostics.warnings, @@ -3283,302 +3566,262 @@ fn writeback_plan_json( } } -#[cfg(test)] -const SIG_WRITEBACK_CONFIDENCE_MIN: u8 = 70; -#[cfg(test)] -const CC_WRITEBACK_CONFIDENCE_MIN: u8 = 80; - -#[derive(Debug, Default)] -struct SignatureTypeEvidenceContext { - pointer_vars: std::collections::HashSet, - scalar_proven_vars: std::collections::HashSet, - scalar_likely_vars: std::collections::HashSet, - bool_like_vars: std::collections::HashSet, - width_bits: std::collections::HashMap, +fn parse_nonempty_scope_json(payload: &str) -> Option { + serde_json::from_str::(payload) + .ok() + .filter(|value| { + !value.is_null() + && value + .as_object() + .map(|object| !object.is_empty()) + .unwrap_or(true) + }) } -fn merge_initial_type_evidence(initial_ty: &r2dec::CType, evidence: &mut TypeEvidence) { - match initial_ty { - r2dec::CType::Pointer(_) => evidence.pointer_likely = evidence.pointer_likely.max(1), - r2dec::CType::Bool => evidence.bool_like = evidence.bool_like.max(1), - r2dec::CType::Int(bits) | r2dec::CType::UInt(bits) => { - evidence.scalar_likely = evidence.scalar_likely.max(1); - if !(evidence.has_scalar_signal() - && !evidence.has_pointer_signal() - && evidence.width_bits > 0 - && evidence.width_bits < *bits) - { - evidence.width_bits = evidence.width_bits.max(*bits); - } - } - r2dec::CType::Float(bits) => { - evidence.scalar_proven = evidence.scalar_proven.max(1); - evidence.width_bits = evidence.width_bits.max(*bits); - } - _ => {} +fn symbolic_scope_view_json( + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + let scope = symbolic_scope?; + let payloads = scope + .helper_functions() + .filter_map(|function| { + function.name.as_ref().map(|name| { + serde_json::json!({ + "function_addr": function.id.0, + "function_name": name, + }) + }) + }) + .collect::>(); + let seeds = scope + .helper_functions() + .filter_map(|function| { + function.name.as_ref().map(|name| { + serde_json::json!({ + "id": function.id.0, + "name": name, + }) + }) + }) + .collect::>(); + if seeds.is_empty() && payloads.is_empty() { + return None; } + Some(serde_json::json!({ + "phase": "symbolic_scope", + "payloads": payloads, + "seeds": seeds, + })) } -fn fallback_scalar_type( - var_size_bytes: u32, - evidence: &TypeEvidence, - ptr_bits: u32, -) -> r2dec::CType { - if evidence.bool_like > 0 - && evidence.pointer_score() == 0 - && evidence.scalar_proven == 0 - && evidence.scalar_likely <= 1 - { - return r2dec::CType::Bool; - } - - let carrier_bits = var_size_bytes.saturating_mul(8); - let width_bits = if evidence.has_scalar_signal() - && !evidence.has_pointer_signal() - && evidence.width_bits > 0 - { - evidence.width_bits - } else { - evidence.width_bits.max(carrier_bits) +fn merged_interproc_scope_json( + scope_json: &str, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + let Some(symbolic_scope_json) = symbolic_scope_view_json(symbolic_scope) else { + return parse_nonempty_scope_json(scope_json); }; - let width_bits = match width_bits { - 0 => { - if ptr_bits >= 64 { - 64 - } else { - 32 - } - } - 1 => 8, - 2..=8 => 8, - 9..=16 => 16, - 17..=32 => 32, - _ => 64, + let Some(mut merged) = parse_nonempty_scope_json(scope_json) else { + return Some(symbolic_scope_json); }; - - r2dec::CType::Int(width_bits) -} - -fn materialize_signature_ctype(ty: r2dec::CType, ptr_bits: u32) -> r2dec::CType { - match ty { - r2dec::CType::Pointer(inner) => { - if matches!(*inner, r2dec::CType::Unknown | r2dec::CType::Void) - || matches!( - inner.as_ref(), - r2dec::CType::Struct(name) - | r2dec::CType::Union(name) - | r2dec::CType::Enum(name) - if is_unmaterialized_aggregate_name(name) - ) - { - return r2dec::CType::void_ptr(); - } - let inner = materialize_signature_ctype(*inner, ptr_bits); - r2dec::CType::ptr(inner) - } - r2dec::CType::Array(inner, len) => { - if matches!(*inner, r2dec::CType::Unknown | r2dec::CType::Void) { - return r2dec::CType::Array(Box::new(r2dec::CType::u8()), len); - } - let inner = materialize_signature_ctype(*inner, ptr_bits); - r2dec::CType::Array(Box::new(inner), len) - } - r2dec::CType::Function { ret, params } => { - let ret = materialize_signature_ctype(*ret, ptr_bits); - let ret = if matches!(ret, r2dec::CType::Unknown) { - fallback_scalar_type((ptr_bits / 8).max(1), &TypeEvidence::default(), ptr_bits) - } else { - ret - }; - let params = params - .into_iter() - .map(|param| materialize_signature_ctype(param, ptr_bits)) - .collect(); - r2dec::CType::Function { - ret: Box::new(ret), - params, - } - } - r2dec::CType::Unknown => { - fallback_scalar_type((ptr_bits / 8).max(1), &TypeEvidence::default(), ptr_bits) - } - r2dec::CType::Struct(name) if is_unmaterialized_aggregate_name(&name) => { - fallback_scalar_type((ptr_bits / 8).max(1), &TypeEvidence::default(), ptr_bits) - } - r2dec::CType::Union(name) if is_unmaterialized_aggregate_name(&name) => { - fallback_scalar_type((ptr_bits / 8).max(1), &TypeEvidence::default(), ptr_bits) - } - r2dec::CType::Enum(name) if is_unmaterialized_aggregate_name(&name) => { - fallback_scalar_type((ptr_bits / 8).max(1), &TypeEvidence::default(), ptr_bits) - } - other => other, - } -} - -fn resolve_evidence_driven_type( - initial_ty: r2dec::CType, - var_size_bytes: u32, - ptr_bits: u32, - evidence: &TypeEvidence, -) -> r2dec::CType { - if matches!(initial_ty, r2dec::CType::Float(_)) { - return initial_ty; - } - - let pointer_score = evidence.pointer_score(); - let scalar_score = evidence.scalar_score(); - let initial_is_pointer = matches!(initial_ty, r2dec::CType::Pointer(_)); - let initial_is_scalar = matches!( - initial_ty, - r2dec::CType::Bool | r2dec::CType::Int(_) | r2dec::CType::UInt(_) - ); - let preferred_scalar = fallback_scalar_type(var_size_bytes, evidence, ptr_bits); - let scalar_width_narrows = match (&initial_ty, &preferred_scalar) { - (r2dec::CType::Bool, r2dec::CType::Bool) => false, - (r2dec::CType::Int(initial_bits), r2dec::CType::Int(preferred_bits)) - | (r2dec::CType::Int(initial_bits), r2dec::CType::UInt(preferred_bits)) - | (r2dec::CType::UInt(initial_bits), r2dec::CType::Int(preferred_bits)) - | (r2dec::CType::UInt(initial_bits), r2dec::CType::UInt(preferred_bits)) => { - preferred_bits < initial_bits - } - (r2dec::CType::Int(_), r2dec::CType::Bool) - | (r2dec::CType::UInt(_), r2dec::CType::Bool) => true, - _ => false, + let (Some(merged_obj), Some(symbolic_obj)) = + (merged.as_object_mut(), symbolic_scope_json.as_object()) + else { + return Some(merged); }; - if initial_is_pointer && pointer_score.saturating_add(1) >= scalar_score { - return initial_ty; + if !merged_obj.contains_key("phase") + && let Some(phase) = symbolic_obj.get("phase") + { + merged_obj.insert("phase".to_string(), phase.clone()); } - if initial_is_scalar && scalar_score.saturating_add(1) >= pointer_score { - if scalar_width_narrows - && evidence.has_scalar_signal() - && !evidence.has_pointer_signal() - && !evidence.has_conflict() - { - return preferred_scalar; + for key in ["payloads", "seeds"] { + let Some(serde_json::Value::Array(symbolic_items)) = symbolic_obj.get(key) else { + continue; + }; + let entry = merged_obj + .entry(key.to_string()) + .or_insert_with(|| serde_json::Value::Array(Vec::new())); + if let serde_json::Value::Array(items) = entry { + items.extend(symbolic_items.iter().cloned()); } - return initial_ty; } - match initial_ty { - r2dec::CType::Struct(_) - | r2dec::CType::Union(_) - | r2dec::CType::Enum(_) - | r2dec::CType::Typedef(_) => { - if pointer_score > scalar_score.saturating_add(1) { - return r2dec::CType::void_ptr(); - } - if scalar_score > pointer_score.saturating_add(2) { - return fallback_scalar_type(var_size_bytes, evidence, ptr_bits); - } - return initial_ty; - } - _ => {} - } + Some(merged) +} - if pointer_score > scalar_score.saturating_add(1) { - return r2dec::CType::void_ptr(); - } - if scalar_score > pointer_score - || matches!(initial_ty, r2dec::CType::Void | r2dec::CType::Unknown) - { - return preferred_scalar; - } +fn type_writeback_payload_from_artifact( + artifact: types::FunctionAnalysisArtifact, + interproc: InterprocInferenceInput<'_>, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> InferredTypeWritebackJson { + let semantics = artifact.function_facts.semantics.clone(); + let compiled_semantics = semantics + .as_ref() + .map(analysis::sym::compiled_semantic_info); + let ssa_blocks = artifact.pattern_ssa_func.local_ssa_blocks(); + let scope = merged_interproc_scope_json(interproc.scope_json, symbolic_scope); + let current_summary = artifact + .function_facts + .interproc_summary_set() + .and_then(|summary_set| { + summary_set + .root + .and_then(|root| summary_set.summaries.get(&root).cloned()) + }); + let current_summary_json = current_summary + .as_ref() + .and_then(|summary| serde_json::to_string(summary).ok()); - sanitize_inferred_param_type(initial_ty, var_size_bytes, ptr_bits) + writeback_plan_json( + artifact.writeback_plan, + InterprocSummaryJson { + callsite_count: count_callsites(&ssa_blocks), + iterations: interproc.iter.max(1), + max_iterations: interproc.max_iters.max(interproc.iter.max(1)), + converged: interproc.converged, + summary: current_summary, + summary_json: current_summary_json, + scope, + }, + &artifact.function_facts, + semantics, + compiled_semantics, + ) } -fn collect_type_evidence_for_var( - evidence_ctx: &SignatureTypeEvidenceContext, - var: &r2ssa::SSAVar, - initial_ty: &r2dec::CType, -) -> TypeEvidence { - let key = types::ssa_var_key(var); - let family = types::scalar_register_family_key(&var.name); - let mut evidence = TypeEvidence::default(); - if evidence_ctx.pointer_vars.contains(&key) { - evidence.pointer_proven = 1; - } - if evidence_ctx.scalar_proven_vars.contains(&key) { - evidence.scalar_proven = 1; - } - if evidence_ctx.scalar_likely_vars.contains(&key) { - evidence.scalar_likely = 1; - } - if evidence_ctx.bool_like_vars.contains(&key) { - evidence.bool_like = 1; - } - if let Some(bits) = evidence_ctx.width_bits.get(&key) { - evidence.width_bits = *bits; - } - if evidence.pointer_proven == 0 - && signal_present_for_register_family(&evidence_ctx.pointer_vars, &family, var.version) - { - evidence.pointer_proven = 1; - } - if evidence.scalar_proven == 0 - && signal_present_for_register_family( - &evidence_ctx.scalar_proven_vars, - &family, - var.version, - ) - { - evidence.scalar_proven = 1; - } - if evidence.scalar_likely == 0 - && signal_present_for_register_family( - &evidence_ctx.scalar_likely_vars, - &family, - var.version, - ) - { - evidence.scalar_likely = 1; - } - if evidence.bool_like == 0 - && signal_present_for_register_family(&evidence_ctx.bool_like_vars, &family, var.version) - { - evidence.bool_like = 1; - } - if evidence.width_bits == 0 - && let Some(bits) = - width_hint_for_register_family(&evidence_ctx.width_bits, &family, var.version) - { - evidence.width_bits = bits; +fn semantic_type_fallback_payload( + function_name: &str, + arch_name: &str, + ptr_bits: u32, + interproc: InterprocInferenceInput<'_>, + compiled: &r2sym::SemanticArtifact, + function_facts: &r2types::FunctionFacts, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> InferredTypeWritebackJson { + let compiled_info = analysis::sym::compiled_semantic_info(compiled); + let plan = + r2types::build_semantic_type_fallback_plan(function_name, arch_name, ptr_bits, compiled); + + InferredTypeWritebackJson { + function_name: plan.signature.function_name, + signature: plan.signature.signature, + ret_type: plan.signature.ret_type, + params: plan + .signature + .params + .into_iter() + .map(|param| InferredParamJson { + name: param.name, + param_type: param.param_type, + }) + .collect(), + callconv: plan.signature.callconv, + arch: plan.signature.arch, + confidence: plan.signature.confidence, + callconv_confidence: plan.signature.callconv_confidence, + var_type_candidates: Vec::new(), + var_rename_candidates: Vec::new(), + struct_decls: plan + .struct_decls + .into_iter() + .map(|decl| StructDeclCandidateJson { + name: decl.name, + decl: decl.decl, + confidence: decl.confidence, + source: decl.source.as_str().to_string(), + fields: struct_fields_json(&decl.fields), + }) + .collect(), + global_type_links: plan + .global_type_links + .into_iter() + .map(|candidate| GlobalTypeLinkCandidateJson { + addr: candidate.addr, + target_type: candidate.target_type, + confidence: candidate.confidence, + source: candidate.source.as_str().to_string(), + }) + .collect(), + interproc: InterprocSummaryJson { + callsite_count: 0, + iterations: interproc.iter.max(1), + max_iterations: interproc.max_iters.max(interproc.iter.max(1)), + converged: interproc.converged, + summary: None, + summary_json: None, + scope: merged_interproc_scope_json(interproc.scope_json, symbolic_scope), + }, + plans: function_facts.plans.clone(), + assumptions: function_facts.assumptions.clone(), + assumption_usage: function_facts.assumption_usage.clone(), + semantics: Some(compiled.clone()), + compiled_semantics: Some(compiled_info), + diagnostics: TypeWritebackDiagnosticsJson { + warnings: plan.diagnostics.warnings, + ..TypeWritebackDiagnosticsJson::default() + }, } - merge_initial_type_evidence(initial_ty, &mut evidence); - evidence } -fn signal_present_for_register_family( - keys: &std::collections::HashSet, - family: &str, - version: u32, -) -> bool { - keys.iter() - .any(|key| key_matches_register_family_version(key, family, version)) +fn semantic_fallback_function_facts( + compiled: &r2sym::SemanticArtifact, + assumptions: &r2ssa::AssumptionSet, +) -> r2types::FunctionFacts { + r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + Some(compiled.clone()), + ) + .with_assumptions(assumptions.clone()) } -fn width_hint_for_register_family( - hints: &std::collections::HashMap, - family: &str, - version: u32, -) -> Option { - hints - .iter() - .filter(|(key, _)| key_matches_register_family_version(key, family, version)) - .map(|(_, bits)| *bits) - .filter(|bits| *bits > 0) - .min() +#[cfg(test)] +const SIG_WRITEBACK_CONFIDENCE_MIN: u8 = 70; +#[cfg(test)] +const CC_WRITEBACK_CONFIDENCE_MIN: u8 = 80; + +#[cfg(test)] +fn merge_initial_type_evidence(initial_ty: &r2dec::CType, evidence: &mut TypeEvidence) { + r2types::merge_initial_signature_type_evidence(&ctype_to_type_like(initial_ty), evidence); } -fn key_matches_register_family_version(key: &str, family: &str, version: u32) -> bool { - let Some((name, version_str)) = key.rsplit_once('_') else { - return false; - }; - version_str.parse::().ok() == Some(version) - && types::scalar_register_family_key(name) == family +#[cfg(test)] +fn materialize_signature_ctype(ty: r2dec::CType, ptr_bits: u32) -> r2dec::CType { + type_like_to_ctype(&r2types::materialize_signature_type_like( + ctype_to_type_like(&ty), + ptr_bits, + )) +} + +#[cfg(test)] +fn resolve_evidence_driven_type( + initial_ty: r2dec::CType, + var_size_bytes: u32, + ptr_bits: u32, + evidence: &TypeEvidence, +) -> r2dec::CType { + type_like_to_ctype(&r2types::resolve_evidence_driven_signature_type( + ctype_to_type_like(&initial_ty), + var_size_bytes, + ptr_bits, + evidence, + )) } +#[cfg(test)] +fn collect_type_evidence_for_var( + evidence_ctx: &r2types::SignatureTypeEvidenceContext, + var: &r2ssa::SSAVar, + initial_ty: &r2dec::CType, +) -> TypeEvidence { + r2types::collect_signature_type_evidence_for_var( + evidence_ctx, + var, + &ctype_to_type_like(initial_ty), + ) +} + +#[cfg(test)] fn type_like_to_ctype(ty: &r2types::CTypeLike) -> r2dec::CType { match ty { r2types::CTypeLike::Void => r2dec::CType::Void, @@ -3601,7 +3844,6 @@ fn type_like_to_ctype(ty: &r2types::CTypeLike) -> r2dec::CType { } } -#[cfg(test)] fn ctype_to_type_like(ty: &r2dec::CType) -> r2types::CTypeLike { match ty { r2dec::CType::Void => r2types::CTypeLike::Void, @@ -3630,248 +3872,95 @@ fn ctype_to_type_like(ty: &r2dec::CType) -> r2types::CTypeLike { } } -fn infer_signature_return_type( - func: &r2ssa::SSAFunction, - type_inference: &r2types::TypeInference, +#[cfg(test)] +fn fallback_scalar_type( + var_size_bytes: u32, + evidence: &TypeEvidence, ptr_bits: u32, - evidence_ctx: &SignatureTypeEvidenceContext, -) -> (r2dec::CType, TypeEvidence) { - let mut candidates = Vec::new(); - let mut candidate_evidence = Vec::new(); - - for block in func.blocks() { - for op in &block.ops { - let r2ssa::SSAOp::Return { target } = op else { - continue; - }; - - let target_name = target.name.to_ascii_lowercase(); - if target_name.starts_with("xmm0") || target_name.starts_with("st0") { - let bits = if target.size.saturating_mul(8) <= 32 { - 32 - } else { - 64 - }; - let ty = r2dec::CType::Float(bits); - let mut evidence = TypeEvidence::default(); - merge_initial_type_evidence(&ty, &mut evidence); - evidence.width_bits = bits; - candidates.push(ty); - candidate_evidence.push(evidence); - continue; - } - - let initial_ty = type_like_to_ctype(&type_inference.get_type(target)); - let evidence = collect_type_evidence_for_var(evidence_ctx, target, &initial_ty); - let ty = resolve_evidence_driven_type(initial_ty, target.size, ptr_bits, &evidence); - candidates.push(ty); - candidate_evidence.push(evidence); - } - } - - if candidates.is_empty() { - return (r2dec::CType::Void, TypeEvidence::default()); - } - - let mut meaningful: Vec = candidates - .iter() - .filter(|ty| !matches!(ty, r2dec::CType::Unknown)) - .cloned() - .collect(); - if meaningful.is_empty() { - let fallback_evidence = candidate_evidence.into_iter().next().unwrap_or_default(); - return ( - fallback_scalar_type((ptr_bits / 8).max(1), &fallback_evidence, ptr_bits), - fallback_evidence, - ); - } - if meaningful.iter().all(|ty| ty == &meaningful[0]) { - return ( - meaningful.remove(0), - candidate_evidence.into_iter().next().unwrap_or_default(), - ); - } - if let Some(float_ty) = meaningful - .iter() - .find(|ty| matches!(ty, r2dec::CType::Float(_))) - .cloned() - { - let evidence = candidate_evidence - .into_iter() - .find(|e| e.width_bits >= 32) - .unwrap_or_default(); - return (float_ty, evidence); - } - let evidence = candidate_evidence.into_iter().next().unwrap_or_default(); - (meaningful.remove(0), evidence) -} - -fn canonical_x86_64_arg_reg(name: &str) -> Option<&'static str> { - match name.to_ascii_lowercase().as_str() { - "rdi" | "edi" | "di" | "dil" => Some("rdi"), - "rsi" | "esi" | "si" | "sil" => Some("rsi"), - "rdx" | "edx" | "dx" | "dl" | "dh" => Some("rdx"), - "rcx" | "ecx" | "cx" | "cl" | "ch" => Some("rcx"), - "r8" | "r8d" | "r8w" | "r8b" => Some("r8"), - "r9" | "r9d" | "r9w" | "r9b" => Some("r9"), - _ => None, - } +) -> r2dec::CType { + type_like_to_ctype(&r2types::resolve_evidence_driven_signature_type( + r2types::CTypeLike::Unknown, + var_size_bytes, + ptr_bits, + evidence, + )) } -fn collect_version0_input_regs( - func: &r2ssa::SSAFunction, -) -> std::collections::HashMap { - let mut counts = std::collections::HashMap::new(); - for block in func.blocks() { - for op in &block.ops { - for src in op.sources() { - if src.version != 0 { - continue; - } - if src.name.starts_with("tmp:") || src.name.starts_with("const:") { - continue; - } - let key = src.name.to_ascii_lowercase(); - *counts.entry(key).or_insert(0) += 1; - } - } - } - counts +#[cfg(test)] +fn sanitize_inferred_param_type( + ty: r2dec::CType, + var_size_bytes: u32, + ptr_bits: u32, +) -> r2dec::CType { + type_like_to_ctype(&r2types::resolve_evidence_driven_signature_type( + ctype_to_type_like(&ty), + var_size_bytes, + ptr_bits, + &TypeEvidence::default(), + )) } +#[cfg(test)] fn infer_callconv_x86_64_from_counts( counts: &std::collections::HashMap, -) -> (&'static str, u8) { - let mut canonical = std::collections::BTreeMap::new(); - for (reg, count) in counts { - if let Some(name) = canonical_x86_64_arg_reg(reg) { - *canonical.entry(name).or_insert(0u32) += *count; - } - } - - let rdi = *canonical.get("rdi").unwrap_or(&0); - let rsi = *canonical.get("rsi").unwrap_or(&0); - let rcx = *canonical.get("rcx").unwrap_or(&0); - let rdx = *canonical.get("rdx").unwrap_or(&0); - let r8 = *canonical.get("r8").unwrap_or(&0); - let r9 = *canonical.get("r9").unwrap_or(&0); - - let sysv_primary = rdi + rsi; - let sysv_total = rdi + rsi + rdx + rcx + r8 + r9; - let ms_total = rcx + rdx + r8 + r9; - let ms_regs_used = [rcx, rdx, r8, r9].iter().filter(|&&v| v > 0).count(); - let ms_dominant = sysv_primary == 0 - && rcx > 0 - && ms_regs_used >= 2 - && ms_total >= 3 - && ms_total >= (rdi + rsi + rdx + 1); - - if ms_dominant { - let confidence = if ms_total >= 3 { 90 } else { 76 }; - ("ms", confidence) - } else { - let confidence = if sysv_primary > 0 { - 92 - } else if sysv_total > 0 { - 76 - } else { - 60 - }; - ("amd64", confidence) - } +) -> (String, u8) { + r2types::compute_callconv_inference("x86-64", counts) } -fn sanitize_inferred_param_type( - mut ty: r2dec::CType, - var_size_bytes: u32, +#[cfg(test)] +fn infer_signature_return_type( + func: &r2ssa::SSAFunction, + type_inference: &r2types::TypeInference, ptr_bits: u32, -) -> r2dec::CType { - if matches!(ty, r2dec::CType::Void | r2dec::CType::Unknown) { - ty = match var_size_bytes { - 1 => r2dec::CType::Int(8), - 2 => r2dec::CType::Int(16), - 4 => r2dec::CType::Int(32), - 8 => r2dec::CType::Int(64), - _ => r2dec::CType::Unknown, - }; - } - - if matches!(ty, r2dec::CType::Void | r2dec::CType::Unknown) { - ty = if ptr_bits >= 64 { - r2dec::CType::Int(64) - } else { - r2dec::CType::Int(32) - }; - } - - ty + evidence_ctx: &r2types::SignatureTypeEvidenceContext, +) -> (r2dec::CType, TypeEvidence) { + let (ty, evidence) = + r2types::infer_signature_return_type(func, type_inference, ptr_bits, evidence_ctx); + (type_like_to_ctype(&ty), evidence) } -fn is_informative_type(ty: &r2dec::CType) -> bool { - !matches!(ty, r2dec::CType::Void | r2dec::CType::Unknown) +#[cfg(test)] +#[allow(dead_code)] +fn collect_version0_input_regs( + func: &r2ssa::SSAFunction, +) -> std::collections::HashMap { + r2types::collect_version0_input_regs(func) } +#[cfg(test)] fn compute_signature_confidence( params: &[InferredParam], ret_type: &r2dec::CType, ret_evidence: &TypeEvidence, ) -> u8 { - let mut confidence: i32 = 48; - if !params.is_empty() { - confidence += 8; - } - - for param in params { - let evidence = ¶m.evidence; - if evidence.pointer_proven > 0 || evidence.scalar_proven > 0 { - confidence += 6; - } else if evidence.bool_like > 0 - || evidence.pointer_likely > 0 - || evidence.scalar_likely > 0 - { - confidence += 3; - } else if is_informative_type(¶m.ty) { - confidence += 2; - } else { - confidence -= 2; - } - - if evidence.has_conflict() { - confidence -= 4; - } - } - - if is_informative_type(ret_type) { - confidence += 4; - if ret_evidence.pointer_proven > 0 - || ret_evidence.scalar_proven > 0 - || ret_evidence.bool_like > 0 - { - confidence += 2; - } - } else if ret_evidence.has_pointer_signal() || ret_evidence.has_scalar_signal() { - confidence += 2; - } - - if ret_evidence.has_conflict() { - confidence -= 3; - } - - confidence.clamp(0, 100) as u8 + let canonical_params = params + .iter() + .map(|param| r2types::SignatureParamCandidate { + name: param.name.clone(), + ty: ctype_to_type_like(¶m.ty), + arg_index: param.arg_index, + size_bytes: param.size_bytes, + evidence: param.evidence.clone(), + }) + .collect::>(); + r2types::compute_signature_confidence( + &canonical_params, + &ctype_to_type_like(ret_type), + ret_evidence, + ) } +#[cfg(test)] fn compute_callconv_inference( arch_name: &str, input_counts: &std::collections::HashMap, ) -> (String, u8) { - match arch_name { - "x86-64" => { - let (callconv, confidence) = infer_callconv_x86_64_from_counts(input_counts); - (callconv.to_string(), confidence) - } - "x86" => ("cdecl".to_string(), 64), - _ => (String::new(), 0), - } + r2types::compute_callconv_inference(arch_name, input_counts) +} + +#[cfg(test)] +fn is_informative_type(ty: &r2dec::CType) -> bool { + !matches!(ty, r2dec::CType::Void | r2dec::CType::Unknown) } #[cfg(test)] @@ -3901,6 +3990,7 @@ fn explicit_signature_context_strength(sig: &r2types::FunctionSignatureSpec) -> confidence } +#[cfg(test)] fn normalize_inferred_param_name( raw_name: &str, fallback_idx: usize, @@ -3912,6 +4002,7 @@ fn normalize_inferred_param_name( uniquify_name(clean, used) } +#[cfg(test)] fn format_afs_signature( function_name: &str, ret_type: &str, @@ -3933,6 +4024,7 @@ fn cstr_or_default(ptr: *const c_char, default: &str) -> String { helpers::cstr_or_default(ptr, default) } +#[cfg(test)] fn is_opaque_placeholder_type_name(ty: &str) -> bool { let lower = ty.trim().to_ascii_lowercase(); if lower.is_empty() { @@ -3950,6 +4042,7 @@ fn is_opaque_placeholder_type_name(ty: &str) -> bool { || lower.contains(" type_0x") } +#[cfg(test)] fn is_unmaterialized_aggregate_name(name: &str) -> bool { let lower = name.trim().to_ascii_lowercase(); lower.is_empty() || lower == "anon" || lower.starts_with("anon_") @@ -3982,6 +4075,7 @@ fn is_generic_type_string(ty: &str) -> bool { ) } +#[cfg(test)] fn normalize_external_type_name(ty: &str) -> String { let normalized = r2types::normalize_external_type_name(ty); if normalized.is_empty() || is_opaque_placeholder_type_name(&normalized) { @@ -3991,6 +4085,7 @@ fn normalize_external_type_name(ty: &str) -> String { } } +#[cfg(test)] fn estimate_parsed_c_type_size_bytes(ty: &r2dec::CType, ptr_bits: u32) -> Option { match ty { r2dec::CType::Void => Some(0), @@ -4014,6 +4109,7 @@ fn estimate_parsed_c_type_size_bytes(ty: &r2dec::CType, ptr_bits: u32) -> Option } } +#[cfg(test)] fn estimate_c_type_size_bytes(ty: &str, ptr_bits: u32) -> u64 { if let Some(parsed) = parse_external_type(ty, ptr_bits) && let Some(size) = estimate_parsed_c_type_size_bytes(&parsed, ptr_bits) @@ -4032,6 +4128,7 @@ fn estimate_c_type_size_bytes(ty: &str, ptr_bits: u32) -> u64 { 1 } +#[cfg(test)] fn build_struct_decl( name: &str, fields: &[StructFieldCandidateJson], @@ -4092,6 +4189,7 @@ fn parse_existing_var_types(json_str: &str) -> std::collections::HashMap, ptr_bits: u32, @@ -4123,7 +4221,7 @@ fn collect_pointer_arg_slot_map( if is_riscv64 { return alias.starts_with('x') || alias.starts_with('a'); } - alias == canonical.to_ascii_lowercase() + alias == (*canonical).to_ascii_lowercase() }; if include_alias(canonical) { @@ -4139,6 +4237,7 @@ fn collect_pointer_arg_slot_map( } #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg(test)] struct ArgAddrExpr { slot: usize, offset: i64, @@ -4154,6 +4253,7 @@ struct GlobalAddrExpr { } #[derive(Clone, Debug, Default)] +#[cfg(test)] struct StructFieldEvidence { reads: u32, writes: u32, @@ -4161,16 +4261,21 @@ struct StructFieldEvidence { type_votes: std::collections::BTreeMap, } +#[cfg(test)] type SlotTypeOverrides = std::collections::HashMap; +#[cfg(test)] type SlotFieldProfiles = std::collections::HashMap>; +#[cfg(test)] type SlotFieldEvidenceMap = std::collections::HashMap>; +#[cfg(test)] type StructInferenceArtifacts = ( Vec, SlotTypeOverrides, SlotFieldProfiles, ); +#[cfg(test)] fn build_struct_inference_artifacts_from_field_evidence( slot_field_evidence: SlotFieldEvidenceMap, ptr_bits: u32, @@ -4257,6 +4362,7 @@ fn build_struct_inference_artifacts_from_field_evidence( (struct_decls, slot_type_overrides, slot_fields_for_links) } +#[cfg(test)] fn infer_structs_from_semantic_accesses( ssa_func: &r2ssa::SSAFunction, cfg: &r2dec::DecompilerConfig, @@ -4284,6 +4390,8 @@ fn infer_structs_from_semantic_accesses( build_struct_inference_artifacts_from_field_evidence(slot_field_evidence, ptr_bits, diagnostics) } +#[cfg(test)] +#[allow(dead_code)] fn merge_struct_inference_artifacts( mut base: StructInferenceArtifacts, supplement: StructInferenceArtifacts, @@ -4310,6 +4418,7 @@ fn merge_struct_inference_artifacts( base } +#[cfg(test)] fn parse_ssa_const_offset(name: &str, ptr_bits: u32) -> Option { let val_str = name .strip_prefix("const:") @@ -4333,6 +4442,7 @@ fn parse_ssa_const_offset(name: &str, ptr_bits: u32) -> Option { Some(signed_offset_from_const(raw, ptr_bits)) } +#[cfg(test)] fn signed_offset_from_const(raw: u64, ptr_bits: u32) -> i64 { let bits = ptr_bits.clamp(8, 64); if bits == 64 { @@ -4348,6 +4458,7 @@ fn signed_offset_from_const(raw: u64, ptr_bits: u32) -> i64 { } } +#[cfg(test)] fn infer_structs_from_ssa( ssa_blocks: &[r2ssa::SSABlock], arch: Option<&ArchSpec>, @@ -4357,9 +4468,7 @@ fn infer_structs_from_ssa( use std::collections::HashMap; let pointer_arg_slot_map = collect_pointer_arg_slot_map(arch, ptr_bits); - let arch_name = - crate::decompiler::normalize_sig_arch_name(arch).unwrap_or_else(|| "unknown".to_string()); - let cfg = crate::decompiler::decompiler_config_for_arch_name(&arch_name, ptr_bits); + let (_, _, cfg) = r2dec::DecompilerConfig::for_arch(arch); let sp_name = cfg.sp_name.to_ascii_lowercase(); let fp_name = cfg.fp_name.to_ascii_lowercase(); let mut addr_exprs: HashMap = HashMap::new(); @@ -5476,11 +5585,11 @@ pub extern "C" fn r2sleigh_infer_signature_cc_json( let Some(analysis) = types::build_function_analysis(&input) else { return ptr::null_mut(); }; - let Some(signature_cc) = types::infer_signature_cc_from_analysis(&input, &analysis) else { + let Some(signature) = types::infer_signature_cc_from_analysis(&input, &analysis) else { return ptr::null_mut(); }; - match serde_json::to_string(&signature_cc) { + match serde_json::to_string(&types::signature_to_json(&signature)) { Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), Err(_) => ptr::null_mut(), } @@ -5496,6 +5605,144 @@ fn direct_call_targets_from_analysis(analysis: &types::FunctionAnalysis) -> Vec< targets.into_iter().collect() } +fn runtime_registration_targets_from_analysis( + analysis: &types::FunctionAnalysis, + arch: Option<&ArchSpec>, + registration_call_targets: &[u64], +) -> Vec { + fn debug_runtime_targets_enabled() -> bool { + std::env::var_os("R2SLEIGH_DEBUG_RUNTIME_TARGETS").is_some() + } + + fn debug_runtime_targets_log(message: &str) { + if !debug_runtime_targets_enabled() { + return; + } + let path = std::env::var("R2SLEIGH_DEBUG_RUNTIME_TARGETS_LOG") + .unwrap_or_else(|_| "/tmp/r2sleigh_runtime_targets.log".to_string()); + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { + let _ = writeln!(file, "{message}"); + } + } + + let lower_arch = arch.map(|arch| arch.name.to_ascii_lowercase()); + let supports_windows_x64 = arch.is_some_and(|arch| { + lower_arch.as_deref().is_some_and(|name| { + (name.contains("x86") || matches!(name, "x64" | "amd64")) + && (arch.addr_size == 8 || name.contains("64")) + }) + }); + if !supports_windows_x64 || registration_call_targets.is_empty() { + debug_runtime_targets_log(&format!( + "skip supports_windows_x64={} arch={:?} registrations={}", + supports_windows_x64, + lower_arch.as_deref(), + registration_call_targets.len() + )); + return Vec::new(); + } + + let registrations = registration_call_targets + .iter() + .copied() + .collect::>(); + let observations = + r2ssa::observe_call_arguments(&analysis.ssa_func, &r2ssa::AbiProfile::windows_x64()); + let mut targets = std::collections::BTreeSet::new(); + for (call_id, call) in &analysis.ssa_func.call_sites().by_id { + let Some(target) = analysis.ssa_func.resolved_call_target(call) else { + debug_runtime_targets_log(&format!("call_id={call_id:?} unresolved_target")); + continue; + }; + if !registrations.contains(&target) { + debug_runtime_targets_log(&format!( + "call_id={call_id:?} target=0x{target:x} not_registration" + )); + continue; + } + let Some(args) = observations.get(call_id) else { + debug_runtime_targets_log(&format!( + "call_id={call_id:?} target=0x{target:x} missing_args" + )); + continue; + }; + let Some(r2ssa::CallArgObservation::Const(handler)) = args.get(1) else { + debug_runtime_targets_log(&format!( + "call_id={call_id:?} target=0x{target:x} handler_arg={:?}", + args.get(1) + )); + continue; + }; + if *handler >= 0x1000 { + debug_runtime_targets_log(&format!( + "call_id={call_id:?} target=0x{target:x} handler=0x{handler:x}" + )); + targets.insert(*handler); + } + } + targets.into_iter().collect() +} + +#[derive(Serialize)] +struct RuntimeMaterializedSourceJson { + addr: u64, + size: u64, +} + +fn runtime_materialized_sources_from_analysis( + analysis: &types::FunctionAnalysis, + arch: Option<&ArchSpec>, + copy_call_targets: &[u64], +) -> Vec { + let lower_arch = arch.map(|arch| arch.name.to_ascii_lowercase()); + let supports_windows_x64 = arch.is_some_and(|arch| { + lower_arch.as_deref().is_some_and(|name| { + (name.contains("x86") || matches!(name, "x64" | "amd64")) + && (arch.addr_size == 8 || name.contains("64")) + }) + }); + if !supports_windows_x64 || copy_call_targets.is_empty() { + return Vec::new(); + } + + let copy_targets = copy_call_targets + .iter() + .copied() + .collect::>(); + let observations = + r2ssa::observe_call_arguments(&analysis.ssa_func, &r2ssa::AbiProfile::windows_x64()); + let mut sources = std::collections::BTreeMap::::new(); + for (call_id, call) in &analysis.ssa_func.call_sites().by_id { + let Some(target) = analysis.ssa_func.resolved_call_target(call) else { + continue; + }; + if !copy_targets.contains(&target) { + continue; + } + let Some(args) = observations.get(call_id) else { + continue; + }; + let ( + Some(r2ssa::CallArgObservation::Const(source)), + Some(r2ssa::CallArgObservation::Const(size)), + ) = (args.get(1), args.get(2)) + else { + continue; + }; + if *source >= 0x1000 && *size > 0 { + sources + .entry(*source) + .and_modify(|existing| *existing = (*existing).max(*size)) + .or_insert(*size); + } + } + + sources + .into_iter() + .map(|(addr, size)| RuntimeMaterializedSourceJson { addr, size }) + .collect() +} + #[unsafe(no_mangle)] pub extern "C" fn r2sleigh_get_direct_call_targets_json( ctx: *const R2ILContext, @@ -5518,28 +5765,354 @@ pub extern "C" fn r2sleigh_get_direct_call_targets_json( } } -/// Infer full type write-back payload (signature + per-variable + structs + globals). -/// -/// Returns JSON suitable for plugin-side confidence/conflict policy. -/// Caller must free with r2il_string_free(). -struct InterprocInferenceInput<'a> { - iter: usize, - max_iters: usize, - converged: bool, - scope_json: &'a str, -} - -struct TypeWritebackInferenceInput<'a> { - ctx: *const R2ILContext, - blocks: *const *const R2ILBlock, - num_blocks: usize, - fcn_addr: u64, - fcn_name: *const c_char, - external_context_json: *const c_char, - interproc: InterprocInferenceInput<'a>, -} - -fn infer_type_writeback_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mut c_char { +#[unsafe(no_mangle)] +pub extern "C" fn r2sleigh_get_symbolic_scope_targets_json( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + registration_call_targets_json: *const c_char, +) -> *mut c_char { + let Some(input) = types::build_function_input(ctx, blocks, num_blocks, fcn_addr, fcn_name) + else { + return ptr::null_mut(); + }; + let Some(analysis) = types::build_function_analysis(&input) else { + return ptr::null_mut(); + }; + let registration_call_targets: Vec = serde_json::from_str(&helpers::cstr_or_default( + registration_call_targets_json, + "[]", + )) + .unwrap_or_default(); + let payload = runtime_registration_targets_from_analysis( + &analysis, + input.ctx.arch, + ®istration_call_targets, + ); + match serde_json::to_string(&payload) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => ptr::null_mut(), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sleigh_get_runtime_materialized_sources_json( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + copy_call_targets_json: *const c_char, +) -> *mut c_char { + let Some(input) = types::build_function_input(ctx, blocks, num_blocks, fcn_addr, fcn_name) + else { + return ptr::null_mut(); + }; + let Some(analysis) = types::build_function_analysis(&input) else { + return ptr::null_mut(); + }; + let copy_call_targets: Vec = + serde_json::from_str(&helpers::cstr_or_default(copy_call_targets_json, "[]")) + .unwrap_or_default(); + let payload = + runtime_materialized_sources_from_analysis(&analysis, input.ctx.arch, ©_call_targets); + match serde_json::to_string(&payload) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => ptr::null_mut(), + } +} + +/// Infer full type write-back payload (signature + per-variable + structs + globals). +/// +/// Returns JSON suitable for plugin-side confidence/conflict policy. +/// Caller must free with r2il_string_free(). +#[derive(Clone, Copy)] +struct InterprocInferenceInput<'a> { + iter: usize, + max_iters: usize, + converged: bool, + scope_json: &'a str, +} + +struct TypeWritebackInferenceInput<'a> { + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + external_context_json: *const c_char, + scope_functions: *const analysis::sym::R2ILFunctionBlocks, + scope_num_functions: usize, + interproc: InterprocInferenceInput<'a>, +} + +struct SemanticWorkerLinearizationInput { + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + block_count: usize, + loop_count: usize, + back_edge_count: usize, + max_switch_cases: usize, + scope_functions: *const analysis::sym::R2ILFunctionBlocks, + scope_num_functions: usize, +} + +struct FunctionAnalysisSharedBundle { + function_name: String, + function_addr: u64, + cfg_risk: CfgRiskSummaryJson, + semantic_artifact: Option, + function_facts: r2types::FunctionFacts, + semantic_route: Option, + type_writeback: InferredTypeWritebackJson, + prefer_bounded_type_plan: bool, +} + +fn build_inference_symbolic_scope( + input: &TypeWritebackInferenceInput<'_>, + function_input: &types::FunctionInput<'_>, +) -> Option { + if input.scope_functions.is_null() || input.scope_num_functions == 0 { + None + } else { + unsafe { + analysis::sym::build_symbolic_scope_from_ffi( + input.scope_functions, + input.scope_num_functions, + function_input.ctx.arch, + function_input.function_addr, + ) + } + } +} + +fn build_function_analysis_shared_bundle( + input: TypeWritebackInferenceInput<'_>, +) -> Option { + let function_input = types::build_function_input( + input.ctx, + input.blocks, + input.num_blocks, + input.fcn_addr, + input.fcn_name, + )?; + let external_context = cstr_or_default(input.external_context_json, "{}"); + let symbolic_scope = build_inference_symbolic_scope(&input, &function_input); + let (arch_name, ptr_bits, _) = r2dec::DecompilerConfig::for_arch(function_input.ctx.arch); + let parsed_context = r2types::parse_external_context_json(&external_context, ptr_bits); + let (cfg_risk, semantic_artifact, function_facts, type_writeback, prefer_bounded_type_plan) = + if let Some(cached_artifact) = + types::get_cached_function_analysis_artifact_with_scope_and_interproc( + &function_input, + &external_context, + input.interproc.scope_json, + input.interproc.max_iters, + symbolic_scope.as_ref(), + ) + { + let cfg_risk = + cfg_risk_summary_json(cached_artifact.ssa_func.function().cfg_risk_summary()); + let function_facts = cached_artifact.function_facts.clone(); + let semantic_artifact = function_facts.semantics.clone(); + let prefer_bounded_type_plan = semantic_artifact + .as_ref() + .is_some_and(r2types::semantic_artifact_prefers_bounded_type_plan); + if prefer_bounded_type_plan { + let compiled = semantic_artifact + .as_ref() + .expect("bounded semantic type fallback requires semantic artifact"); + ( + cfg_risk, + semantic_artifact.clone(), + function_facts.clone(), + semantic_type_fallback_payload( + &function_input.function_name, + &arch_name, + ptr_bits, + input.interproc, + compiled, + &function_facts, + symbolic_scope.as_ref(), + ), + true, + ) + } else { + ( + cfg_risk, + semantic_artifact, + function_facts, + type_writeback_payload_from_artifact( + cached_artifact, + input.interproc, + symbolic_scope.as_ref(), + ), + false, + ) + } + } else { + let analysis = types::build_function_analysis(&function_input)?; + let cfg_risk = cfg_risk_summary_json(analysis.ssa_func.function().cfg_risk_summary()); + let semantic_artifact = r2sym::compile_semantic_artifact_default_with_scope( + &z3::Context::thread_local(), + &analysis.ssa_func, + symbolic_scope.as_ref(), + function_input.ctx.arch, + ); + if r2types::semantic_artifact_prefers_bounded_type_plan(&semantic_artifact) { + let function_facts = semantic_fallback_function_facts( + &semantic_artifact, + &parsed_context.assumptions, + ); + ( + cfg_risk, + Some(semantic_artifact.clone()), + function_facts.clone(), + semantic_type_fallback_payload( + &function_input.function_name, + &arch_name, + ptr_bits, + input.interproc, + &semantic_artifact, + &function_facts, + symbolic_scope.as_ref(), + ), + true, + ) + } else { + let interproc_summary_set = types::build_interproc_summary_set_with_scope( + &function_input, + &analysis, + input.interproc.scope_json, + input.interproc.max_iters, + symbolic_scope.as_ref(), + ); + let artifact = + types::build_function_analysis_artifact_from_analysis_with_semantic_artifact( + &function_input, + analysis, + &external_context, + Some(interproc_summary_set), + semantic_artifact, + )?; + let function_facts = artifact.function_facts.clone(); + let semantic_artifact = function_facts.semantics.clone(); + ( + cfg_risk, + semantic_artifact, + function_facts, + type_writeback_payload_from_artifact( + artifact, + input.interproc, + symbolic_scope.as_ref(), + ), + false, + ) + } + }; + let semantic_route = r2dec::detached_semantic_route_plan( + &function_input.function_name, + function_input.blocks.as_slice(), + &function_facts, + ) + .map(semantic_route_plan_json); + + Some(FunctionAnalysisSharedBundle { + function_name: function_input.function_name.clone(), + function_addr: function_input.function_addr, + cfg_risk, + semantic_artifact, + function_facts, + semantic_route, + type_writeback, + prefer_bounded_type_plan, + }) +} + +fn infer_type_writeback_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mut c_char { + let Some(bundle) = build_function_analysis_shared_bundle(input) else { + return ptr::null_mut(); + }; + + match serde_json::to_string(&bundle.type_writeback) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => ptr::null_mut(), + } +} + +fn function_facts_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mut c_char { + let Some(bundle) = build_function_analysis_shared_bundle(input) else { + return ptr::null_mut(); + }; + + let payload = FunctionFactsReportJson { + function_name: bundle.function_name.clone(), + function_addr: bundle.function_addr, + cfg_risk: bundle.cfg_risk, + semantic_build_plan: bundle + .semantic_artifact + .as_ref() + .map(r2sym::SemanticArtifact::build_plan), + plans: bundle.function_facts.plans.clone(), + semantic_route: bundle.semantic_route, + assumptions: bundle.function_facts.assumptions.clone(), + assumption_usage: bundle.function_facts.assumption_usage.clone(), + summary_diagnostics: bundle.function_facts.summary_view.diagnostics().cloned(), + type_writeback: bundle.type_writeback, + }; + + match serde_json::to_string(&payload) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => ptr::null_mut(), + } +} + +fn function_plan_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mut c_char { + let Some(bundle) = build_function_analysis_shared_bundle(input) else { + return ptr::null_mut(); + }; + + let payload = FunctionPlanReportJson { + function_name: bundle.function_name, + function_addr: bundle.function_addr, + cfg_risk: bundle.cfg_risk, + semantic: bundle + .semantic_artifact + .as_ref() + .map(analysis::sym::compiled_semantic_info), + semantic_build_plan: bundle + .semantic_artifact + .as_ref() + .map(r2sym::SemanticArtifact::build_plan), + plans: bundle.function_facts.plans.clone(), + semantic_route: bundle.semantic_route, + assumptions: bundle.function_facts.assumptions.clone(), + assumption_usage: bundle.function_facts.assumption_usage.clone(), + summary_diagnostics: bundle.function_facts.summary_view.diagnostics().cloned(), + prefer_bounded_type_plan: bundle.prefer_bounded_type_plan, + }; + + match serde_json::to_string(&payload) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => ptr::null_mut(), + } +} + +fn semantic_worker_linearization_impl(input: SemanticWorkerLinearizationInput) -> *mut c_char { + let summary = r2ssa::CFGRiskSummary { + block_count: input.block_count, + loop_count: input.loop_count, + back_edge_count: input.back_edge_count, + switch_block_count: usize::from(input.max_switch_cases > 0), + max_switch_cases: input.max_switch_cases, + }; + let Some(_reason) = r2dec::cfg_guard_reason_from_summary(&summary) else { + return ptr::null_mut(); + }; let Some(function_input) = types::build_function_input( input.ctx, input.blocks, @@ -5549,52 +6122,67 @@ fn infer_type_writeback_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mu ) else { return ptr::null_mut(); }; - let external_context = cstr_or_default(input.external_context_json, "{}"); - let Some(artifact) = types::build_function_analysis_artifact( - &function_input, - &external_context, - input.interproc.scope_json, - input.interproc.max_iters, - ) else { + let symbolic_scope = if input.scope_functions.is_null() || input.scope_num_functions == 0 { + None + } else { + unsafe { + analysis::sym::build_symbolic_scope_from_ffi( + input.scope_functions, + input.scope_num_functions, + function_input.ctx.arch, + function_input.function_addr, + ) + } + }; + let (arch_name, ptr_bits, _) = r2dec::DecompilerConfig::for_arch(function_input.ctx.arch); + let semantic_artifact = if let Some(cached_artifact) = + types::get_cached_function_analysis_artifact_with_scope( + &function_input, + "{}", + symbolic_scope.as_ref(), + ) { + cached_artifact.function_facts.semantics + } else { + let Some(analysis) = types::build_function_analysis(&function_input) else { + return ptr::null_mut(); + }; + Some(r2sym::compile_semantic_artifact_default_with_scope( + &z3::Context::thread_local(), + &analysis.ssa_func, + symbolic_scope.as_ref(), + function_input.ctx.arch, + )) + }; + let Some(semantic_artifact) = semantic_artifact else { return ptr::null_mut(); }; - let ssa_blocks = artifact.pattern_ssa_func.local_ssa_blocks(); - if ssa_blocks.is_empty() { + let function_facts = r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + Some(semantic_artifact.clone()), + ); + let Some(route) = r2dec::detached_semantic_route_plan( + &function_input.function_name, + function_input.blocks.as_slice(), + &function_facts, + ) else { return ptr::null_mut(); - } - let scope = serde_json::from_str::(input.interproc.scope_json) - .ok() - .filter(|v| !v.is_null() && v.as_object().map(|obj| !obj.is_empty()).unwrap_or(true)); - - let current_summary = artifact - .interproc_summary_set - .as_ref() - .and_then(|summary_set| { - summary_set - .root - .and_then(|root| summary_set.summaries.get(&root).cloned()) - }); - let current_summary_json = current_summary - .as_ref() - .and_then(|summary| serde_json::to_string(summary).ok()); - - let payload = writeback_plan_json( - artifact.writeback_plan, - InterprocSummaryJson { - callsite_count: count_callsites(&ssa_blocks), - iterations: input.interproc.iter.max(1), - max_iterations: input.interproc.max_iters.max(input.interproc.iter.max(1)), - converged: input.interproc.converged, - summary: current_summary, - summary_json: current_summary_json, - scope, - }, + }; + let reason = match route { + r2dec::SemanticRoutePlan::LinearWorker { reason } => reason, + _ => return ptr::null_mut(), + }; + let plan = r2types::build_semantic_type_fallback_plan( + &function_input.function_name, + &arch_name, + ptr_bits, + &semantic_artifact, ); - - match serde_json::to_string(&payload) { - Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), - Err(_) => ptr::null_mut(), - } + CString::new(r2dec::render_semantic_worker_linearization( + &plan, + Some(&semantic_artifact), + &reason, + )) + .map_or(ptr::null_mut(), |c| c.into_raw()) } #[unsafe(no_mangle)] @@ -5613,6 +6201,8 @@ pub extern "C" fn r2sleigh_infer_type_writeback_json( fcn_addr, fcn_name, external_context_json, + scope_functions: ptr::null(), + scope_num_functions: 0, interproc: InterprocInferenceInput { iter: 1, max_iters: 1, @@ -5643,6 +6233,110 @@ pub extern "C" fn r2sleigh_infer_type_writeback_json_ex( fcn_addr, fcn_name, external_context_json, + scope_functions: ptr::null(), + scope_num_functions: 0, + interproc: InterprocInferenceInput { + iter: interproc_iter.max(1), + max_iters: interproc_max_iters.max(1), + converged: interproc_converged != 0, + scope_json: &scope, + }, + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sleigh_infer_type_writeback_json_scope_ex( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + external_context_json: *const c_char, + interproc_iter: usize, + interproc_max_iters: usize, + interproc_converged: i32, + interproc_scope_json: *const c_char, + scope_functions: *const analysis::sym::R2ILFunctionBlocks, + scope_num_functions: usize, +) -> *mut c_char { + let scope = cstr_or_default(interproc_scope_json, "{}"); + infer_type_writeback_json_impl(TypeWritebackInferenceInput { + ctx, + blocks, + num_blocks, + fcn_addr, + fcn_name, + external_context_json, + scope_functions, + scope_num_functions, + interproc: InterprocInferenceInput { + iter: interproc_iter.max(1), + max_iters: interproc_max_iters.max(1), + converged: interproc_converged != 0, + scope_json: &scope, + }, + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sleigh_function_facts_json_scope_ex( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + external_context_json: *const c_char, + interproc_iter: usize, + interproc_max_iters: usize, + interproc_converged: i32, + interproc_scope_json: *const c_char, + scope_functions: *const analysis::sym::R2ILFunctionBlocks, + scope_num_functions: usize, +) -> *mut c_char { + let scope = cstr_or_default(interproc_scope_json, "{}"); + function_facts_json_impl(TypeWritebackInferenceInput { + ctx, + blocks, + num_blocks, + fcn_addr, + fcn_name, + external_context_json, + scope_functions, + scope_num_functions, + interproc: InterprocInferenceInput { + iter: interproc_iter.max(1), + max_iters: interproc_max_iters.max(1), + converged: interproc_converged != 0, + scope_json: &scope, + }, + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sleigh_function_plan_json_scope_ex( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + external_context_json: *const c_char, + interproc_iter: usize, + interproc_max_iters: usize, + interproc_converged: i32, + interproc_scope_json: *const c_char, + scope_functions: *const analysis::sym::R2ILFunctionBlocks, + scope_num_functions: usize, +) -> *mut c_char { + let scope = cstr_or_default(interproc_scope_json, "{}"); + function_plan_json_impl(TypeWritebackInferenceInput { + ctx, + blocks, + num_blocks, + fcn_addr, + fcn_name, + external_context_json, + scope_functions, + scope_num_functions, interproc: InterprocInferenceInput { iter: interproc_iter.max(1), max_iters: interproc_max_iters.max(1), @@ -5652,6 +6346,93 @@ pub extern "C" fn r2sleigh_infer_type_writeback_json_ex( }) } +#[unsafe(no_mangle)] +pub extern "C" fn r2sleigh_function_analysis_artifact_cache_key( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + external_context_json: *const c_char, + interproc_max_iters: usize, + interproc_scope_json: *const c_char, +) -> u64 { + let Some(function_input) = + types::build_function_input(ctx, blocks, num_blocks, fcn_addr, fcn_name) + else { + return 0; + }; + let external_context = cstr_or_default(external_context_json, "{}"); + let interproc_scope = cstr_or_default(interproc_scope_json, "{}"); + types::function_analysis_artifact_cache_identity_hash( + &function_input, + &external_context, + &interproc_scope, + interproc_max_iters.max(1), + None, + ) + .unwrap_or(0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sleigh_function_interproc_summary_json_ex( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + external_context_json: *const c_char, + interproc_max_iters: usize, + interproc_scope_json: *const c_char, +) -> *mut c_char { + let Some(function_input) = + types::build_function_input(ctx, blocks, num_blocks, fcn_addr, fcn_name) + else { + return ptr::null_mut(); + }; + let external_context = cstr_or_default(external_context_json, "{}"); + let interproc_scope = cstr_or_default(interproc_scope_json, "{}"); + let Some(summary_json) = types::function_root_interproc_summary_json_with_scope( + &function_input, + &external_context, + &interproc_scope, + interproc_max_iters.max(1), + None, + ) else { + return ptr::null_mut(); + }; + CString::new(summary_json).map_or(ptr::null_mut(), |c| c.into_raw()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2dec_semantic_worker_linearization_scope_ffi( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + block_count: usize, + loop_count: usize, + back_edge_count: usize, + max_switch_cases: usize, + scope_functions: *const analysis::sym::R2ILFunctionBlocks, + scope_num_functions: usize, +) -> *mut c_char { + semantic_worker_linearization_impl(SemanticWorkerLinearizationInput { + ctx, + blocks, + num_blocks, + fcn_addr, + fcn_name, + block_count, + loop_count, + back_edge_count, + max_switch_cases, + scope_functions, + scope_num_functions, + }) +} + #[unsafe(no_mangle)] pub extern "C" fn r2sleigh_alias_function_analysis_artifact_cache( ctx: *const R2ILContext, @@ -7174,8 +7955,7 @@ mod tests { max_switch_cases: 40, }; - let reason = - decompiler_cfg_guard_reason_from_summary(&summary).expect("guard reason expected"); + let reason = r2dec::cfg_guard_reason_from_summary(&summary).expect("guard reason expected"); assert!( reason.contains("dense switch") || reason.contains("max_switch_cases"), "unexpected reason: {reason}" @@ -7192,8 +7972,7 @@ mod tests { max_switch_cases: 0, }; - let reason = - decompiler_cfg_guard_reason_from_summary(&summary).expect("guard reason expected"); + let reason = r2dec::cfg_guard_reason_from_summary(&summary).expect("guard reason expected"); assert!( reason.contains("back_edges=38"), "expected back-edge detail in reason, got: {reason}" @@ -7210,7 +7989,192 @@ mod tests { max_switch_cases: 0, }; - assert_eq!(decompiler_cfg_guard_reason_from_summary(&summary), None); + assert_eq!(r2dec::cfg_guard_reason_from_summary(&summary), None); + } + + #[test] + fn r2dec_cfg_guard_comment_ffi_reports_complex_loop_graph() { + let name = CString::new("fcn.140010138").unwrap(); + let out = r2dec_cfg_guard_comment_ffi(name.as_ptr(), 107, 9, 17, 0); + assert!(!out.is_null()); + let rendered = unsafe { CStr::from_ptr(out) }.to_str().unwrap().to_string(); + assert!(rendered.contains("r2dec fallback")); + assert!(rendered.contains("complex loop graph")); + r2il_string_free(out); + } + + #[test] + fn r2dec_cfg_guard_comment_ffi_returns_null_for_benign_summary() { + let name = CString::new("sym.small").unwrap(); + let out = r2dec_cfg_guard_comment_ffi(name.as_ptr(), 12, 1, 1, 0); + assert!(out.is_null()); + } + + fn test_compiled_condition(expr: &str) -> r2sym::BackwardConditionSummary { + r2sym::BackwardConditionSummary { + simplified: expr.to_string(), + terms: vec![expr.to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + } + } + + fn test_large_cfg_semantic_artifact() -> r2sym::SemanticArtifact { + let compiled = test_compiled_condition("x == 0"); + let region = r2sym::SemanticRegion { + anchor: 0x401000, + frontier: std::collections::BTreeSet::from([0x401020, 0x401030]), + control: vec![ + r2sym::Judged::new( + r2sym::ControlFact { + target: 0x401020, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("x == 0".to_string()), + compiled: Some(compiled.clone()), + }, + r2sym::SemanticEvidence::exact(), + ), + r2sym::Judged::new( + r2sym::ControlFact { + target: 0x401030, + status: r2sym::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + condition: Some("x != 0".to_string()), + compiled: None, + }, + r2sym::SemanticEvidence::exact(), + ), + r2sym::Judged::new( + r2sym::ControlFact { + target: 0x401020, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: None, + condition: Some("x == 0".to_string()), + compiled: Some(compiled.clone()), + }, + r2sym::SemanticEvidence::exact(), + ), + ], + memory: Vec::new(), + pre: Vec::new(), + post: Vec::new(), + targets: vec![ + r2sym::Judged::new( + r2sym::TargetFact { + target: 0x401020, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + }, + r2sym::SemanticEvidence::exact(), + ), + r2sym::Judged::new( + r2sym::TargetFact { + target: 0x401030, + status: r2sym::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + }, + r2sym::SemanticEvidence::exact(), + ), + ], + }; + r2sym::SemanticArtifact { + stage: r2sym::RefinementStage::Residual, + granularity: r2sym::ArtifactGranularity::Regioned, + execution: r2sym::ExecutionModel::Native, + body: r2sym::SemanticArtifactBody::Native(r2sym::NativeArtifactBody { + summary: r2sym::NativeFunctionSummary { + slice_class: r2sym::SliceClass::Worker, + closure_functions: 4, + helper_functions: 3, + derived_summaries: 0, + derived_diagnostics: r2sym::DerivedSummaryDiagnostics::default(), + }, + regions: std::iter::once((region.key(), region)).collect(), + }), + diagnostics: r2sym::SemanticArtifactDiagnostics { + branches_evaluated: 1, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: true, + residual_reasons: vec![r2sym::ResidualReason::LargeCfg], + ambiguous_targets: Vec::new(), + cache_hit: false, + }, + } + } + + #[test] + fn semantic_type_fallback_payload_preserves_assumptions() { + let compiled = test_large_cfg_semantic_artifact(); + assert!( + r2types::semantic_artifact_prefers_bounded_type_plan(&compiled), + "expected large-CFG worker artifact to use bounded semantic type fallback" + ); + + let assumptions = r2ssa::AssumptionSet::new(vec![r2ssa::AnalysisAssumption { + id: Some("seed-rdi".to_string()), + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::User, + subject: r2ssa::AssumptionSubject::Register { + name: "rdi".to_string(), + }, + value: r2ssa::AssumptionValue::Constant { value: 0xdead }, + }]); + let function_facts = semantic_fallback_function_facts(&compiled, &assumptions); + let payload = semantic_type_fallback_payload( + "fcn.401000", + "x86-64", + 64, + InterprocInferenceInput { + iter: 0, + max_iters: 0, + converged: true, + scope_json: "{}", + }, + &compiled, + &function_facts, + None, + ); + let value = serde_json::to_value(payload).expect("payload should serialize"); + let Some(items) = value["assumptions"]["items"].as_array() else { + panic!("expected serialized assumptions, got {value:?}"); + }; + assert_eq!( + items.len(), + 1, + "expected one serialized assumption: {value:?}" + ); + assert_eq!( + items[0]["subject"]["register"]["name"].as_str(), + Some("rdi"), + "expected register assumption to survive fallback payload: {value:?}" + ); + } + + #[test] + fn decompile_ready_large_cfg_worker_keeps_real_decompile_path() { + let compiled = test_large_cfg_semantic_artifact(); + assert!( + r2dec::preferred_semantic_fallback_comment("fcn.401000", Some(&compiled)).is_none() + ); + } + + #[test] + fn semantic_fallback_text_reports_canonical_region_counts() { + let compiled = test_large_cfg_semantic_artifact(); + let output = r2dec::semantic_fallback_comment("_401000", Some(&compiled)) + .expect("typed semantic fallback comment"); + assert!(output.contains("semantic fallback: worker slice in residual mode")); + assert!(output.contains("regions=1")); + assert!(output.contains("actionable_conditions=3")); + assert!(output.contains("exact_conditions=3")); } #[test] @@ -7419,7 +8383,7 @@ mod integration_tests { r2il_free(ctx_ptr); } - #[cfg(feature = "arm")] + #[cfg(any(feature = "arm", feature = "mips"))] fn profile_for_arch(arch: &str) -> String { let arch_cstr = CString::new(arch).unwrap(); let ctx_ptr = r2il_arch_init(arch_cstr.as_ptr()); @@ -7440,7 +8404,7 @@ mod integration_tests { profile } - #[cfg(feature = "arm")] + #[cfg(any(feature = "arm", feature = "mips"))] fn role_target(profile: &str, role: &str) -> Option { profile .lines() @@ -7553,6 +8517,47 @@ mod integration_tests { r2il_free(ctx_ptr); } + #[test] + #[cfg(feature = "mips")] + fn create_disassembler_for_arch_mips32be() { + let (spec, disasm) = + create_disassembler_for_arch("mips32be").expect("mips32be disassembler"); + assert_eq!(spec.name, "mips32be"); + assert!(spec.addr_size > 0); + assert_eq!(spec.instruction_endianness, r2il::Endianness::Big); + assert_eq!(spec.memory_endianness, r2il::Endianness::Big); + assert_eq!( + disasm.userop_name(0), + userop_map_for_arch("mips32be").get(&0).map(String::as_str) + ); + } + + #[test] + #[cfg(feature = "mips")] + fn r2il_arch_init_mips32be_loaded() { + let arch_cstr = CString::new("mips32be").unwrap(); + let ctx_ptr = r2il_arch_init(arch_cstr.as_ptr()); + assert!(!ctx_ptr.is_null(), "context pointer should not be null"); + assert_eq!( + r2il_is_loaded(ctx_ptr), + 1, + "mips32be context should be loaded" + ); + r2il_free(ctx_ptr); + } + + #[test] + #[cfg(feature = "mips")] + fn mips32be_reg_profile_includes_arg_roles() { + let profile = profile_for_arch("mips32be"); + for role in ["PC", "SP", "A0", "A1", "A2", "A3", "R0"] { + assert!( + role_target(&profile, role).is_some(), + "mips32be profile should define ={role}" + ); + } + } + #[test] fn score_global_links_prefers_stronger_struct_type_signal() { let block = r2ssa::SSABlock { @@ -7820,13 +8825,223 @@ mod integration_tests { Some("HeapAlloc"), "payload={output}" ); - assert_eq!( - payload["ret_type"].as_str(), - Some("void*"), + assert!( + matches!(payload["ret_type"].as_str(), Some("void *" | "void*")), "payload={output}" ); } + #[test] + fn function_interproc_summary_json_ex_returns_typed_root_summary() { + let arch = CString::new("x86-64").expect("valid arch"); + let ctx = r2il_arch_init(arch.as_ptr()); + assert!(!ctx.is_null(), "context should initialize"); + + let mut block = R2ILBlock::new(0x401000, 4); + block.push(R2ILOp::Call { + target: Varnode::constant(0x2000, 8), + }); + block.push(R2ILOp::Return { + target: Varnode { + space: r2il::SpaceId::Register, + offset: 0, + size: 8, + meta: None, + }, + }); + let raw_block = Box::into_raw(Box::new(block)); + let blocks = [raw_block as *const R2ILBlock]; + + let func_name = CString::new("sym.alloc_wrapper").expect("valid function name"); + let external_context = CString::new("{}").expect("valid context"); + let scope_json = CString::new(r#"{"seeds":[{"id":8192,"name":"sym.imp.malloc"}]}"#) + .expect("valid scope json"); + + let out = r2sleigh_function_interproc_summary_json_ex( + ctx, + blocks.as_ptr(), + blocks.len(), + 0x401000, + func_name.as_ptr(), + external_context.as_ptr(), + 4, + scope_json.as_ptr(), + ); + assert!(!out.is_null(), "summary json should not be null"); + let output = unsafe { CStr::from_ptr(out) }.to_string_lossy().to_string(); + let payload: serde_json::Value = + serde_json::from_str(&output).expect("summary should parse"); + + r2il_string_free(out); + r2il_block_free(raw_block); + r2il_free(ctx); + + assert_eq!(payload["return_relation"].as_str(), Some("HeapAlloc")); + } + + #[test] + fn function_analysis_artifact_cache_key_changes_with_interproc_scope() { + let arch = CString::new("x86-64").expect("valid arch"); + let ctx = r2il_arch_init(arch.as_ptr()); + assert!(!ctx.is_null(), "context should initialize"); + + let mut block = R2ILBlock::new(0x401000, 4); + block.push(R2ILOp::Call { + target: Varnode::constant(0x2000, 8), + }); + block.push(R2ILOp::Return { + target: Varnode { + space: r2il::SpaceId::Register, + offset: 0, + size: 8, + meta: None, + }, + }); + let raw_block = Box::into_raw(Box::new(block)); + let blocks = [raw_block as *const R2ILBlock]; + + let func_name = CString::new("sym.alloc_wrapper").expect("valid function name"); + let external_context = CString::new("{}").expect("valid context"); + let empty_scope = CString::new("{}").expect("valid scope"); + let seeded_scope = CString::new(r#"{"seeds":[{"id":8192,"name":"sym.imp.malloc"}]}"#) + .expect("valid seeded scope"); + + let root_key = r2sleigh_function_analysis_artifact_cache_key( + ctx, + blocks.as_ptr(), + blocks.len(), + 0x401000, + func_name.as_ptr(), + external_context.as_ptr(), + 1, + empty_scope.as_ptr(), + ); + let seeded_key = r2sleigh_function_analysis_artifact_cache_key( + ctx, + blocks.as_ptr(), + blocks.len(), + 0x401000, + func_name.as_ptr(), + external_context.as_ptr(), + 1, + seeded_scope.as_ptr(), + ); + + r2il_block_free(raw_block); + r2il_free(ctx); + + assert_ne!(root_key, 0); + assert_ne!(seeded_key, 0); + assert_ne!(root_key, seeded_key); + } + + #[test] + fn function_analysis_artifact_cache_key_changes_with_interproc_budget() { + let arch = CString::new("x86-64").expect("valid arch"); + let ctx = r2il_arch_init(arch.as_ptr()); + assert!(!ctx.is_null(), "context should initialize"); + + let mut block = R2ILBlock::new(0x401000, 4); + block.push(R2ILOp::Call { + target: Varnode::constant(0x2000, 8), + }); + block.push(R2ILOp::Return { + target: Varnode { + space: r2il::SpaceId::Register, + offset: 0, + size: 8, + meta: None, + }, + }); + let raw_block = Box::into_raw(Box::new(block)); + let blocks = [raw_block as *const R2ILBlock]; + + let func_name = CString::new("sym.alloc_wrapper").expect("valid function name"); + let external_context = CString::new("{}").expect("valid context"); + let scope = CString::new(r#"{"seeds":[{"id":8192,"name":"sym.imp.malloc"}]}"#) + .expect("valid scope"); + + let low_budget_key = r2sleigh_function_analysis_artifact_cache_key( + ctx, + blocks.as_ptr(), + blocks.len(), + 0x401000, + func_name.as_ptr(), + external_context.as_ptr(), + 1, + scope.as_ptr(), + ); + let high_budget_key = r2sleigh_function_analysis_artifact_cache_key( + ctx, + blocks.as_ptr(), + blocks.len(), + 0x401000, + func_name.as_ptr(), + external_context.as_ptr(), + 4, + scope.as_ptr(), + ); + + r2il_block_free(raw_block); + r2il_free(ctx); + + assert_ne!(low_budget_key, 0); + assert_ne!(high_budget_key, 0); + assert_ne!(low_budget_key, high_budget_key); + } + + #[test] + fn function_analysis_artifact_cache_key_changes_with_function_name() { + let arch = CString::new("x86-64").expect("valid arch"); + let ctx = r2il_arch_init(arch.as_ptr()); + assert!(!ctx.is_null(), "context should initialize"); + + let mut block = R2ILBlock::new(0x401000, 4); + block.push(R2ILOp::Return { + target: Varnode { + space: r2il::SpaceId::Register, + offset: 0, + size: 8, + meta: None, + }, + }); + let raw_block = Box::into_raw(Box::new(block)); + let blocks = [raw_block as *const R2ILBlock]; + + let first_name = CString::new("sym.first").expect("valid function name"); + let second_name = CString::new("sym.second").expect("valid function name"); + let external_context = CString::new("{}").expect("valid context"); + let scope = CString::new("{}").expect("valid scope"); + + let first_key = r2sleigh_function_analysis_artifact_cache_key( + ctx, + blocks.as_ptr(), + blocks.len(), + 0x401000, + first_name.as_ptr(), + external_context.as_ptr(), + 1, + scope.as_ptr(), + ); + let second_key = r2sleigh_function_analysis_artifact_cache_key( + ctx, + blocks.as_ptr(), + blocks.len(), + 0x401000, + second_name.as_ptr(), + external_context.as_ptr(), + 1, + scope.as_ptr(), + ); + + r2il_block_free(raw_block); + r2il_free(ctx); + + assert_ne!(first_key, 0); + assert_ne!(second_key, 0); + assert_ne!(first_key, second_key); + } + #[test] fn direct_call_targets_json_reports_constant_call_target() { let arch = CString::new("x86-64").expect("valid arch"); @@ -9175,7 +10390,8 @@ mod integration_tests { .expect("analysis artifact"); let rendered = artifact - .type_facts + .function_facts + .types .merged_signature .as_ref() .and_then(|sig| sig.params.first()) @@ -9189,10 +10405,10 @@ mod integration_tests { && compact.ends_with('*') && !compact.eq_ignore_ascii_case("void*"), "expected lifted-byte x86 artifact to override arg0 to a struct pointer, got signature={:?}, slot_overrides={:?}, slot_fields={:?}, type_db={:?}, semantic_structs={:?}, semantic_diagnostics={:?}, raw_structs={:?}, raw_diagnostics={:?}, pattern_ssa_blocks={:?}", - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs, + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs, semantic_structs, semantic_diagnostics, raw_structs, @@ -9254,16 +10470,17 @@ mod integration_tests { assert_eq!( artifact - .type_facts + .function_facts + .types .slot_type_overrides .get(&0) .map(String::as_str), Some("struct sla_struct_420703e08f70f00e *"), "expected live-context detached artifact to keep the local struct override, got merged_signature={:?}, slot_overrides={:?}, slot_fields={:?}, type_db={:?}", - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs ); } @@ -9337,7 +10554,8 @@ mod integration_tests { ); let db_struct = artifact - .type_facts + .function_facts + .types .external_type_db .structs .get("sla_struct_420703e08f70f00e") @@ -9402,7 +10620,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); decompiler.set_function_names(HashMap::from([ (0x401140, "sym.imp.memcpy".to_string()), (0x401150, "sym.imp.malloc".to_string()), @@ -9455,72 +10673,72 @@ mod integration_tests { .ops = pattern_ssa_blocks[0].ops.clone(); manual_func = manual_func.with_name("dbg.test_struct_array_index"); let mut manual_decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - manual_decompiler.set_type_facts(artifact.type_facts.clone()); + manual_decompiler.set_function_facts(artifact.function_facts.clone()); let manual_output = manual_decompiler.decompile(&manual_func); assert!( output.contains("[idx].f_8"), "expected detached artifact store rendering in decompiled output, got:\n{output}\nmanual_output:\n{manual_output}\ntail_ops={tail_ops:?}\nregister_params={:?}\nmerged_signature={:?}\nslot_overrides={:?}\nslot_fields={:?}\ntype_db={:?}", - artifact.type_facts.register_params, - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs + artifact.function_facts.types.register_params, + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs ); assert!( output.contains("[idx].f_34"), "expected detached artifact load rendering in decompiled output, got:\n{output}\nmanual_output:\n{manual_output}\ntail_ops={tail_ops:?}\nregister_params={:?}\nmerged_signature={:?}\nslot_overrides={:?}\nslot_fields={:?}\ntype_db={:?}", - artifact.type_facts.register_params, - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs + artifact.function_facts.types.register_params, + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs ); assert!( output.contains("return arr[idx].f_8 + arr[idx].f_34;") || output.contains("return arr[idx].f_34 + arr[idx].f_8;"), "expected detached artifact return to preserve both member loads, got:\n{output}\nmanual_output:\n{manual_output}\ntail_ops={tail_ops:?}\nregister_params={:?}\nmerged_signature={:?}\nslot_overrides={:?}\nslot_fields={:?}\ntype_db={:?}", - artifact.type_facts.register_params, - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs + artifact.function_facts.types.register_params, + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs ); assert!( !output.contains("local_c ="), "dead x86 stack-home index carrier should not leak into decompiled output, got:\n{output}\nmanual_output:\n{manual_output}\ntail_ops={tail_ops:?}\nregister_params={:?}\nmerged_signature={:?}\nslot_overrides={:?}\nslot_fields={:?}\ntype_db={:?}", - artifact.type_facts.register_params, - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs + artifact.function_facts.types.register_params, + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs ); assert!( !output.contains("local_"), "autogenerated x86 stack-home locals should not leak into decompiled output, got:\n{output}\nmanual_output:\n{manual_output}\ntail_ops={tail_ops:?}\nregister_params={:?}\nmerged_signature={:?}\nslot_overrides={:?}\nslot_fields={:?}\ntype_db={:?}", - artifact.type_facts.register_params, - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs + artifact.function_facts.types.register_params, + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs ); assert!( output.contains("arr[idx].f_8 = v;"), "expected detached artifact store to inline the parameter value, got:\n{output}\nmanual_output:\n{manual_output}\ntail_ops={tail_ops:?}\nregister_params={:?}\nmerged_signature={:?}\nslot_overrides={:?}\nslot_fields={:?}\ntype_db={:?}", - artifact.type_facts.register_params, - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs + artifact.function_facts.types.register_params, + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs ); assert!( !output.contains("(int64_t)arr[idx].f_34"), "x86 scalar return should not widen the member load in decompiled output, got:\n{output}\nmanual_output:\n{manual_output}\ntail_ops={tail_ops:?}\nregister_params={:?}\nmerged_signature={:?}\nslot_overrides={:?}\nslot_fields={:?}\ntype_db={:?}", - artifact.type_facts.register_params, - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs + artifact.function_facts.types.register_params, + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs ); } @@ -9583,7 +10801,8 @@ mod integration_tests { assert_eq!( artifact - .type_facts + .function_facts + .types .merged_signature .as_ref() .expect("merged signature") @@ -9591,7 +10810,7 @@ mod integration_tests { .len(), 2, "expected authoritative external signature to keep two params, got merged_signature={:?}", - artifact.type_facts.merged_signature + artifact.function_facts.types.merged_signature ); assert_eq!( artifact.writeback_plan.signature.params.len(), @@ -9602,16 +10821,16 @@ mod integration_tests { assert_eq!( artifact.writeback_plan.signature.params[0].name, "src", "expected first param name to come from external signature, got merged_signature={:?}, writeback_signature={:?}", - artifact.type_facts.merged_signature, artifact.writeback_plan.signature + artifact.function_facts.types.merged_signature, artifact.writeback_plan.signature ); assert_eq!( artifact.writeback_plan.signature.params[1].name, "len", "expected second param name to come from external signature, got merged_signature={:?}, writeback_signature={:?}", - artifact.type_facts.merged_signature, artifact.writeback_plan.signature + artifact.function_facts.types.merged_signature, artifact.writeback_plan.signature ); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); decompiler.set_function_names(HashMap::from([ (0x401140, "sym.imp.memcpy".to_string()), (0x401150, "sym.imp.malloc".to_string()), @@ -9653,19 +10872,19 @@ mod integration_tests { assert!( output.contains("sym.imp.memcpy(buf, src, len);"), "expected detached x86 alloc_and_copy to keep the malloc owner for memcpy, got:\n{output}\nssa_ops={ssa_ops:?}\nmerged_signature={:?}\nwriteback_signature={:?}", - artifact.type_facts.merged_signature, + artifact.function_facts.types.merged_signature, artifact.writeback_plan.signature ); assert!( output.contains("buf[len] = 0;"), "expected detached x86 alloc_and_copy to keep the malloc owner for the NUL store, got:\n{output}\nmerged_signature={:?}\nwriteback_signature={:?}", - artifact.type_facts.merged_signature, + artifact.function_facts.types.merged_signature, artifact.writeback_plan.signature ); assert!( output.contains("return buf;"), "expected detached x86 alloc_and_copy to return the owned malloc result, got:\n{output}\nmerged_signature={:?}\nwriteback_signature={:?}", - artifact.type_facts.merged_signature, + artifact.function_facts.types.merged_signature, artifact.writeback_plan.signature ); } @@ -9730,7 +10949,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); decompiler.set_function_names(HashMap::from([ (0x401110, "sym.imp.printf".to_string()), (0x401140, "sym.imp.memcpy".to_string()), @@ -9868,7 +11087,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); decompiler.set_function_names(HashMap::from([(0x401130, "sym.imp.strcmp".to_string())])); decompiler.set_strings(HashMap::from([(0x403014, "secret123".to_string())])); decompiler.set_known_function_signatures(HashMap::from([( @@ -10006,7 +11225,7 @@ mod integration_tests { .collect(); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); let output = decompiler.decompile(&artifact.ssa_func); let output_without_types = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()).decompile(&artifact.ssa_func); @@ -10101,9 +11320,9 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); let output = decompiler.decompile(&artifact.ssa_func); - let visible_bindings = artifact.type_facts.visible_bindings.clone(); + let visible_bindings = artifact.function_facts.types.visible_bindings.clone(); assert!( output.contains("if (x != y)") @@ -10181,7 +11400,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); decompiler.set_function_names(HashMap::from([(0x401160, "sym.imp.setlocale".to_string())])); decompiler.set_strings(HashMap::from([(0x403040, "C".to_string())])); decompiler.set_known_function_signatures(HashMap::from([( @@ -10290,7 +11509,7 @@ mod integration_tests { }) .to_string(); - let mut artifact = crate::types::build_detached_function_analysis_artifact( + let artifact = crate::types::build_detached_function_analysis_artifact( &blocks, "dbg.test_setlocale_wrapper", Some(&arch), @@ -10303,16 +11522,12 @@ mod integration_tests { let function_names = HashMap::from([(0x401160, "sym.imp.setlocale".to_string())]); let strings = HashMap::from([(0x403040, "C".to_string())]); - crate::types::enrich_known_function_signatures_from_names( - &mut artifact.type_facts, - &function_names, - 64, - ); let input = crate::decompiler::decompiler_input_from_artifact( artifact, function_names, strings, HashMap::new(), + 64, ); let decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); let output = decompiler.decompile_input(&input); @@ -10408,7 +11623,7 @@ mod integration_tests { ) .expect("analysis artifact"); - let mut type_facts = artifact.type_facts.clone(); + let mut type_facts = artifact.function_facts.types.clone(); type_facts.known_function_signatures.extend(HashMap::from([ ( "sym.imp.strlen".to_string(), @@ -10584,6 +11799,7 @@ mod integration_tests { function_names, HashMap::new(), HashMap::new(), + 64, ); let decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); let output = decompiler.decompile_input(&input); @@ -10691,7 +11907,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); decompiler.set_function_names(HashMap::from([(0x401100, "sym.imp.strlen".to_string())])); decompiler.set_known_function_signatures(HashMap::from([( "sym.imp.strlen".to_string(), @@ -10817,7 +12033,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); decompiler.set_function_names(HashMap::from([(0x401100, "sym.imp.strlen".to_string())])); decompiler.set_known_function_signatures(HashMap::from([( "sym.imp.strlen".to_string(), @@ -10938,7 +12154,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); decompiler.set_function_names(HashMap::from([(0x401100, "sym.imp.strlen".to_string())])); decompiler.set_known_function_signatures(HashMap::from([( "sym.imp.strlen".to_string(), @@ -11053,12 +12269,14 @@ mod integration_tests { HashMap::from([(0x401100, "sym.imp.strlen".to_string())]), HashMap::new(), HashMap::new(), + 64, ); let empty_input = crate::decompiler::decompiler_input_from_artifact( artifact.clone(), HashMap::new(), HashMap::new(), HashMap::new(), + 64, ); let decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); let output = decompiler.decompile_input(&input); @@ -11760,6 +12978,133 @@ mod integration_tests { ); } + #[test] + fn detached_symbolic_branch_facts_prune_self_xor_guard_in_decompiler() { + use r2il::{R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; + + fn reg(offset: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Register, + offset, + size, + meta: None, + } + } + + fn konst(value: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: value, + size, + meta: None, + } + } + + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("RAX", 0, 8)); + arch.add_register(RegisterDef::new("EDI", 56, 4)); + arch.add_register(RegisterDef::new("RDI", 56, 8)); + + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntXor { + dst: reg(0x80, 8), + a: reg(56, 8), + b: reg(56, 8), + }, + R2ILOp::CBranch { + target: konst(0x1010, 8), + cond: reg(0x80, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![ + R2ILOp::Copy { + dst: reg(0, 8), + src: konst(0, 8), + }, + R2ILOp::Return { target: reg(0, 8) }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![ + R2ILOp::Copy { + dst: reg(0, 8), + src: konst(1, 8), + }, + R2ILOp::Return { target: reg(0, 8) }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + + let artifact = crate::types::build_detached_function_analysis_artifact( + &blocks, + "dbg.test_symbolic_xor_guard", + Some(&arch), + 64, + false, + &HashMap::new(), + "{}", + ) + .expect("analysis artifact"); + + let semantics = artifact + .function_facts + .semantics + .as_ref() + .expect("canonical semantics"); + assert_eq!(semantics.diagnostics.branches_pruned, 1); + let native = semantics.native_body().expect("native semantics"); + assert_eq!(native.regions.len(), 1); + let region = native.regions.values().next().expect("semantic region"); + assert!(region.targets.iter().any(|fact| { + fact.value.status == r2sym::SymbolicReachabilityStatus::Unreachable + && fact.value.branch_truth == Some(true) + })); + assert!(region.targets.iter().any(|fact| { + fact.value.status == r2sym::SymbolicReachabilityStatus::Reachable + && fact.value.branch_truth == Some(false) + })); + + let decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); + let output = + decompiler.decompile_input(&crate::decompiler::decompiler_input_from_artifact( + artifact, + HashMap::new(), + HashMap::new(), + HashMap::new(), + 64, + )); + + assert!( + output.contains("return 1;") || output.contains("return 0;"), + "expected decompiled function body, got:\n{output}" + ); + assert!( + !output.contains("if ("), + "symbolic branch facts should collapse the impossible guard, got:\n{output}" + ); + assert!( + !output.contains("return 1;"), + "impossible self-xor branch should be removed, got:\n{output}" + ); + } + #[test] fn live_arm64_main_atoi_arg_keeps_semantic_root() { use r2il::{R2ILBlock, R2ILOp, Varnode}; diff --git a/r2plugin/src/types.rs b/r2plugin/src/types.rs index 9999244..d93c378 100644 --- a/r2plugin/src/types.rs +++ b/r2plugin/src/types.rs @@ -1,14 +1,13 @@ +#[cfg(test)] +use crate::InferredParam; use crate::blocks::BlockSlice; use crate::context::{PluginCtxView, require_ctx_view}; -use crate::decompiler::{ - build_decompiler_env, decompiler_config_for_arch_name, normalize_sig_arch_name, -}; -use crate::helpers::{effective_ptr_bits, normalize_sim_name, resolve_function_name}; +use crate::decompiler::build_decompiler_env; +use crate::helpers::{effective_ptr_bits, resolve_function_name}; use crate::{ - ArchSpec, Disassembler, InferredParam, InferredParamJson, InferredSignatureCcJson, R2ILBlock, - R2ILContext, + ArchSpec, Disassembler, InferredParamJson, InferredSignatureCcJson, R2ILBlock, R2ILContext, }; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap}; use std::ffi::CString; use std::hash::{DefaultHasher, Hash, Hasher}; use std::io::{self, Write}; @@ -43,9 +42,8 @@ pub(crate) struct FunctionAnalysis { pub(crate) struct FunctionAnalysisArtifact { pub(crate) ssa_func: r2ssa::SsaArtifact, pub(crate) pattern_ssa_func: r2ssa::SsaArtifact, - pub(crate) type_facts: r2types::FunctionTypeFacts, + pub(crate) function_facts: r2types::FunctionFacts, pub(crate) writeback_plan: r2types::TypeWritebackPlan, - pub(crate) interproc_summary_set: Option, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -57,9 +55,12 @@ struct FunctionAnalysisCacheKey { #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct FunctionArtifactCacheKey { analysis: FunctionAnalysisCacheKey, + function_name_hash: u64, semantic_metadata_enabled: bool, external_context_hash: u64, interproc_scope_hash: u64, + interproc_max_iterations: u64, + symbolic_scope_hash: u64, } struct HasherWriter<'a, H: Hasher>(&'a mut H); @@ -92,6 +93,12 @@ fn hash_string_payload(payload: &str) -> u64 { hasher.finish() } +fn hash_value(value: &T) -> u64 { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + hasher.finish() +} + fn function_analysis_cache_key_parts( _function_name: &str, arch: Option<&ArchSpec>, @@ -103,6 +110,7 @@ fn function_analysis_cache_key_parts( }) } +#[allow(clippy::too_many_arguments)] fn function_artifact_cache_key_parts( function_name: &str, arch: Option<&ArchSpec>, @@ -110,12 +118,17 @@ fn function_artifact_cache_key_parts( semantic_metadata_enabled: bool, external_context_json: &str, interproc_scope_json: &str, + interproc_max_iterations: usize, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> Option { Some(FunctionArtifactCacheKey { analysis: function_analysis_cache_key_parts(function_name, arch, blocks)?, + function_name_hash: hash_string_payload(function_name), semantic_metadata_enabled, external_context_hash: hash_string_payload(external_context_json), interproc_scope_hash: hash_string_payload(interproc_scope_json), + interproc_max_iterations: interproc_max_iterations as u64, + symbolic_scope_hash: r2sym::stable_scope_hash(symbolic_scope), }) } @@ -162,19 +175,28 @@ fn rename_function_analysis_artifact( let FunctionAnalysisArtifact { ssa_func, pattern_ssa_func, - type_facts, + function_facts, writeback_plan, - interproc_summary_set, + .. } = artifact; FunctionAnalysisArtifact { ssa_func: ssa_func.with_name(function_name), pattern_ssa_func: pattern_ssa_func.with_name(function_name), - type_facts, + function_facts, writeback_plan, - interproc_summary_set, } } +fn merged_assumption_usage( + prepared: &r2ssa::SsaArtifact, + function_facts: &r2types::FunctionFacts, +) -> r2ssa::AssumptionUsageReport { + let mut usage = prepared.facts().assumption_usage.clone(); + usage.extend(&function_facts.assumption_usage); + usage +} + +#[cfg(test)] fn type_like_to_ctype(ty: &r2types::CTypeLike) -> r2dec::CType { match ty { r2types::CTypeLike::Void => r2dec::CType::Void, @@ -197,85 +219,15 @@ fn type_like_to_ctype(ty: &r2types::CTypeLike) -> r2dec::CType { } } -fn insert_known_signature_aliases( - known: &mut std::collections::HashMap, - name: &str, - sig: &r2types::FunctionType, -) { - if name.is_empty() { - return; - } - known.insert(name.to_string(), sig.clone()); - - for prefix in ["sym.imp.", "sym.", "imp.", "dbg.", "fcn."] { - if let Some(stripped) = name.strip_prefix(prefix) - && !stripped.is_empty() - { - known.insert(stripped.to_string(), sig.clone()); - } - } -} - -pub(crate) fn enrich_known_function_signatures_from_names( - type_facts: &mut r2types::FunctionTypeFacts, - function_names: &std::collections::HashMap, - ptr_bits: u32, -) { - let registry = r2types::SignatureRegistry::from_embedded_json(); - - for name in function_names.values() { - let mut candidates = vec![name.clone()]; - if let Some(sim_name) = normalize_sim_name(name) - && sim_name != name - && !candidates.iter().any(|candidate| candidate == sim_name) - { - candidates.push(sim_name.to_string()); - } - - let resolved = candidates.into_iter().find_map(|candidate| { - let mut arena = r2types::TypeArena::default(); - registry - .resolve(&candidate, &mut arena, ptr_bits) - .map(|sig| { - ( - candidate, - r2types::FunctionType { - return_type: r2types::to_c_type_like(&arena, sig.ret), - params: sig - .params - .into_iter() - .map(|param| r2types::to_c_type_like(&arena, param)) - .collect(), - variadic: sig.variadic, - }, - ) - }) - }); - - let Some((resolved_name, sig)) = resolved else { - continue; - }; - - insert_known_signature_aliases(&mut type_facts.known_function_signatures, name, &sig); - if resolved_name != *name { - insert_known_signature_aliases( - &mut type_facts.known_function_signatures, - &resolved_name, - &sig, - ); - } - } -} - -fn signature_cc_to_writeback(sig: &InferredSignatureCcJson) -> r2types::InferredSignature { - r2types::InferredSignature { +pub(crate) fn signature_to_json(sig: &r2types::InferredSignature) -> InferredSignatureCcJson { + InferredSignatureCcJson { function_name: sig.function_name.clone(), signature: sig.signature.clone(), ret_type: sig.ret_type.clone(), params: sig .params .iter() - .map(|param| r2types::InferredSignatureParam { + .map(|param| InferredParamJson { name: param.name.clone(), param_type: param.param_type.clone(), }) @@ -287,63 +239,39 @@ fn signature_cc_to_writeback(sig: &InferredSignatureCcJson) -> r2types::Inferred } } -fn var_prot_to_writeback(var: &VarProt) -> r2types::RecoveredVariable { - r2types::RecoveredVariable { - name: var.name.clone(), - kind: var.kind.clone(), - delta: var.delta, - var_type: var.var_type.clone(), - isarg: var.isarg, - reg: var.reg.clone(), - } -} +pub(crate) type VarProt = r2types::RecoveredVariable; +pub(crate) type TypeHintRank = r2types::TypeHintRank; +pub(crate) type TypeHint = r2types::TypeHint; -fn writeback_diagnostics_from_plugin( - diagnostics: crate::TypeWritebackDiagnosticsJson, -) -> r2types::TypeWritebackDiagnostics { - r2types::TypeWritebackDiagnostics { - conflicts: diagnostics.conflicts, - warnings: diagnostics.warnings, - solver_warnings: diagnostics.solver_warnings, - } +fn var_prot_to_writeback(var: &VarProt) -> r2types::RecoveredVariable { + var.clone() } -fn struct_decl_to_writeback(decl: crate::StructDeclCandidateJson) -> r2types::StructDeclCandidate { - r2types::StructDeclCandidate { - name: decl.name, - decl: decl.decl, - confidence: decl.confidence, - source: if decl.source == "external_type_db" { - r2types::StructDeclSource::ExternalTypeDb - } else { - r2types::StructDeclSource::LocalInferred - }, - fields: decl - .fields - .into_iter() - .map(|field| r2types::StructFieldCandidate { - name: field.name, - offset: field.offset, - field_type: field.field_type, - confidence: field.confidence, - }) - .collect(), - } +fn local_field_accesses_to_writeback( + accesses: Vec, +) -> Vec { + accesses + .into_iter() + .map(|access| r2types::LocalFieldAccessFact { + slot: access.arg_index, + field_offset: access.field_offset, + field_name: format!("f_{:x}", access.field_offset), + field_type: Some(r2types::size_to_type(access.access_size)), + }) + .collect() } -fn local_struct_artifacts_to_writeback( - struct_decls: Vec, - slot_type_overrides: std::collections::HashMap, - slot_field_profiles: std::collections::HashMap>, -) -> r2types::LocalStructArtifacts { - r2types::LocalStructArtifacts { - struct_decls: struct_decls - .into_iter() - .map(struct_decl_to_writeback) - .collect(), - slot_type_overrides, - slot_field_profiles, - } +fn recovered_signature_params_from_var_recovery( + params: Vec<&r2dec::variable::VarInfo>, +) -> Vec { + params + .into_iter() + .map(|param| r2types::RecoveredSignatureParam { + name: param.name.clone(), + ssa_var: param.ssa_var.clone(), + initial_ty: crate::ctype_to_type_like(¶m.ty), + }) + .collect() } fn build_var_recovery_ssa_blocks( @@ -383,6 +311,8 @@ fn function_artifact_cache_key( input: &FunctionInput<'_>, external_context_json: &str, interproc_scope_json: &str, + interproc_max_iterations: usize, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> Option { function_artifact_cache_key_parts( &input.function_name, @@ -391,10 +321,29 @@ fn function_artifact_cache_key( input.ctx.semantic_metadata_enabled, external_context_json, interproc_scope_json, + interproc_max_iterations, + symbolic_scope, + ) +} + +pub(crate) fn function_analysis_artifact_cache_identity_hash( + input: &FunctionInput<'_>, + external_context_json: &str, + interproc_scope_json: &str, + interproc_max_iterations: usize, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + function_artifact_cache_key( + input, + external_context_json, + interproc_scope_json, + interproc_max_iterations, + symbolic_scope, ) + .map(|key| hash_value(&key)) } -fn build_function_analysis_from_parts( +pub(crate) fn build_function_analysis_from_parts( function_name: &str, blocks: &[R2ILBlock], arch: Option<&ArchSpec>, @@ -431,6 +380,21 @@ pub(crate) fn build_function_analysis(input: &FunctionInput<'_>) -> Option, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + let analysis = build_function_analysis_from_parts(function_name, blocks, arch)?; + Some(r2sym::compile_semantic_artifact_default_with_scope( + &z3::Context::thread_local(), + &analysis.ssa_func, + symbolic_scope, + arch, + )) +} + #[derive(Debug, Clone, Default, serde::Deserialize)] struct InterprocScopeSeedNameJson { id: u64, @@ -501,20 +465,38 @@ fn parse_interproc_seed_summaries( seeds } -pub(crate) fn build_interproc_summary_set( +pub(crate) fn build_interproc_summary_set_with_scope( input: &FunctionInput<'_>, analysis: &FunctionAnalysis, scope_json: &str, max_iterations: usize, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> r2ssa::InterprocSummarySet { let root = r2ssa::InterprocFunctionId(input.function_addr); let seeds = parse_interproc_seed_summaries(scope_json); + let seeded_helpers = seeds + .keys() + .copied() + .collect::>(); + let mut functions = vec![r2ssa::InterprocFunctionInput { + id: root, + name: Some(input.function_name.clone()), + prepared: &analysis.ssa_func, + }]; + if let Some(scope) = symbolic_scope { + for function in scope.functions().values() { + if function.id == root || seeded_helpers.contains(&function.id) { + continue; + } + functions.push(r2ssa::InterprocFunctionInput { + id: function.id, + name: function.name.clone(), + prepared: &function.prepared, + }); + } + } r2ssa::solve_interproc_summary_set( - &[r2ssa::InterprocFunctionInput { - id: root, - name: Some(input.function_name.clone()), - prepared: &analysis.ssa_func, - }], + &functions, input.ctx.arch, Some(root), &seeds, @@ -527,129 +509,39 @@ pub(crate) fn build_interproc_summary_set( pub(crate) fn infer_signature_cc_from_analysis( input: &FunctionInput<'_>, analysis: &FunctionAnalysis, -) -> Option { +) -> Option { let env = build_decompiler_env(&input.ctx); let pattern_ssa_blocks = analysis.pattern_ssa_func.local_ssa_blocks(); - let evidence_ctx = collect_signature_type_evidence_context(&pattern_ssa_blocks); let mut var_recovery = r2dec::VariableRecovery::new(&env.cfg.sp_name, &env.cfg.fp_name, env.cfg.ptr_size); var_recovery.recover(&analysis.ssa_func); - let mut type_inference = r2types::TypeInference::new(env.cfg.ptr_size); - type_inference.set_prepared_ssa(&analysis.ssa_func); - type_inference.infer_function(&analysis.ssa_func); - - let mut inferred_params: Vec = var_recovery - .parameters() - .into_iter() - .map(|v| { - let initial_ty = type_like_to_ctype(&type_inference.get_type(&v.ssa_var)); - let mut evidence = - crate::collect_type_evidence_for_var(&evidence_ctx, &v.ssa_var, &initial_ty); - if matches!(initial_ty, r2dec::CType::Void | r2dec::CType::Unknown) { - crate::merge_initial_type_evidence( - &type_like_to_ctype(&type_inference.type_from_size(v.ssa_var.size)), - &mut evidence, - ); - } - let ty = crate::resolve_evidence_driven_type( - initial_ty, - v.ssa_var.size, - env.ptr_bits, - &evidence, - ); - let arg_index = v - .name - .strip_prefix("arg") - .and_then(|n| n.parse::().ok()) - .unwrap_or(usize::MAX); - InferredParam { - name: v.name.clone(), - ty, - arg_index, - size_bytes: v.ssa_var.size, - evidence, - } - }) - .collect(); - - if input.ctx.semantic_metadata_enabled { + let pointer_arg_slots = if input.ctx.semantic_metadata_enabled { let reg_type_hints = collect_register_type_hints(input.blocks.as_slice(), input.ctx.disasm); let recovered_vars = recover_vars_from_ssa(&pattern_ssa_blocks, input.ctx.arch, ®_type_hints, true); - let pointer_arg_slots = collect_pointer_arg_slots(&recovered_vars); - merge_pointer_slot_evidence(&mut inferred_params, &pointer_arg_slots); - } - - for param in &mut inferred_params { - param.ty = crate::resolve_evidence_driven_type( - param.ty.clone(), - param.size_bytes, - env.ptr_bits, - ¶m.evidence, - ); - } - - inferred_params.sort_by(|a, b| { - a.arg_index - .cmp(&b.arg_index) - .then_with(|| a.name.cmp(&b.name)) - }); - let mut used_param_names = HashSet::new(); - let params: Vec = inferred_params - .iter() - .enumerate() - .map(|(idx, p)| { - let fallback_idx = if p.arg_index == usize::MAX { - idx - } else { - p.arg_index - }; - InferredParamJson { - name: crate::normalize_inferred_param_name( - &p.name, - fallback_idx, - &mut used_param_names, - ), - param_type: crate::materialize_signature_ctype(p.ty.clone(), env.ptr_bits) - .to_string(), - } - }) - .collect(); + collect_pointer_arg_slots(&recovered_vars) + } else { + std::collections::BTreeSet::new() + }; + let recovered_params = recovered_signature_params_from_var_recovery(var_recovery.parameters()); - let (ret_type, ret_evidence) = crate::infer_signature_return_type( - &analysis.ssa_func, - &type_inference, + Some(r2types::infer_signature_from_prepared_ssa( + &input.function_name, + &env.arch_name, env.ptr_bits, - &evidence_ctx, - ); - let ret_type = crate::materialize_signature_ctype(ret_type, env.ptr_bits); - let ret_type_str = ret_type.to_string(); - - let input_counts = crate::collect_version0_input_regs(&analysis.ssa_func); - let (callconv, callconv_confidence) = - crate::compute_callconv_inference(&env.arch_name, &input_counts); - let confidence = - crate::compute_signature_confidence(&inferred_params, &ret_type, &ret_evidence); - - let signature = crate::format_afs_signature(&input.function_name, &ret_type_str, ¶ms); - Some(InferredSignatureCcJson { - function_name: input.function_name.clone(), - signature, - ret_type: ret_type_str, - params, - callconv, - arch: env.arch_name, - confidence, - callconv_confidence, - }) + &analysis.ssa_func, + &pattern_ssa_blocks, + &recovered_params, + &pointer_arg_slots, + )) } #[allow(dead_code)] pub(crate) fn infer_signature_cc_inner( input: &FunctionInput<'_>, -) -> Option { +) -> Option { let analysis = build_function_analysis(input)?; infer_signature_cc_from_analysis(input, &analysis) } @@ -762,11 +654,12 @@ fn signature_strength(signature: &r2types::FunctionSignatureSpec) -> u8 { } } -pub(crate) fn build_function_analysis_artifact_from_analysis( +pub(crate) fn build_function_analysis_artifact_from_analysis_with_semantic_artifact( input: &FunctionInput<'_>, analysis: FunctionAnalysis, external_context_json: &str, interproc_summary_set: Option, + semantic_artifact: r2sym::SemanticArtifact, ) -> Option { let ptr_bits = input .ctx @@ -774,26 +667,38 @@ pub(crate) fn build_function_analysis_artifact_from_analysis( .as_ref() .map(|arch| effective_ptr_bits(arch)) .unwrap_or(64); - let signature_cc = infer_signature_cc_from_analysis(input, &analysis)?; + let signature = infer_signature_cc_from_analysis(input, &analysis)?; let parsed_context = r2types::parse_external_context_json(external_context_json, ptr_bits); + let assumption_applied_analysis = if parsed_context.assumptions.is_empty() { + analysis.clone() + } else { + FunctionAnalysis { + ssa_func: analysis + .ssa_func + .with_assumptions(&parsed_context.assumptions), + pattern_ssa_func: analysis + .pattern_ssa_func + .with_assumptions(&parsed_context.assumptions), + } + }; - let pattern_ssa_blocks = analysis.pattern_ssa_func.local_ssa_blocks(); - let mut diagnostics = crate::TypeWritebackDiagnosticsJson::default(); - let raw_structs = crate::infer_structs_from_ssa( + let pattern_ssa_blocks = assumption_applied_analysis + .pattern_ssa_func + .local_ssa_blocks(); + let decompiler_env = build_decompiler_env(&input.ctx); + let decompiler_cfg = decompiler_env.cfg; + let mut diagnostics = r2types::TypeWritebackDiagnostics::default(); + let local_structs = r2types::infer_local_struct_artifacts_from_ssa( &pattern_ssa_blocks, - input.ctx.arch, - ptr_bits, - &mut diagnostics, - ); - let semantic_structs = crate::infer_structs_from_semantic_accesses( - &analysis.pattern_ssa_func, - &build_decompiler_env(&input.ctx).cfg, + Some(decompiler_env.arch_name.as_str()), ptr_bits, &mut diagnostics, ); - let (struct_decls, slot_type_overrides, slot_field_profiles) = - crate::merge_struct_inference_artifacts(raw_structs, semantic_structs); - + let local_field_accesses = + local_field_accesses_to_writeback(r2dec::infer_local_struct_field_accesses( + &assumption_applied_analysis.pattern_ssa_func, + &decompiler_cfg, + )); let reg_type_hints = if input.ctx.semantic_metadata_enabled { collect_register_type_hints(input.blocks.as_slice(), input.ctx.disasm) } else { @@ -806,30 +711,77 @@ pub(crate) fn build_function_analysis_artifact_from_analysis( input.ctx.semantic_metadata_enabled, ); let recovered_vars = vars.iter().map(var_prot_to_writeback).collect::>(); - let writeback = r2types::build_type_writeback_analysis(r2types::TypeWritebackAnalysisInput { - function_name: &input.function_name, - ptr_bits, - inferred_signature: signature_cc_to_writeback(&signature_cc), - recovered_vars: &recovered_vars, - ssa_blocks: &pattern_ssa_blocks, - parsed_context, - local_structs: local_struct_artifacts_to_writeback( - struct_decls, - slot_type_overrides, - slot_field_profiles, - ), - interproc_summary_set: interproc_summary_set.clone(), - diagnostics: writeback_diagnostics_from_plugin(diagnostics), - }); + let writeback = r2types::build_type_writeback_analysis_with_semantics( + r2types::TypeWritebackAnalysisInput { + function_name: &input.function_name, + ptr_bits, + inferred_signature: signature.clone(), + recovered_vars: &recovered_vars, + ssa_blocks: &pattern_ssa_blocks, + parsed_context, + local_structs, + interproc_summary_set: interproc_summary_set.clone(), + diagnostics, + }, + r2types::TypeWritebackSemanticInputs { + artifact: &semantic_artifact, + local_field_accesses: &local_field_accesses, + }, + ); + let mut function_facts = writeback.function_facts; + function_facts.assumption_usage = + merged_assumption_usage(&assumption_applied_analysis.ssa_func, &function_facts); Some(FunctionAnalysisArtifact { - ssa_func: analysis.ssa_func, - pattern_ssa_func: analysis.pattern_ssa_func, - type_facts: writeback.type_facts, + ssa_func: assumption_applied_analysis.ssa_func, + pattern_ssa_func: assumption_applied_analysis.pattern_ssa_func, + function_facts, writeback_plan: writeback.plan, - interproc_summary_set, }) } +pub(crate) fn build_function_analysis_artifact_from_analysis( + input: &FunctionInput<'_>, + analysis: FunctionAnalysis, + external_context_json: &str, + interproc_summary_set: Option, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + let parsed_context = r2types::parse_external_context_json( + external_context_json, + input + .ctx + .arch + .as_ref() + .map(|arch| effective_ptr_bits(arch)) + .unwrap_or(64), + ); + let analysis = if parsed_context.assumptions.is_empty() { + analysis + } else { + FunctionAnalysis { + ssa_func: analysis + .ssa_func + .with_assumptions(&parsed_context.assumptions), + pattern_ssa_func: analysis + .pattern_ssa_func + .with_assumptions(&parsed_context.assumptions), + } + }; + let semantic_artifact = r2sym::compile_semantic_artifact_default_with_scope( + &z3::Context::thread_local(), + &analysis.ssa_func, + symbolic_scope, + input.ctx.arch, + ); + build_function_analysis_artifact_from_analysis_with_semantic_artifact( + input, + analysis, + external_context_json, + interproc_summary_set, + semantic_artifact, + ) +} + #[allow(dead_code)] pub(crate) fn build_function_analysis_artifact( input: &FunctionInput<'_>, @@ -837,13 +789,35 @@ pub(crate) fn build_function_analysis_artifact( interproc_scope_json: &str, interproc_max_iterations: usize, ) -> Option { - let cache_key = - function_artifact_cache_key(input, external_context_json, interproc_scope_json)?; - if let Some(cached) = artifact_cache() - .read() - .expect("plugin cache read lock poisoned") - .get(&cache_key) - .cloned() + build_function_analysis_artifact_with_scope( + input, + external_context_json, + interproc_scope_json, + interproc_max_iterations, + None, + ) +} + +pub(crate) fn build_function_analysis_artifact_with_scope( + input: &FunctionInput<'_>, + external_context_json: &str, + interproc_scope_json: &str, + interproc_max_iterations: usize, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + let cache_key = function_artifact_cache_key( + input, + external_context_json, + interproc_scope_json, + interproc_max_iterations, + symbolic_scope, + ); + if let Some(cache_key) = cache_key.as_ref() + && let Some(cached) = artifact_cache() + .read() + .expect("plugin cache read lock poisoned") + .get(cache_key) + .cloned() { return Some(rename_function_analysis_artifact( (*cached).clone(), @@ -852,30 +826,43 @@ pub(crate) fn build_function_analysis_artifact( } let analysis = build_function_analysis(input)?; - let interproc_summary_set = build_interproc_summary_set( + let interproc_summary_set = build_interproc_summary_set_with_scope( input, &analysis, interproc_scope_json, interproc_max_iterations, + symbolic_scope, ); let artifact = build_function_analysis_artifact_from_analysis( input, analysis, external_context_json, Some(interproc_summary_set), + symbolic_scope, )?; - cache_insert_bounded(artifact_cache(), cache_key, Arc::new(artifact.clone())); + if let Some(cache_key) = cache_key { + cache_insert_bounded(artifact_cache(), cache_key, Arc::new(artifact.clone())); + } Some(rename_function_analysis_artifact( artifact, &input.function_name, )) } -pub(crate) fn get_cached_function_analysis_artifact( +fn get_cached_function_analysis_artifact_with_interproc_scope( input: &FunctionInput<'_>, external_context_json: &str, + interproc_scope_json: &str, + interproc_max_iterations: usize, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> Option { - let cache_key = function_artifact_cache_key(input, external_context_json, "{}")?; + let cache_key = function_artifact_cache_key( + input, + external_context_json, + interproc_scope_json, + interproc_max_iterations, + symbolic_scope, + )?; artifact_cache() .read() .expect("plugin cache read lock poisoned") @@ -886,31 +873,112 @@ pub(crate) fn get_cached_function_analysis_artifact( }) } +pub(crate) fn get_cached_function_analysis_artifact_with_scope( + input: &FunctionInput<'_>, + external_context_json: &str, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + get_cached_function_analysis_artifact_with_interproc_scope( + input, + external_context_json, + "{}", + 1, + symbolic_scope, + ) +} + +pub(crate) fn get_cached_function_analysis_artifact_with_scope_and_interproc( + input: &FunctionInput<'_>, + external_context_json: &str, + interproc_scope_json: &str, + interproc_max_iterations: usize, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + get_cached_function_analysis_artifact_with_interproc_scope( + input, + external_context_json, + interproc_scope_json, + interproc_max_iterations, + symbolic_scope, + ) +} + pub(crate) fn alias_cached_function_analysis_artifact( input: &FunctionInput<'_>, - source_external_context_json: &str, - target_external_context_json: &str, + _source_external_context_json: &str, + _target_external_context_json: &str, ) -> bool { - let Some(source_key) = function_artifact_cache_key(input, source_external_context_json, "{}") - else { - return false; - }; - let Some(target_key) = function_artifact_cache_key(input, target_external_context_json, "{}") - else { - return false; - }; - let Some(cached) = artifact_cache() - .read() - .expect("plugin cache read lock poisoned") - .get(&source_key) - .cloned() - else { + let Some(analysis_key) = function_analysis_cache_key_parts( + &input.function_name, + input.ctx.arch, + input.blocks.as_slice(), + ) else { return false; }; - cache_insert_bounded(artifact_cache(), target_key, cached); - true + let function_name_hash = hash_string_payload(&input.function_name); + let mut cache = artifact_cache() + .write() + .expect("plugin cache write lock poisoned"); + let before = cache.len(); + cache.retain(|key, _| { + key.analysis != analysis_key || key.function_name_hash != function_name_hash + }); + cache.len() != before +} + +pub(crate) fn function_root_interproc_summary_with_scope( + input: &FunctionInput<'_>, + external_context_json: &str, + interproc_scope_json: &str, + interproc_max_iterations: usize, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + get_cached_function_analysis_artifact_with_interproc_scope( + input, + external_context_json, + interproc_scope_json, + interproc_max_iterations, + symbolic_scope, + ) + .or_else(|| { + build_function_analysis_artifact_with_scope( + input, + external_context_json, + interproc_scope_json, + interproc_max_iterations, + symbolic_scope, + ) + }) + .and_then(|artifact| { + artifact + .function_facts + .interproc_summary_set() + .and_then(|summary_set| { + summary_set + .root + .and_then(|root| summary_set.summaries.get(&root).cloned()) + }) + }) +} + +pub(crate) fn function_root_interproc_summary_json_with_scope( + input: &FunctionInput<'_>, + external_context_json: &str, + interproc_scope_json: &str, + interproc_max_iterations: usize, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + let summary = function_root_interproc_summary_with_scope( + input, + external_context_json, + interproc_scope_json, + interproc_max_iterations, + symbolic_scope, + )?; + serde_json::to_string(&summary).ok() } +#[allow(dead_code)] #[allow(clippy::too_many_arguments)] pub(crate) fn build_detached_function_analysis_artifact( blocks: &[R2ILBlock], @@ -921,19 +989,71 @@ pub(crate) fn build_detached_function_analysis_artifact( reg_type_hints: &std::collections::HashMap, external_context_json: &str, ) -> Option { - let cache_key = function_artifact_cache_key_parts( + build_detached_function_analysis_artifact_with_scope_and_semantics( + blocks, function_name, arch, - blocks, + ptr_bits, semantic_metadata_enabled, + reg_type_hints, external_context_json, - "{}", - )?; - if let Some(cached) = artifact_cache() - .read() - .expect("plugin cache read lock poisoned") - .get(&cache_key) - .cloned() + None, + None, + ) +} + +#[allow(dead_code, clippy::too_many_arguments)] +pub(crate) fn build_detached_function_analysis_artifact_with_scope( + blocks: &[R2ILBlock], + function_name: &str, + arch: Option<&ArchSpec>, + ptr_bits: u32, + semantic_metadata_enabled: bool, + reg_type_hints: &std::collections::HashMap, + external_context_json: &str, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + build_detached_function_analysis_artifact_with_scope_and_semantics( + blocks, + function_name, + arch, + ptr_bits, + semantic_metadata_enabled, + reg_type_hints, + external_context_json, + symbolic_scope, + None, + ) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn build_detached_function_analysis_artifact_with_scope_and_semantics( + blocks: &[R2ILBlock], + function_name: &str, + arch: Option<&ArchSpec>, + ptr_bits: u32, + semantic_metadata_enabled: bool, + reg_type_hints: &std::collections::HashMap, + external_context_json: &str, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, + precomputed_semantic_artifact: Option, +) -> Option { + let cache_key = function_artifact_cache_key_parts( + function_name, + arch, + blocks, + semantic_metadata_enabled, + external_context_json, + "{}", + 1, + symbolic_scope, + ); + if let Some(cache_key) = cache_key.as_ref() + && let Some(cached) = artifact_cache() + .read() + .expect("plugin cache read lock poisoned") + .get(cache_key) + .cloned() { return Some(rename_function_analysis_artifact( (*cached).clone(), @@ -944,128 +1064,45 @@ pub(crate) fn build_detached_function_analysis_artifact( let analysis = build_function_analysis_from_parts(function_name, blocks, arch)?; let pattern_ssa_blocks = analysis.pattern_ssa_func.local_ssa_blocks(); - let arch_name = normalize_sig_arch_name(arch).unwrap_or_else(|| "unknown".to_string()); - let cfg = decompiler_config_for_arch_name(&arch_name, ptr_bits); - let evidence_ctx = collect_signature_type_evidence_context(&pattern_ssa_blocks); + let (arch_name, _, cfg) = r2dec::DecompilerConfig::for_arch(arch); let mut var_recovery = r2dec::VariableRecovery::new(&cfg.sp_name, &cfg.fp_name, cfg.ptr_size); var_recovery.recover(&analysis.ssa_func); - let mut type_inference = r2types::TypeInference::new(cfg.ptr_size); - type_inference.set_prepared_ssa(&analysis.ssa_func); - type_inference.infer_function(&analysis.ssa_func); - - let mut inferred_params: Vec = var_recovery - .parameters() - .into_iter() - .map(|v| { - let initial_ty = type_like_to_ctype(&type_inference.get_type(&v.ssa_var)); - let mut evidence = - crate::collect_type_evidence_for_var(&evidence_ctx, &v.ssa_var, &initial_ty); - if matches!(initial_ty, r2dec::CType::Void | r2dec::CType::Unknown) { - crate::merge_initial_type_evidence( - &type_like_to_ctype(&type_inference.type_from_size(v.ssa_var.size)), - &mut evidence, - ); - } - let ty = crate::resolve_evidence_driven_type( - initial_ty, - v.ssa_var.size, - ptr_bits, - &evidence, - ); - let arg_index = v - .name - .strip_prefix("arg") - .and_then(|n| n.parse::().ok()) - .unwrap_or(usize::MAX); - InferredParam { - name: v.name.clone(), - ty, - arg_index, - size_bytes: v.ssa_var.size, - evidence, - } - }) - .collect(); - - if semantic_metadata_enabled { + let pointer_arg_slots = if semantic_metadata_enabled { let recovered_vars = recover_vars_from_ssa(&pattern_ssa_blocks, arch, reg_type_hints, true); - let pointer_arg_slots = collect_pointer_arg_slots(&recovered_vars); - merge_pointer_slot_evidence(&mut inferred_params, &pointer_arg_slots); - } - - for param in &mut inferred_params { - param.ty = crate::resolve_evidence_driven_type( - param.ty.clone(), - param.size_bytes, - ptr_bits, - ¶m.evidence, - ); - } - - inferred_params.sort_by(|a, b| { - a.arg_index - .cmp(&b.arg_index) - .then_with(|| a.name.cmp(&b.name)) - }); - let mut used_param_names = HashSet::new(); - let params: Vec = inferred_params - .iter() - .enumerate() - .map(|(idx, p)| { - let fallback_idx = if p.arg_index == usize::MAX { - idx - } else { - p.arg_index - }; - InferredParamJson { - name: crate::normalize_inferred_param_name( - &p.name, - fallback_idx, - &mut used_param_names, - ), - param_type: crate::materialize_signature_ctype(p.ty.clone(), ptr_bits).to_string(), - } - }) - .collect(); - - let (ret_type, ret_evidence) = crate::infer_signature_return_type( - &analysis.ssa_func, - &type_inference, + collect_pointer_arg_slots(&recovered_vars) + } else { + std::collections::BTreeSet::new() + }; + let recovered_params = recovered_signature_params_from_var_recovery(var_recovery.parameters()); + let signature = r2types::infer_signature_from_prepared_ssa( + function_name, + &arch_name, ptr_bits, - &evidence_ctx, + &analysis.ssa_func, + &pattern_ssa_blocks, + &recovered_params, + &pointer_arg_slots, ); - let ret_type = crate::materialize_signature_ctype(ret_type, ptr_bits); - let signature_cc = InferredSignatureCcJson { - function_name: function_name.to_string(), - signature: crate::format_afs_signature(function_name, &ret_type.to_string(), ¶ms), - ret_type: ret_type.to_string(), - params, - callconv: crate::compute_callconv_inference( - &arch_name, - &crate::collect_version0_input_regs(&analysis.ssa_func), - ) - .0, - arch: arch_name.clone(), - confidence: crate::compute_signature_confidence(&inferred_params, &ret_type, &ret_evidence), - callconv_confidence: crate::compute_callconv_inference( - &arch_name, - &crate::collect_version0_input_regs(&analysis.ssa_func), - ) - .1, - }; let parsed_context = r2types::parse_external_context_json(external_context_json, ptr_bits); - let mut diagnostics = crate::TypeWritebackDiagnosticsJson::default(); - let raw_structs = - crate::infer_structs_from_ssa(&pattern_ssa_blocks, arch, ptr_bits, &mut diagnostics); - let semantic_structs = crate::infer_structs_from_semantic_accesses( - &analysis.pattern_ssa_func, - &cfg, + let mut diagnostics = r2types::TypeWritebackDiagnostics::default(); + let local_structs = r2types::infer_local_struct_artifacts_from_ssa( + &pattern_ssa_blocks, + Some(arch_name.as_str()), ptr_bits, &mut diagnostics, ); - let (struct_decls, slot_type_overrides, slot_field_profiles) = - crate::merge_struct_inference_artifacts(raw_structs, semantic_structs); + let local_field_accesses = local_field_accesses_to_writeback( + r2dec::infer_local_struct_field_accesses(&analysis.pattern_ssa_func, &cfg), + ); + let semantic_artifact = precomputed_semantic_artifact.unwrap_or_else(|| { + r2sym::compile_semantic_artifact_default_with_scope( + &z3::Context::thread_local(), + &analysis.ssa_func, + symbolic_scope, + arch, + ) + }); let vars = recover_vars_from_ssa( &pattern_ssa_blocks, arch, @@ -1073,44 +1110,35 @@ pub(crate) fn build_detached_function_analysis_artifact( semantic_metadata_enabled, ); let recovered_vars = vars.iter().map(var_prot_to_writeback).collect::>(); - let writeback = r2types::build_type_writeback_analysis(r2types::TypeWritebackAnalysisInput { - function_name, - ptr_bits, - inferred_signature: signature_cc_to_writeback(&signature_cc), - recovered_vars: &recovered_vars, - ssa_blocks: &pattern_ssa_blocks, - parsed_context, - local_structs: local_struct_artifacts_to_writeback( - struct_decls, - slot_type_overrides, - slot_field_profiles, - ), - interproc_summary_set: None, - diagnostics: writeback_diagnostics_from_plugin(diagnostics), - }); + let writeback = r2types::build_type_writeback_analysis_with_semantics( + r2types::TypeWritebackAnalysisInput { + function_name, + ptr_bits, + inferred_signature: signature.clone(), + recovered_vars: &recovered_vars, + ssa_blocks: &pattern_ssa_blocks, + parsed_context, + local_structs, + interproc_summary_set: None, + diagnostics, + }, + r2types::TypeWritebackSemanticInputs { + artifact: &semantic_artifact, + local_field_accesses: &local_field_accesses, + }, + ); let artifact = FunctionAnalysisArtifact { ssa_func: analysis.ssa_func, pattern_ssa_func: analysis.pattern_ssa_func, - type_facts: writeback.type_facts, + function_facts: writeback.function_facts, writeback_plan: writeback.plan, - interproc_summary_set: None, }; - cache_insert_bounded(artifact_cache(), cache_key, Arc::new(artifact.clone())); + if let Some(cache_key) = cache_key { + cache_insert_bounded(artifact_cache(), cache_key, Arc::new(artifact.clone())); + } Some(rename_function_analysis_artifact(artifact, function_name)) } -#[derive(serde::Serialize)] -pub(crate) struct VarProt { - pub(crate) name: String, - pub(crate) kind: String, - pub(crate) delta: i64, - #[serde(rename = "type")] - pub(crate) var_type: String, - pub(crate) isarg: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) reg: Option, -} - #[derive(Debug, serde::Serialize)] pub(crate) struct DataRef { pub(crate) from: u64, @@ -1119,43 +1147,12 @@ pub(crate) struct DataRef { pub(crate) ref_type: String, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) enum TypeHintRank { - Integer = 1, - Float = 2, - Pointer = 3, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct TypeHint { - pub(crate) rank: TypeHintRank, - pub(crate) ty: String, -} - -impl TypeHint { - pub(crate) fn pointer() -> Self { - Self { - rank: TypeHintRank::Pointer, - ty: "void *".to_string(), - } - } -} - -fn incoming_hint_should_replace(current: &TypeHint, incoming: &TypeHint) -> bool { - incoming.rank > current.rank || (incoming.rank == current.rank && incoming.ty < current.ty) -} - pub(crate) fn merge_type_hint( hints: &mut std::collections::HashMap, key: String, incoming: TypeHint, ) { - match hints.get(&key) { - Some(current) if !incoming_hint_should_replace(current, &incoming) => {} - _ => { - hints.insert(key, incoming); - } - } + r2types::merge_type_hint(hints, key, incoming); } fn size_to_signed_int_type(size: u32) -> String { @@ -1164,1074 +1161,129 @@ fn size_to_signed_int_type(size: u32) -> String { 2 => "int16_t".to_string(), 4 => "int32_t".to_string(), 8 => "int64_t".to_string(), - _ => format!("int{}_t", size.saturating_mul(8)), - } -} - -fn size_to_unsigned_int_type(size: u32) -> String { - match size { - 1 => "uint8_t".to_string(), - 2 => "uint16_t".to_string(), - 4 => "uint32_t".to_string(), - 8 => "uint64_t".to_string(), - _ => format!("uint{}_t", size.saturating_mul(8)), - } -} - -fn scalar_kind_to_type(kind: r2il::ScalarKind, size: u32) -> Option { - match kind { - r2il::ScalarKind::Bool => Some(TypeHint { - rank: TypeHintRank::Integer, - ty: "bool".to_string(), - }), - r2il::ScalarKind::SignedInt => Some(TypeHint { - rank: TypeHintRank::Integer, - ty: size_to_signed_int_type(size), - }), - r2il::ScalarKind::UnsignedInt => Some(TypeHint { - rank: TypeHintRank::Integer, - ty: size_to_unsigned_int_type(size), - }), - r2il::ScalarKind::Float => { - let ty = match size { - 4 => "float".to_string(), - 8 => "double".to_string(), - 16 => "long double".to_string(), - _ => "float".to_string(), - }; - Some(TypeHint { - rank: TypeHintRank::Float, - ty, - }) - } - r2il::ScalarKind::Bitvector | r2il::ScalarKind::Unknown => None, - } -} - -fn metadata_type_hint(vn: &r2il::Varnode) -> Option { - let meta = vn.meta.as_ref()?; - - if let Some(pointer_hint) = meta.pointer_hint - && !matches!(pointer_hint, r2il::PointerHint::Unknown) - { - return Some(TypeHint::pointer()); - } - - let scalar_kind = meta.scalar_kind?; - scalar_kind_to_type(scalar_kind, vn.size) -} - -pub(crate) fn collect_register_type_hints( - r2il_blocks: &[R2ILBlock], - disasm: &Disassembler, -) -> std::collections::HashMap { - let mut hints: std::collections::HashMap = std::collections::HashMap::new(); - - for block in r2il_blocks { - for op in &block.ops { - for vn in crate::op_all_varnodes(op) { - if !vn.is_register() { - continue; - } - let Some(hint) = metadata_type_hint(vn) else { - continue; - }; - let Some(name) = disasm.register_name(vn) else { - continue; - }; - - let key = name.to_ascii_lowercase(); - merge_type_hint(&mut hints, key, hint); - } - } - } - - hints -} - -pub(crate) const X86_ARG_REGS: &[(&str, &[&str])] = &[ - ("rdi", &["rdi", "edi", "di", "dil"]), - ("rsi", &["rsi", "esi", "si", "sil"]), - ("rdx", &["rdx", "edx", "dx", "dl", "dh"]), - ("rcx", &["rcx", "ecx", "cx", "cl", "ch"]), - ("r8", &["r8", "r8d", "r8w", "r8b"]), - ("r9", &["r9", "r9d", "r9w", "r9b"]), -]; -const RISCV_ARG_REGS: &[(&str, &[&str])] = &[ - ("a0", &["a0", "x10"]), - ("a1", &["a1", "x11"]), - ("a2", &["a2", "x12"]), - ("a3", &["a3", "x13"]), - ("a4", &["a4", "x14"]), - ("a5", &["a5", "x15"]), - ("a6", &["a6", "x16"]), - ("a7", &["a7", "x17"]), -]; -const ARM64_ARG_REGS: &[(&str, &[&str])] = &[ - ("x0", &["x0", "w0"]), - ("x1", &["x1", "w1"]), - ("x2", &["x2", "w2"]), - ("x3", &["x3", "w3"]), - ("x4", &["x4", "w4"]), - ("x5", &["x5", "w5"]), - ("x6", &["x6", "w6"]), - ("x7", &["x7", "w7"]), -]; -const ARM32_ARG_REGS: &[(&str, &[&str])] = &[ - ("r0", &["r0"]), - ("r1", &["r1"]), - ("r2", &["r2"]), - ("r3", &["r3"]), -]; -const MIPS_ARG_REGS: &[(&str, &[&str])] = &[ - ("a0", &["a0", "$a0", "r4"]), - ("a1", &["a1", "$a1", "r5"]), - ("a2", &["a2", "$a2", "r6"]), - ("a3", &["a3", "$a3", "r7"]), -]; -const X86_STACK_BASES: &[&str] = &["rbp", "rsp", "ebp", "esp"]; -pub(crate) const X86_FRAME_BASES: &[&str] = &["rbp", "ebp"]; -const RISCV_STACK_BASES: &[&str] = &["sp", "s0", "fp", "x2", "x8"]; -const RISCV_FRAME_BASES: &[&str] = &["s0", "fp", "x8"]; -const ARM64_STACK_BASES: &[&str] = &["sp", "x29", "fp"]; -const ARM64_FRAME_BASES: &[&str] = &["x29", "fp"]; -const ARM32_STACK_BASES: &[&str] = &["sp", "r11", "fp"]; -const ARM32_FRAME_BASES: &[&str] = &["r11", "fp"]; -const MIPS_STACK_BASES: &[&str] = &["sp", "$sp", "fp", "$fp", "s8", "$s8"]; -const MIPS_FRAME_BASES: &[&str] = &["fp", "$fp", "s8", "$s8"]; -const GENERIC_STACK_BASES: &[&str] = &["sp", "fp", "bp", "s0", "x2", "x8", "rbp", "rsp"]; -const GENERIC_FRAME_BASES: &[&str] = &["fp", "bp", "s0", "x8", "rbp"]; - -type ArgAliasMap = &'static [(&'static str, &'static [&'static str])]; -type BaseRegList = &'static [&'static str]; - -pub(crate) fn recover_vars_arch_profile( - arch: Option<&ArchSpec>, -) -> (ArgAliasMap, BaseRegList, BaseRegList) { - let Some(arch) = arch else { - return (&[], GENERIC_STACK_BASES, GENERIC_FRAME_BASES); - }; - - let arch_name = arch.name.to_ascii_lowercase(); - if arch_name.contains("x86") { - return (X86_ARG_REGS, X86_STACK_BASES, X86_FRAME_BASES); - } - if arch_name.contains("aarch64") || arch_name.contains("arm64") { - return (ARM64_ARG_REGS, ARM64_STACK_BASES, ARM64_FRAME_BASES); - } - if arch_name == "arm" || arch_name.starts_with("armv") { - return (ARM32_ARG_REGS, ARM32_STACK_BASES, ARM32_FRAME_BASES); - } - if arch_name.contains("riscv") || arch_name.starts_with("rv") { - return (RISCV_ARG_REGS, RISCV_STACK_BASES, RISCV_FRAME_BASES); - } - if arch_name.contains("mips") { - return (MIPS_ARG_REGS, MIPS_STACK_BASES, MIPS_FRAME_BASES); - } - - (&[], GENERIC_STACK_BASES, GENERIC_FRAME_BASES) -} - -pub(crate) fn ssa_var_key(var: &r2ssa::SSAVar) -> String { - format!("{}_{}", var.name.to_ascii_lowercase(), var.version) -} - -pub(crate) fn ssa_var_block_key(block_addr: u64, var: &r2ssa::SSAVar) -> String { - format!("{}@{block_addr:x}", ssa_var_key(var)) -} - -fn ssa_var_is_const(var: &r2ssa::SSAVar) -> bool { - parse_const_value(&var.name).is_some() -} - -fn ssa_var_is_register_like(name: &str) -> bool { - let lower = name.to_ascii_lowercase(); - !(lower.starts_with("tmp:") - || lower.starts_with("const:") - || lower.starts_with("ram:") - || lower.starts_with("space")) -} - -pub(crate) fn scalar_register_family_key(name: &str) -> String { - let lower = name.to_ascii_lowercase(); - - if let Some(idx) = lower.strip_prefix('x').or_else(|| lower.strip_prefix('w')) - && !idx.is_empty() - && idx.chars().all(|ch| ch.is_ascii_digit()) - { - return format!("aarch64:gpr:{idx}"); - } - - match lower.as_str() { - "fp" => "aarch64:gpr:29".to_string(), - "lr" => "aarch64:gpr:30".to_string(), - "sp" | "wsp" => "aarch64:sp".to_string(), - "xzr" | "wzr" => "aarch64:zr".to_string(), - _ => lower, - } -} - -fn collect_register_version_keys( - ssa_blocks: &[r2ssa::SSABlock], -) -> std::collections::HashMap> { - use std::collections::HashMap; - - let mut reg_versions: HashMap> = HashMap::new(); - for block in ssa_blocks { - for op in &block.ops { - let mut collect_var = |var: &r2ssa::SSAVar| { - if !ssa_var_is_register_like(&var.name) { - return; - } - let reg_name = scalar_register_family_key(&var.name); - reg_versions - .entry(reg_name) - .or_default() - .push(ssa_var_key(var)); - }; - if let Some(dst) = op.dst() { - collect_var(dst); - } - op.for_each_source(&mut collect_var); - } - } - for keys in reg_versions.values_mut() { - keys.sort(); - keys.dedup(); - } - reg_versions -} - -fn set_width_hint_prefer_narrower( - width_hints: &mut std::collections::HashMap, - key: String, - bits: u32, -) -> bool { - if bits == 0 { - return false; - } - match width_hints.get(&key).copied() { - Some(current) if current == bits => false, - Some(current) if current > 0 && current <= bits => false, - _ => { - width_hints.insert(key, bits); - true - } - } -} - -fn normalize_register_family_width_hints( - register_versions: &std::collections::HashMap>, - width_hints: &mut std::collections::HashMap, -) -> bool { - let mut changed = false; - for reg_keys in register_versions.values() { - let max_bits = reg_keys - .iter() - .filter_map(|key| width_hints.get(key).copied()) - .max() - .unwrap_or(0); - let min_bits = reg_keys - .iter() - .filter_map(|key| { - let current = width_hints.get(key).copied().unwrap_or(0); - let natural = register_key_natural_bits(key).unwrap_or(0); - let candidate = match (current, natural) { - (0, 0) => 0, - (0, natural) => natural, - (current, 0) => current, - (current, natural) => current.min(natural), - }; - (candidate > 0).then_some(candidate) - }) - .min() - .unwrap_or(0); - let preferred_bits = if min_bits > 0 && max_bits == 64 && min_bits <= 32 { - min_bits - } else { - max_bits - }; - for key in reg_keys { - changed |= set_width_hint_prefer_narrower(width_hints, key.clone(), preferred_bits); - } - } - changed -} - -fn register_key_natural_bits(key: &str) -> Option { - let reg = key.split('_').next()?; - if let Some(idx) = reg.strip_prefix('w') - && !idx.is_empty() - && idx.chars().all(|ch| ch.is_ascii_digit()) - { - return Some(32); - } - if let Some(idx) = reg.strip_prefix('x') - && !idx.is_empty() - && idx.chars().all(|ch| ch.is_ascii_digit()) - { - return Some(64); - } - match reg { - "wsp" | "wzr" => Some(32), - "sp" | "fp" | "lr" | "xzr" => Some(64), - _ => None, - } -} - -fn propagate_normalized_scalar_result_widths( - ssa_blocks: &[r2ssa::SSABlock], - width_hints: &mut std::collections::HashMap, -) -> bool { - let mut changed = false; - for block in ssa_blocks { - for op in &block.ops { - let (dst, source_bits) = match op { - r2ssa::SSAOp::Copy { dst, src } - | r2ssa::SSAOp::Cast { dst, src } - | r2ssa::SSAOp::New { dst, src } - | r2ssa::SSAOp::IntZExt { dst, src } - | r2ssa::SSAOp::IntSExt { dst, src } - | r2ssa::SSAOp::Subpiece { dst, src, .. } => { - let bits = width_hints.get(&ssa_var_key(src)).copied().unwrap_or(0); - (dst, bits) - } - r2ssa::SSAOp::IntAdd { dst, a, b } - | r2ssa::SSAOp::IntSub { dst, a, b } - | r2ssa::SSAOp::IntMult { dst, a, b } - | r2ssa::SSAOp::IntDiv { dst, a, b } - | r2ssa::SSAOp::IntSDiv { dst, a, b } - | r2ssa::SSAOp::IntRem { dst, a, b } - | r2ssa::SSAOp::IntSRem { dst, a, b } - | r2ssa::SSAOp::IntAnd { dst, a, b } - | r2ssa::SSAOp::IntOr { dst, a, b } - | r2ssa::SSAOp::IntXor { dst, a, b } - | r2ssa::SSAOp::IntLeft { dst, a, b } - | r2ssa::SSAOp::IntRight { dst, a, b } - | r2ssa::SSAOp::IntSRight { dst, a, b } => { - let bits = [a, b] - .into_iter() - .filter(|var| !ssa_var_is_const(var)) - .filter_map(|var| width_hints.get(&ssa_var_key(var)).copied()) - .filter(|bits| *bits > 0) - .max() - .unwrap_or(0); - (dst, bits) - } - _ => continue, - }; - changed |= set_width_hint_prefer_narrower(width_hints, ssa_var_key(dst), source_bits); - } - } - changed -} - -fn ssa_var_is_stack_base(var: &r2ssa::SSAVar) -> bool { - matches!( - var.name.to_ascii_lowercase().as_str(), - "rbp" | "rsp" | "ebp" | "esp" | "sp" | "fp" | "bp" | "s0" | "x2" | "x8" - ) -} - -fn infer_pointer_width_bytes(ssa_blocks: &[r2ssa::SSABlock]) -> u32 { - let mut width = 0u32; - for block in ssa_blocks { - for op in &block.ops { - if let Some(dst) = op.dst() - && ssa_var_is_stack_base(dst) - { - width = width.max(dst.size); - } - op.for_each_source(|src| { - if ssa_var_is_stack_base(src) { - width = width.max(src.size); - } - }); - } - } - if width == 0 { 8 } else { width } -} - -fn infer_index_like_var_keys(ssa_blocks: &[r2ssa::SSABlock]) -> std::collections::HashSet { - use std::collections::HashSet; - - let mut index_like: HashSet = HashSet::new(); - for block in ssa_blocks { - for op in &block.ops { - if let r2ssa::SSAOp::IntSExt { dst, src } | r2ssa::SSAOp::IntZExt { dst, src } = op - && src.size < dst.size - { - index_like.insert(ssa_var_key(dst)); - } - } - } - - let mut changed = true; - while changed { - changed = false; - for block in ssa_blocks { - for op in &block.ops { - match op { - r2ssa::SSAOp::Copy { dst, src } - | r2ssa::SSAOp::Cast { dst, src } - | r2ssa::SSAOp::New { dst, src } => { - if index_like.contains(&ssa_var_key(src)) { - changed |= index_like.insert(ssa_var_key(dst)); - } - } - r2ssa::SSAOp::IntMult { dst, a, b } => { - let a_key = ssa_var_key(a); - let b_key = ssa_var_key(b); - let a_is_scaled_const = ssa_var_is_const(a); - let b_is_scaled_const = ssa_var_is_const(b); - if (index_like.contains(&a_key) && ssa_var_is_const(b)) - || (index_like.contains(&b_key) && ssa_var_is_const(a)) - || (a_is_scaled_const && !b_is_scaled_const) - || (b_is_scaled_const && !a_is_scaled_const) - { - changed |= index_like.insert(ssa_var_key(dst)); - } - } - r2ssa::SSAOp::IntLeft { dst, a, b } => { - let shift_amount = parse_const_value(&b.name).unwrap_or(u64::MAX); - if (index_like.contains(&ssa_var_key(a)) && ssa_var_is_const(b)) - || shift_amount <= 6 - { - changed |= index_like.insert(ssa_var_key(dst)); - } - } - _ => {} - } - } - } - } - - index_like -} - -fn infer_pointer_var_keys_from_ssa( - ssa_blocks: &[r2ssa::SSABlock], -) -> std::collections::HashSet { - use std::collections::{HashMap, HashSet}; - - let mut pointer_vars: HashSet = HashSet::new(); - let register_versions = collect_register_version_keys(ssa_blocks); - let index_like_vars = infer_index_like_var_keys(ssa_blocks); - let pointer_width = infer_pointer_width_bytes(ssa_blocks); - let mut stack_addr_slots: HashMap = HashMap::new(); - let mut pointer_stack_slots: HashSet = HashSet::new(); - - for block in ssa_blocks { - for op in &block.ops { - match op { - r2ssa::SSAOp::IntAdd { dst, a, b } | r2ssa::SSAOp::IntSub { dst, a, b } => { - let a_is_stack = ssa_var_is_stack_base(a); - let b_is_stack = ssa_var_is_stack_base(b); - let a_const = parse_const_value(&a.name); - let b_const = parse_const_value(&b.name); - - if a_is_stack && b_const.is_some() { - let raw = b_const.unwrap_or(0); - let offset = if matches!(op, r2ssa::SSAOp::IntSub { .. }) { - -(raw as i64) - } else { - raw as i64 - }; - stack_addr_slots.insert( - ssa_var_block_key(block.addr, dst), - format!("{}:{offset}", a.name.to_ascii_lowercase()), - ); - } else if matches!(op, r2ssa::SSAOp::IntAdd { .. }) - && b_is_stack - && a_const.is_some() - { - let raw = a_const.unwrap_or(0); - stack_addr_slots.insert( - ssa_var_block_key(block.addr, dst), - format!("{}:{}", b.name.to_ascii_lowercase(), raw as i64), - ); - } - } - r2ssa::SSAOp::Load { addr, .. } - | r2ssa::SSAOp::Store { addr, .. } - | r2ssa::SSAOp::LoadLinked { addr, .. } - | r2ssa::SSAOp::StoreConditional { addr, .. } - | r2ssa::SSAOp::LoadGuarded { addr, .. } - | r2ssa::SSAOp::StoreGuarded { addr, .. } - | r2ssa::SSAOp::AtomicCAS { addr, .. } => { - pointer_vars.insert(ssa_var_key(addr)); - } - _ => {} - } - } - } - - let mut changed = true; - while changed { - changed = false; - for block in ssa_blocks { - for op in &block.ops { - match op { - r2ssa::SSAOp::Phi { dst, sources } => { - let dst_key = ssa_var_key(dst); - let dst_is_pointer = pointer_vars.contains(&dst_key); - let any_source_pointer = sources - .iter() - .any(|src| pointer_vars.contains(&ssa_var_key(src))); - - if any_source_pointer { - changed |= pointer_vars.insert(dst_key.clone()); - } - if dst_is_pointer { - for src in sources { - changed |= pointer_vars.insert(ssa_var_key(src)); - } - } - } - r2ssa::SSAOp::Copy { dst, src } - | r2ssa::SSAOp::Cast { dst, src } - | r2ssa::SSAOp::New { dst, src } => { - let dst_key = ssa_var_key(dst); - let src_key = ssa_var_key(src); - if pointer_vars.contains(&dst_key) { - changed |= pointer_vars.insert(src_key.clone()); - } - if pointer_vars.contains(&src_key) { - changed |= pointer_vars.insert(dst_key); - } - } - r2ssa::SSAOp::IntAdd { dst, a, b } | r2ssa::SSAOp::IntSub { dst, a, b } => { - let dst_key = ssa_var_key(dst); - let a_key = ssa_var_key(a); - let b_key = ssa_var_key(b); - let a_is_const = ssa_var_is_const(a); - let b_is_const = ssa_var_is_const(b); - let a_index_like = index_like_vars.contains(&a_key); - let b_index_like = index_like_vars.contains(&b_key); - - if pointer_vars.contains(&dst_key) { - if a_is_const && !b_is_const { - changed |= pointer_vars.insert(b_key.clone()); - } else if b_is_const && !a_is_const { - changed |= pointer_vars.insert(a_key.clone()); - } else if a_index_like && !b_index_like { - changed |= pointer_vars.insert(b_key.clone()); - } else if b_index_like && !a_index_like { - changed |= pointer_vars.insert(a_key.clone()); - } else if a_index_like && b_index_like { - let a_is_tmp = a.name.starts_with("tmp:"); - let b_is_tmp = b.name.starts_with("tmp:"); - if a_is_tmp && !b_is_tmp { - changed |= pointer_vars.insert(b_key.clone()); - } else if b_is_tmp && !a_is_tmp { - changed |= pointer_vars.insert(a_key.clone()); - } - } - } - - if pointer_vars.contains(&a_key) && b_is_const { - changed |= pointer_vars.insert(dst_key.clone()); - } - if pointer_vars.contains(&b_key) && a_is_const { - changed |= pointer_vars.insert(dst_key.clone()); - } - if pointer_vars.contains(&a_key) && index_like_vars.contains(&b_key) { - changed |= pointer_vars.insert(dst_key.clone()); - } - if pointer_vars.contains(&b_key) && index_like_vars.contains(&a_key) { - changed |= pointer_vars.insert(dst_key.clone()); - } - } - r2ssa::SSAOp::PtrAdd { dst, base, .. } - | r2ssa::SSAOp::PtrSub { dst, base, .. } => { - let dst_key = ssa_var_key(dst); - let base_key = ssa_var_key(base); - if pointer_vars.contains(&dst_key) { - changed |= pointer_vars.insert(base_key.clone()); - } - if pointer_vars.contains(&base_key) { - changed |= pointer_vars.insert(dst_key); - } - } - r2ssa::SSAOp::SegmentOp { dst, offset, .. } => { - let dst_key = ssa_var_key(dst); - let offset_key = ssa_var_key(offset); - if pointer_vars.contains(&dst_key) { - changed |= pointer_vars.insert(offset_key.clone()); - } - if pointer_vars.contains(&offset_key) { - changed |= pointer_vars.insert(dst_key); - } - } - r2ssa::SSAOp::Store { addr, val, .. } => { - if let Some(slot) = - stack_addr_slots.get(&ssa_var_block_key(block.addr, addr)) - { - let val_key = ssa_var_key(val); - if val.size >= pointer_width && pointer_vars.contains(&val_key) { - changed |= pointer_stack_slots.insert(slot.clone()); - } - if val.size >= pointer_width && pointer_stack_slots.contains(slot) { - changed |= pointer_vars.insert(val_key); - } - } - } - r2ssa::SSAOp::Load { dst, addr, .. } => { - if let Some(slot) = - stack_addr_slots.get(&ssa_var_block_key(block.addr, addr)) - { - let dst_key = ssa_var_key(dst); - if dst.size >= pointer_width && pointer_stack_slots.contains(slot) { - changed |= pointer_vars.insert(dst_key.clone()); - } - if dst.size >= pointer_width && pointer_vars.contains(&dst_key) { - changed |= pointer_stack_slots.insert(slot.clone()); - } - } - } - _ => {} - } - } - } - - for reg_keys in register_versions.values() { - if reg_keys.iter().any(|key| pointer_vars.contains(key)) { - for key in reg_keys { - changed |= pointer_vars.insert(key.clone()); - } - } - } - } - - pointer_vars -} - -fn merge_width_hint( - width_hints: &mut std::collections::HashMap, - var: &r2ssa::SSAVar, - bits: u32, -) { - let entry = width_hints.entry(ssa_var_key(var)).or_insert(0); - *entry = (*entry).max(bits.max(var.size.saturating_mul(8))); -} - -fn mark_scalar_var( - vars: &mut std::collections::HashSet, - width_hints: &mut std::collections::HashMap, - var: &r2ssa::SSAVar, -) { - vars.insert(ssa_var_key(var)); - merge_width_hint(width_hints, var, var.size.saturating_mul(8)); -} - -fn infer_scalar_var_evidence_from_ssa( - ssa_blocks: &[r2ssa::SSABlock], -) -> ( - std::collections::HashSet, - std::collections::HashSet, - std::collections::HashSet, - std::collections::HashMap, -) { - use std::collections::{HashMap, HashSet}; - - let register_versions = collect_register_version_keys(ssa_blocks); - let mut scalar_proven: HashSet = HashSet::new(); - let mut scalar_likely: HashSet = HashSet::new(); - let mut bool_like: HashSet = HashSet::new(); - let mut width_hints: HashMap = HashMap::new(); - - for block in ssa_blocks { - for op in &block.ops { - match op { - r2ssa::SSAOp::IntMult { a, b, .. } - | r2ssa::SSAOp::IntDiv { a, b, .. } - | r2ssa::SSAOp::IntSDiv { a, b, .. } - | r2ssa::SSAOp::IntRem { a, b, .. } - | r2ssa::SSAOp::IntSRem { a, b, .. } - | r2ssa::SSAOp::IntAnd { a, b, .. } - | r2ssa::SSAOp::IntOr { a, b, .. } - | r2ssa::SSAOp::IntXor { a, b, .. } - | r2ssa::SSAOp::IntLeft { a, b, .. } - | r2ssa::SSAOp::IntRight { a, b, .. } - | r2ssa::SSAOp::IntSRight { a, b, .. } - | r2ssa::SSAOp::IntCarry { a, b, .. } - | r2ssa::SSAOp::IntSCarry { a, b, .. } - | r2ssa::SSAOp::IntSBorrow { a, b, .. } => { - mark_scalar_var(&mut scalar_proven, &mut width_hints, a); - mark_scalar_var(&mut scalar_proven, &mut width_hints, b); - } - r2ssa::SSAOp::IntNegate { src, .. } - | r2ssa::SSAOp::IntNot { src, .. } - | r2ssa::SSAOp::PopCount { src, .. } - | r2ssa::SSAOp::Lzcount { src, .. } => { - mark_scalar_var(&mut scalar_proven, &mut width_hints, src); - } - r2ssa::SSAOp::PtrAdd { index, .. } | r2ssa::SSAOp::PtrSub { index, .. } => { - mark_scalar_var(&mut scalar_proven, &mut width_hints, index); - } - r2ssa::SSAOp::IntAdd { a, b, .. } | r2ssa::SSAOp::IntSub { a, b, .. } => { - mark_scalar_var(&mut scalar_likely, &mut width_hints, a); - mark_scalar_var(&mut scalar_likely, &mut width_hints, b); - } - r2ssa::SSAOp::BoolNot { dst, src } => { - mark_scalar_var(&mut scalar_likely, &mut width_hints, src); - bool_like.insert(ssa_var_key(src)); - bool_like.insert(ssa_var_key(dst)); - merge_width_hint(&mut width_hints, dst, 1); - } - r2ssa::SSAOp::CBranch { cond, .. } - | r2ssa::SSAOp::LoadGuarded { guard: cond, .. } - | r2ssa::SSAOp::StoreGuarded { guard: cond, .. } => { - mark_scalar_var(&mut scalar_likely, &mut width_hints, cond); - bool_like.insert(ssa_var_key(cond)); - merge_width_hint(&mut width_hints, cond, 1); - } - r2ssa::SSAOp::FloatNeg { src, .. } - | r2ssa::SSAOp::FloatAbs { src, .. } - | r2ssa::SSAOp::FloatSqrt { src, .. } - | r2ssa::SSAOp::Cast { src, .. } => { - mark_scalar_var(&mut scalar_likely, &mut width_hints, src); - } - r2ssa::SSAOp::IntEqual { dst, a, b } - | r2ssa::SSAOp::IntNotEqual { dst, a, b } - | r2ssa::SSAOp::IntLess { dst, a, b } - | r2ssa::SSAOp::IntSLess { dst, a, b } - | r2ssa::SSAOp::IntLessEqual { dst, a, b } - | r2ssa::SSAOp::IntSLessEqual { dst, a, b } - | r2ssa::SSAOp::FloatEqual { dst, a, b } - | r2ssa::SSAOp::FloatNotEqual { dst, a, b } - | r2ssa::SSAOp::FloatLess { dst, a, b } - | r2ssa::SSAOp::FloatLessEqual { dst, a, b } => { - mark_scalar_var(&mut scalar_likely, &mut width_hints, a); - mark_scalar_var(&mut scalar_likely, &mut width_hints, b); - bool_like.insert(ssa_var_key(dst)); - merge_width_hint(&mut width_hints, dst, 1); - } - r2ssa::SSAOp::BoolAnd { dst, a, b } - | r2ssa::SSAOp::BoolOr { dst, a, b } - | r2ssa::SSAOp::BoolXor { dst, a, b } => { - mark_scalar_var(&mut scalar_likely, &mut width_hints, a); - mark_scalar_var(&mut scalar_likely, &mut width_hints, b); - bool_like.insert(ssa_var_key(a)); - bool_like.insert(ssa_var_key(b)); - bool_like.insert(ssa_var_key(dst)); - merge_width_hint(&mut width_hints, dst, 1); - } - r2ssa::SSAOp::IntZExt { dst, src } | r2ssa::SSAOp::IntSExt { dst, src } => { - mark_scalar_var(&mut scalar_likely, &mut width_hints, src); - mark_scalar_var(&mut scalar_likely, &mut width_hints, dst); - merge_width_hint(&mut width_hints, dst, dst.size.saturating_mul(8)); - } - r2ssa::SSAOp::Subpiece { dst, src, .. } => { - mark_scalar_var(&mut scalar_likely, &mut width_hints, src); - mark_scalar_var(&mut scalar_likely, &mut width_hints, dst); - merge_width_hint(&mut width_hints, dst, dst.size.saturating_mul(8)); - } - _ => {} - } - } + _ => format!("int{}_t", size.saturating_mul(8)), } +} - let mut changed = true; - while changed { - changed = false; - for block in ssa_blocks { - for op in &block.ops { - match op { - r2ssa::SSAOp::IntAdd { dst, a, b } - | r2ssa::SSAOp::IntSub { dst, a, b } - | r2ssa::SSAOp::IntMult { dst, a, b } - | r2ssa::SSAOp::IntDiv { dst, a, b } - | r2ssa::SSAOp::IntSDiv { dst, a, b } - | r2ssa::SSAOp::IntRem { dst, a, b } - | r2ssa::SSAOp::IntSRem { dst, a, b } - | r2ssa::SSAOp::IntAnd { dst, a, b } - | r2ssa::SSAOp::IntOr { dst, a, b } - | r2ssa::SSAOp::IntXor { dst, a, b } - | r2ssa::SSAOp::IntLeft { dst, a, b } - | r2ssa::SSAOp::IntRight { dst, a, b } - | r2ssa::SSAOp::IntSRight { dst, a, b } => { - let dst_key = ssa_var_key(dst); - let a_key = ssa_var_key(a); - let b_key = ssa_var_key(b); - let any_proven = - scalar_proven.contains(&a_key) || scalar_proven.contains(&b_key); - let any_likely = scalar_likely.contains(&a_key) - || scalar_likely.contains(&b_key) - || any_proven; - if any_proven { - changed |= scalar_proven.insert(dst_key.clone()); - } - if any_likely { - changed |= scalar_likely.insert(dst_key.clone()); - } - - let operand_bits = [a, b] - .into_iter() - .filter(|var| !ssa_var_is_const(var)) - .filter_map(|var| width_hints.get(&ssa_var_key(var)).copied()) - .filter(|bits| *bits > 0) - .max() - .unwrap_or(0); - if operand_bits > 0 { - let entry = width_hints.entry(dst_key).or_insert(0); - if operand_bits > *entry { - *entry = operand_bits; - changed = true; - } - } - } - r2ssa::SSAOp::Phi { dst, sources } => { - let dst_key = ssa_var_key(dst); - let any_proven = sources - .iter() - .any(|src| scalar_proven.contains(&ssa_var_key(src))); - let any_likely = sources - .iter() - .any(|src| scalar_likely.contains(&ssa_var_key(src))); - let any_bool = sources - .iter() - .any(|src| bool_like.contains(&ssa_var_key(src))); - - if any_proven { - changed |= scalar_proven.insert(dst_key.clone()); - } - if any_likely || any_proven { - changed |= scalar_likely.insert(dst_key.clone()); - } - if any_bool { - changed |= bool_like.insert(dst_key.clone()); - } - let max_bits = sources - .iter() - .filter_map(|src| width_hints.get(&ssa_var_key(src)).copied()) - .max() - .unwrap_or(0); - if max_bits > 0 { - let entry = width_hints.entry(dst_key).or_insert(0); - if max_bits > *entry { - *entry = max_bits; - changed = true; - } - } - } - r2ssa::SSAOp::Copy { dst, src } - | r2ssa::SSAOp::Cast { dst, src } - | r2ssa::SSAOp::New { dst, src } => { - let dst_key = ssa_var_key(dst); - let src_key = ssa_var_key(src); - if scalar_proven.contains(&src_key) { - changed |= scalar_proven.insert(dst_key.clone()); - } - if scalar_likely.contains(&src_key) || scalar_proven.contains(&src_key) { - changed |= scalar_likely.insert(dst_key.clone()); - } - if bool_like.contains(&src_key) { - changed |= bool_like.insert(dst_key.clone()); - } - let bits = width_hints.get(&src_key).copied().unwrap_or(0); - if bits > 0 { - let entry = width_hints.entry(dst_key).or_insert(0); - if bits > *entry { - *entry = bits; - changed = true; - } - } - } - r2ssa::SSAOp::IntZExt { dst, src } - | r2ssa::SSAOp::IntSExt { dst, src } - | r2ssa::SSAOp::Subpiece { dst, src, .. } => { - let dst_key = ssa_var_key(dst); - let src_key = ssa_var_key(src); - if scalar_likely.contains(&src_key) || scalar_proven.contains(&src_key) { - changed |= scalar_likely.insert(dst_key.clone()); - } - if bool_like.contains(&src_key) { - changed |= bool_like.insert(dst_key.clone()); - } - let entry = width_hints.entry(dst_key).or_insert(0); - let bits = dst.size.saturating_mul(8); - if bits > *entry { - *entry = bits; - changed = true; - } - } - _ => {} - } - } - } +fn size_to_unsigned_int_type(size: u32) -> String { + match size { + 1 => "uint8_t".to_string(), + 2 => "uint16_t".to_string(), + 4 => "uint32_t".to_string(), + 8 => "uint64_t".to_string(), + _ => format!("uint{}_t", size.saturating_mul(8)), + } +} - for reg_keys in register_versions.values() { - let any_proven = reg_keys.iter().any(|key| scalar_proven.contains(key)); - let any_likely = reg_keys.iter().any(|key| scalar_likely.contains(key)); - let any_bool = reg_keys.iter().any(|key| bool_like.contains(key)); - let max_bits = reg_keys - .iter() - .filter_map(|key| width_hints.get(key).copied()) - .max() - .unwrap_or(0); - let min_bits = reg_keys - .iter() - .filter_map(|key| width_hints.get(key).copied()) - .filter(|bits| *bits > 0) - .min() - .unwrap_or(0); - let preferred_bits = if min_bits > 0 && max_bits == 64 && min_bits <= 32 { - min_bits - } else { - max_bits +fn scalar_kind_to_type(kind: r2il::ScalarKind, size: u32) -> Option { + match kind { + r2il::ScalarKind::Bool => Some(TypeHint { + rank: TypeHintRank::Integer, + ty: "bool".to_string(), + }), + r2il::ScalarKind::SignedInt => Some(TypeHint { + rank: TypeHintRank::Integer, + ty: size_to_signed_int_type(size), + }), + r2il::ScalarKind::UnsignedInt => Some(TypeHint { + rank: TypeHintRank::Integer, + ty: size_to_unsigned_int_type(size), + }), + r2il::ScalarKind::Float => { + let ty = match size { + 4 => "float".to_string(), + 8 => "double".to_string(), + 16 => "long double".to_string(), + _ => "float".to_string(), }; - for key in reg_keys { - if any_proven { - changed |= scalar_proven.insert(key.clone()); - } - if any_likely || any_proven { - changed |= scalar_likely.insert(key.clone()); - } - if any_bool { - changed |= bool_like.insert(key.clone()); - } - if preferred_bits > 0 { - let entry = width_hints.entry(key.clone()).or_insert(0); - if preferred_bits > *entry { - *entry = preferred_bits; - changed = true; - } - } - } + Some(TypeHint { + rank: TypeHintRank::Float, + ty, + }) } + r2il::ScalarKind::Bitvector | r2il::ScalarKind::Unknown => None, } - - (scalar_proven, scalar_likely, bool_like, width_hints) } -pub(crate) fn collect_signature_type_evidence_context( - ssa_blocks: &[r2ssa::SSABlock], -) -> crate::SignatureTypeEvidenceContext { - let pointer_vars = infer_pointer_var_keys_from_ssa(ssa_blocks); - let (scalar_proven_vars, scalar_likely_vars, bool_like_vars, mut width_bits) = - infer_scalar_var_evidence_from_ssa(ssa_blocks); - let register_versions = collect_register_version_keys(ssa_blocks); - normalize_register_family_width_hints(®ister_versions, &mut width_bits); - propagate_normalized_scalar_result_widths(ssa_blocks, &mut width_bits); - normalize_register_family_width_hints(®ister_versions, &mut width_bits); - crate::SignatureTypeEvidenceContext { - pointer_vars, - scalar_proven_vars, - scalar_likely_vars, - bool_like_vars, - width_bits, +fn metadata_type_hint(vn: &r2il::Varnode) -> Option { + let meta = vn.meta.as_ref()?; + + if let Some(pointer_hint) = meta.pointer_hint + && !matches!(pointer_hint, r2il::PointerHint::Unknown) + { + return Some(TypeHint::pointer()); } + + let scalar_kind = meta.scalar_kind?; + scalar_kind_to_type(scalar_kind, vn.size) } -fn infer_usage_register_type_hints( - ssa_blocks: &[r2ssa::SSABlock], -) -> ( - std::collections::HashMap, - std::collections::HashSet, -) { - let pointer_vars = infer_pointer_var_keys_from_ssa(ssa_blocks); - let mut hints = std::collections::HashMap::new(); +pub(crate) fn collect_register_type_hints( + r2il_blocks: &[R2ILBlock], + disasm: &Disassembler, +) -> std::collections::HashMap { + let mut hints: std::collections::HashMap = std::collections::HashMap::new(); - for block in ssa_blocks { + for block in r2il_blocks { for op in &block.ops { - let mut maybe_add = |var: &r2ssa::SSAVar| { - let key = ssa_var_key(var); - if !pointer_vars.contains(&key) || !ssa_var_is_register_like(&var.name) { - return; + for vn in crate::op_all_varnodes(op) { + if !vn.is_register() { + continue; } - merge_type_hint( - &mut hints, - var.name.to_ascii_lowercase(), - TypeHint::pointer(), - ); - }; + let Some(hint) = metadata_type_hint(vn) else { + continue; + }; + let Some(name) = disasm.register_name(vn) else { + continue; + }; - if let Some(dst) = op.dst() { - maybe_add(dst); + let key = name.to_ascii_lowercase(); + merge_type_hint(&mut hints, key, hint); } - op.for_each_source(&mut maybe_add); } } - (hints, pointer_vars) + hints } -fn strongest_hint_for_aliases( - hints: &std::collections::HashMap, - canonical: &str, - aliases: &[&str], -) -> Option { - let mut best = hints.get(canonical).cloned(); - for alias in aliases { - if let Some(candidate) = hints.get(*alias).cloned() { - match &best { - Some(current) if !incoming_hint_should_replace(current, &candidate) => {} - _ => best = Some(candidate), - } - } - } - best +#[cfg(test)] +pub(crate) const X86_ARG_REGS: &[(&str, &[&str])] = r2types::X86_ARG_REGS; +#[cfg(test)] +pub(crate) const X86_FRAME_BASES: &[&str] = r2types::X86_FRAME_BASES; +#[cfg(test)] +type ArgAliasMap = r2types::ArgAliasMap; +#[cfg(test)] +type BaseRegList = r2types::BaseRegList; + +#[cfg(test)] +pub(crate) fn recover_vars_arch_profile( + arch: Option<&ArchSpec>, +) -> (ArgAliasMap, BaseRegList, BaseRegList) { + r2types::recover_vars_arch_profile(arch.map(|spec| spec.name.as_str())) } -fn width_hint_key_matches_family(key: &str, family: &str) -> bool { - let Some((name, _version)) = key.rsplit_once('_') else { - return false; - }; - scalar_register_family_key(name) == family +pub(crate) fn ssa_var_key(var: &r2ssa::SSAVar) -> String { + r2types::ssa_var_key(var) } -fn width_hint_key_matches_family_version(key: &str, family: &str, version: u32) -> bool { - let Some((name, version_str)) = key.rsplit_once('_') else { - return false; - }; - version_str.parse::().ok() == Some(version) && scalar_register_family_key(name) == family +#[cfg(test)] +pub(crate) fn ssa_var_block_key(block_addr: u64, var: &r2ssa::SSAVar) -> String { + r2types::ssa_var_block_key(block_addr, var) } -fn recovered_arg_family_width_hint( - evidence: &crate::SignatureTypeEvidenceContext, - src: &r2ssa::SSAVar, -) -> Option { - let family = scalar_register_family_key(&src.name); - evidence - .width_bits - .iter() - .filter(|(key, bits)| { - **bits > 0 && width_hint_key_matches_family_version(key, &family, src.version) - }) - .map(|(_, bits)| *bits) - .min() - .or_else(|| { - evidence - .width_bits - .iter() - .filter(|(key, bits)| **bits > 0 && width_hint_key_matches_family(key, &family)) - .map(|(_, bits)| *bits) - .min() - }) +#[cfg(test)] +pub(crate) fn scalar_register_family_key(name: &str) -> String { + r2types::scalar_register_family_key(name) } -fn recovered_arg_type_hint( - reg_type_hints: &std::collections::HashMap, - canonical: &str, - aliases: &[&str], - src: &r2ssa::SSAVar, - signature_evidence: Option<&crate::SignatureTypeEvidenceContext>, -) -> Option { - let best_hint = strongest_hint_for_aliases(reg_type_hints, canonical, aliases); - if let Some(hint) = best_hint.as_ref() - && hint.rank == TypeHintRank::Pointer - { - return Some(hint.ty.clone()); - } - if let Some(evidence) = signature_evidence - && let Some(bits) = recovered_arg_family_width_hint(evidence, src) - { - return Some(size_to_type(bits.div_ceil(8))); - } - best_hint.map(|hint| hint.ty) +#[cfg(test)] +pub(crate) fn collect_signature_type_evidence_context( + ssa_blocks: &[r2ssa::SSABlock], +) -> r2types::SignatureTypeEvidenceContext { + r2types::collect_signature_type_evidence_context(ssa_blocks) } +#[cfg(test)] pub(crate) fn merge_register_type_hints( metadata_hints: &std::collections::HashMap, usage_hints: &std::collections::HashMap, @@ -2247,7 +1299,14 @@ pub(crate) fn merge_register_type_hints( } for (canonical, aliases) in arg_regs { - if let Some(best) = strongest_hint_for_aliases(&merged, canonical, aliases) { + let candidates: Vec = std::iter::once(*canonical) + .chain(aliases.iter().copied()) + .filter_map(|name| merged.get(name).cloned()) + .collect(); + if let Some(best) = candidates + .into_iter() + .max_by(|a, b| a.rank.cmp(&b.rank).then_with(|| b.ty.cmp(&a.ty))) + { merge_type_hint(&mut merged, (*canonical).to_string(), best.clone()); for alias in *aliases { merge_type_hint(&mut merged, alias.to_string(), best.clone()); @@ -2259,16 +1318,10 @@ pub(crate) fn merge_register_type_hints( } pub(crate) fn collect_pointer_arg_slots(vars: &[VarProt]) -> std::collections::BTreeSet { - vars.iter() - .filter(|var| var.kind == "r" && var.isarg && var.var_type.contains('*')) - .filter_map(|var| { - var.name - .strip_prefix("arg") - .and_then(|idx| idx.parse::().ok()) - }) - .collect() + r2types::collect_pointer_arg_slots(vars) } +#[cfg(test)] pub(crate) fn merge_pointer_slot_evidence( inferred_params: &mut [InferredParam], pointer_arg_slots: &std::collections::BTreeSet, @@ -2299,140 +1352,15 @@ pub(crate) fn recover_vars_from_ssa( metadata_reg_type_hints: &std::collections::HashMap, semantic_typing_enabled: bool, ) -> Vec { - use std::collections::{HashMap, HashSet}; - - let mut vars = Vec::new(); - let mut seen_slots: HashMap<(bool, i64), usize> = HashMap::new(); - let mut seen_arg_regs: HashSet = HashSet::new(); - let (arg_regs, stack_bases, frame_bases) = recover_vars_arch_profile(arch); - let signature_evidence = - semantic_typing_enabled.then(|| collect_signature_type_evidence_context(ssa_blocks)); - let (usage_reg_type_hints, pointer_var_keys) = if semantic_typing_enabled { - infer_usage_register_type_hints(ssa_blocks) - } else { - (HashMap::new(), HashSet::new()) - }; - let reg_type_hints = if semantic_typing_enabled { - merge_register_type_hints(metadata_reg_type_hints, &usage_reg_type_hints, arg_regs) - } else { - HashMap::new() - }; - - let mut stack_addr_temps: HashMap = HashMap::new(); - - for block in ssa_blocks { - for op in &block.ops { - match op { - r2ssa::SSAOp::IntAdd { dst, a, b } | r2ssa::SSAOp::IntSub { dst, a, b } => { - let a_name = a.name.to_lowercase(); - let b_name = b.name.to_lowercase(); - - let is_a_base = stack_bases.contains(&a_name.as_str()); - let is_b_const = b_name.starts_with("const:"); - - if is_a_base && is_b_const { - if let Some(raw_offset) = parse_const_value(&b.name) { - let offset = if matches!(op, r2ssa::SSAOp::IntSub { .. }) { - -(raw_offset as i64) - } else { - raw_offset as i64 - }; - let dst_key = ssa_var_block_key(block.addr, dst); - stack_addr_temps.insert(dst_key, (a_name.clone(), offset)); - } - } else if stack_bases.contains(&b_name.as_str()) - && a_name.starts_with("const:") - && let Some(raw_offset) = parse_const_value(&a.name) - { - let offset = raw_offset as i64; - let dst_key = ssa_var_block_key(block.addr, dst); - stack_addr_temps.insert(dst_key, (b_name.clone(), offset)); - } - } - r2ssa::SSAOp::Store { addr, val, .. } => { - let addr_key = ssa_var_block_key(block.addr, addr); - if let Some((base_reg, offset)) = stack_addr_temps.get(&addr_key) { - let type_override = if semantic_typing_enabled - && pointer_var_keys.contains(&ssa_var_key(val)) - { - Some("void *".to_string()) - } else { - None - }; - add_stack_var( - &mut vars, - &mut seen_slots, - base_reg, - frame_bases, - *offset, - val.size, - type_override, - ); - } - } - r2ssa::SSAOp::Load { dst, addr, .. } => { - let addr_key = ssa_var_block_key(block.addr, addr); - if let Some((base_reg, offset)) = stack_addr_temps.get(&addr_key) { - let type_override = if semantic_typing_enabled - && pointer_var_keys.contains(&ssa_var_key(dst)) - { - Some("void *".to_string()) - } else { - None - }; - add_stack_var( - &mut vars, - &mut seen_slots, - base_reg, - frame_bases, - *offset, - dst.size, - type_override, - ); - } - } - _ => {} - } - - for src in op.sources() { - let base_name = src.name.to_lowercase(); - if src.version == 0 { - for (i, (canonical, aliases)) in arg_regs.iter().enumerate() { - if aliases.contains(&base_name.as_str()) - && !seen_arg_regs.contains(*canonical) - { - seen_arg_regs.insert(canonical.to_string()); - let hinted_type = if semantic_typing_enabled { - recovered_arg_type_hint( - ®_type_hints, - canonical, - aliases, - src, - signature_evidence.as_ref(), - ) - } else { - None - }; - vars.push(VarProt { - name: format!("arg{}", i), - kind: "r".to_string(), - delta: 0, - var_type: hinted_type.unwrap_or_else(|| size_to_type(src.size)), - isarg: true, - reg: Some(canonical.to_string()), - }); - break; - } - } - } - } - } - } - - vars.sort_by_key(|v| v.delta); - vars + r2types::recover_vars_from_ssa( + ssa_blocks, + arch.map(|spec| spec.name.as_str()), + metadata_reg_type_hints, + semantic_typing_enabled, + ) } +#[cfg(test)] pub(crate) fn add_stack_var( vars: &mut Vec, seen_slots: &mut std::collections::HashMap<(bool, i64), usize>, @@ -2496,14 +1424,9 @@ pub(crate) fn parse_const_value(name: &str) -> Option { u64::from_str_radix(val_str, 16).ok() } +#[cfg(test)] pub(crate) fn size_to_type(size: u32) -> String { - match size { - 1 => "int8_t".to_string(), - 2 => "int16_t".to_string(), - 4 => "int32_t".to_string(), - 8 => "int64_t".to_string(), - _ => format!("byte[{}]", size), - } + r2types::size_to_type(size) } fn parse_const_addr(name: &str) -> Option { @@ -3036,6 +1959,14 @@ mod tests { infer_signature_return_type, resolve_evidence_driven_type, }; + fn const_return_blocks(addr: u64, value: u64) -> Vec { + let mut block = r2il::R2ILBlock::new(addr, 4); + block.push(r2il::R2ILOp::Return { + target: r2il::Varnode::constant(value, 8), + }); + vec![block] + } + #[test] fn get_data_refs_resolves_const_add_chain_target() { let block = r2ssa::SSABlock { @@ -4119,4 +3050,176 @@ mod tests { assert_eq!(mips_args.len(), 4, "mips should expose a0..a3 args"); assert!(mips_args[0].1.contains(&"$a0")); } + + #[test] + fn function_artifact_cache_key_distinguishes_symbolic_scope() { + let arch = ArchSpec::new("x86-64"); + let root_blocks = const_return_blocks(0x1000, 0); + let helper_a_blocks = const_return_blocks(0x2000, 1); + let helper_b_blocks = const_return_blocks(0x2000, 2); + + let root_prepared = + r2ssa::SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic ssa"); + let helper_a_prepared = r2ssa::SsaArtifact::for_symbolic(&helper_a_blocks, Some(&arch)) + .expect("helper a symbolic ssa"); + let helper_b_prepared = r2ssa::SsaArtifact::for_symbolic(&helper_b_blocks, Some(&arch)) + .expect("helper b symbolic ssa"); + + let scope_a = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root_prepared.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper".to_string()), + prepared: helper_a_prepared, + }, + ], + ) + .expect("scope a"); + let scope_b = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root_prepared, + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper".to_string()), + prepared: helper_b_prepared, + }, + ], + ) + .expect("scope b"); + + let key_without_scope = function_artifact_cache_key_parts( + "root", + Some(&arch), + &root_blocks, + true, + "{}", + "{}", + 1, + None, + ) + .expect("root-only cache key"); + let key_a = function_artifact_cache_key_parts( + "root", + Some(&arch), + &root_blocks, + true, + "{}", + "{}", + 1, + Some(&scope_a), + ) + .expect("scoped cache key a"); + let key_a_repeat = function_artifact_cache_key_parts( + "root", + Some(&arch), + &root_blocks, + true, + "{}", + "{}", + 1, + Some(&scope_a), + ) + .expect("scoped cache key a repeat"); + let key_b = function_artifact_cache_key_parts( + "root", + Some(&arch), + &root_blocks, + true, + "{}", + "{}", + 1, + Some(&scope_b), + ) + .expect("scoped cache key b"); + + assert_eq!( + key_a, key_a_repeat, + "same symbolic scope should hash stably" + ); + assert_ne!( + key_without_scope, key_a, + "scope-aware artifacts must not alias the root-only cache key" + ); + assert_ne!( + key_a, key_b, + "different helper closures must not alias the same artifact cache entry" + ); + } + + #[test] + fn function_artifact_cache_key_distinguishes_function_name() { + let arch = ArchSpec::new("x86-64"); + let blocks = const_return_blocks(0x1000, 0); + let first = function_artifact_cache_key_parts( + "sym.first", + Some(&arch), + &blocks, + true, + "{}", + "{}", + 1, + None, + ) + .expect("first cache key"); + let second = function_artifact_cache_key_parts( + "sym.second", + Some(&arch), + &blocks, + true, + "{}", + "{}", + 1, + None, + ) + .expect("second cache key"); + + assert_ne!( + first, second, + "name-sensitive writeback artifacts must not alias across functions" + ); + } + + #[test] + fn function_artifact_cache_key_distinguishes_interproc_iteration_budget() { + let arch = ArchSpec::new("x86-64"); + let blocks = const_return_blocks(0x1000, 0); + let first = function_artifact_cache_key_parts( + "sym.root", + Some(&arch), + &blocks, + true, + "{}", + r#"{"phase":"fixpoint","summaries":[],"seeds":[]}"#, + 1, + None, + ) + .expect("low budget cache key"); + let second = function_artifact_cache_key_parts( + "sym.root", + Some(&arch), + &blocks, + true, + "{}", + r#"{"phase":"fixpoint","summaries":[],"seeds":[]}"#, + 4, + None, + ) + .expect("high budget cache key"); + + assert_ne!( + first, second, + "interproc summary artifacts must invalidate when the fixpoint budget changes" + ); + } } diff --git a/tests/e2e/stress_test.c b/tests/e2e/stress_test.c index 3f86d55..a6df51e 100644 --- a/tests/e2e/stress_test.c +++ b/tests/e2e/stress_test.c @@ -713,6 +713,53 @@ int interpret_bytecode(uint8_t *code, int len) { return acc; } +// Small interpreter loop intended to be step-summary friendly. +__attribute__((noinline)) +int tiny_vm_dispatch(uint8_t *code, int len) { + int ip = 0; + int acc = 0; + uint8_t dispatch_op = 0; + +dispatch: + if (ip >= len) { + return acc; + } + + dispatch_op = code[ip++]; + switch (dispatch_op) { + case 0x00: + acc += 1; + goto dispatch; + case 0x01: + acc += 2; + goto dispatch; + case 0x02: + acc -= 1; + goto dispatch; + case 0x03: + acc ^= 0x55; + goto dispatch; + case 0x04: + acc ^= 0xaa; + goto dispatch; + case 0x05: + acc <<= 1; + goto dispatch; + case 0x06: + acc >>= 1; + goto dispatch; + case 0x07: + if (acc == 0 && ip < len) { + ip += (int8_t)code[ip]; + } + goto dispatch; + case 0xff: + return acc; + default: + return -1; + } +} + // ============================================================================ // 16. Vulnerability patterns for taint analysis // ============================================================================ @@ -896,6 +943,7 @@ int main(int argc, char *argv[]) { printf(" 18: ht_insert + ht_lookup\n"); printf(" 19: vuln patterns\n"); printf(" 20: saturating_add\n"); + printf(" 21: tiny_vm_dispatch\n"); return 1; } @@ -1059,6 +1107,11 @@ int main(int argc, char *argv[]) { } break; } + case 21: { + uint8_t code[] = {0x00, 0x01, 0x03, 0x05, 0x06, 0x04, 0xff}; + printf("tiny_vm_dispatch result = %d\n", tiny_vm_dispatch(code, sizeof(code))); + break; + } default: printf("Unknown test: %d\n", test); return 1; diff --git a/tests/e2e/vuln_test.c b/tests/e2e/vuln_test.c index aa4403b..0921d58 100644 --- a/tests/e2e/vuln_test.c +++ b/tests/e2e/vuln_test.c @@ -20,10 +20,14 @@ #include #include #include +#include +#include volatile int global_counter = 0; volatile int global_limit = 10; +volatile int global_symbolic_guard = 0; volatile int global_tail = 0; +volatile unsigned long global_region_guard = 0x41UL; // Test 1: Simple password check (symbolic should find 0xDEAD) int check_secret(int x) { @@ -168,6 +172,69 @@ int test_boolxor(int a, int b) { return (a > 0) ^ (b > 0); } +// Test 16b: Volatile-backed opaque guard for symbolic branch pruning +int test_symbolic_xor_guard(int x) { + global_symbolic_guard = x; + if ((global_symbolic_guard ^ global_symbolic_guard) != 0) { + return 1; + } + return 0; +} + +__attribute__((noinline)) +static int helper_symbolic_zero(int x) { + global_symbolic_guard = x; + return global_symbolic_guard ^ global_symbolic_guard; +} + +// Test 16c: Helper-return opaque guard that requires helper-scope symbolic facts +int test_symbolic_helper_xor_guard(int x) { + if (helper_symbolic_zero(x) != 0) { + return 1; + } + return 0; +} + +__attribute__((noinline)) +static int helper_symbolic_zero_pure(int x) { + return x ^ x; +} + +// Test 16d: Pure helper-return opaque guard for live symbolic fact pruning +int test_symbolic_helper_pure_guard(int x) { + if (helper_symbolic_zero_pure(x) != 0) { + return 1; + } + return 0; +} + +// Test 16e: Global-backed symbolic guard for region-backed compiled predicates +int test_global_symbolic_eq_guard(int x) { + global_symbolic_guard = x; + if (global_symbolic_guard == 7) { + return 1; + } + return 0; +} + +// Test 16f: Stack-backed symbolic guard for region-backed compiled predicates +int test_stack_symbolic_eq_guard(int x) { + volatile int slot = x; + volatile int *ptr = &slot; + if (*ptr == 7) { + return 1; + } + return 0; +} + +// Test 16g: Global-backed constant guard for region-backed compiled predicates +int test_global_region_guard(void) { + if (global_region_guard == 0x41UL) { + return 1; + } + return 0; +} + // Test 17: Pointer add (array indexing) int test_array_index(int *arr, int idx) { return arr[idx]; @@ -476,6 +543,50 @@ int test_bool_carrier_chain(int x, int y) { return x; } +// Test 44: Stdin-driven gate for typed symbolic fd input specs. +__attribute__((noinline)) int stdin_gate(void) { + char buf[2] = {0}; + ssize_t n = read(0, buf, 1); + if (n != 1) { + return 0; + } + if (isatty(0)) { + global_tail++; + } + usleep(1000); + if (buf[0] == 'k') { + return 0x1337; + } + return 0; +} + +// Test 45: Replay-focused stdin gate without tty-sensitive behavior. +__attribute__((noinline)) int stdin_gate_replay(void) { + char buf[2] = {0}; + struct termios old_termios; + struct termios raw_termios; + int has_termios = tcgetattr(0, &old_termios) == 0; + if (has_termios) { + raw_termios = old_termios; + raw_termios.c_lflag &= (tcflag_t) ~(ICANON | ECHO); + raw_termios.c_cc[VMIN] = 1; + raw_termios.c_cc[VTIME] = 0; + tcsetattr(0, TCSANOW, &raw_termios); + } + ssize_t n = read(0, buf, 1); + if (has_termios) { + tcsetattr(0, TCSANOW, &old_termios); + } + if (n != 1) { + return 0; + } + usleep(1000); + if (buf[0] == 'k') { + return 0x1337; + } + return 0; +} + int main(int argc, char *argv[]) { if (argc < 2) { printf("Usage: %s [args...]\n", argv[0]); @@ -822,6 +933,55 @@ int main(int argc, char *argv[]) { ); } break; + case 44: + printf("stdin_gate() = %d\n", stdin_gate()); + break; + case 45: + (void)stdin_gate_replay(); + break; + case 46: + if (argc > 2) { + int x = atoi(argv[2]); + printf( + "test_symbolic_helper_xor_guard(%d) = %d\n", + x, + test_symbolic_helper_xor_guard(x) + ); + } + break; + case 47: + if (argc > 2) { + int x = atoi(argv[2]); + printf( + "test_symbolic_helper_pure_guard(%d) = %d\n", + x, + test_symbolic_helper_pure_guard(x) + ); + } + break; + case 48: + if (argc > 2) { + int x = atoi(argv[2]); + printf( + "test_global_symbolic_eq_guard(%d) = %d\n", + x, + test_global_symbolic_eq_guard(x) + ); + } + break; + case 49: + if (argc > 2) { + int x = atoi(argv[2]); + printf( + "test_stack_symbolic_eq_guard(%d) = %d\n", + x, + test_stack_symbolic_eq_guard(x) + ); + } + break; + case 50: + printf("test_global_region_guard() = %d\n", test_global_region_guard()); + break; default: printf("Unknown test: %d\n", test); return 1; diff --git a/tests/r2r/db/extras/r2sleigh_decompiler_snapshots b/tests/r2r/db/extras/r2sleigh_decompiler_snapshots index 8cb5361..c86f5a2 100644 --- a/tests/r2r/db/extras/r2sleigh_decompiler_snapshots +++ b/tests/r2r/db/extras/r2sleigh_decompiler_snapshots @@ -2,7 +2,7 @@ NAME=decompiler_bool_carrier_chain_full_snapshot FILE=bins/vuln_test_x86 ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true EXPECT=</dev/null -a:sla.sym.paths | jq -c 'type=="array"' +a:sla.sym.paths | jq -c 'type=="array" and length>0 and length<=32' EOF_CMDS RUN @@ -24,6 +24,34 @@ a:sla.dec | jq -Rs 'contains("r2dec fallback: skipped decompilation")' EOF_CMDS RUN +NAME=stress_ls_main_materialized_decompile_fallback_from_semantic_capability +FILE=/bin/ls +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sla.sym | jq -c 'has("sat_queries") and has("sat_cache_hits") and has("sat_cache_misses") and has("solve_unsat_shortcuts")' +EOF_CMDS +RUN + NAME=symbolic_unlock_has_paths_explored FILE=bins/vuln_test ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true @@ -195,6 +236,305 @@ a:sla.sym | jq -c 'has("states_explored")' EOF_CMDS RUN +NAME=symbolic_tiny_vm_dispatch_reports_vm_summary +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<=8 and (.semantic.vm_step.loop_latches|length)>=7 and (.semantic.vm_step.step_blocks|length)>=10 and (.semantic.vm_step.handler_state_updates | to_entries | length) >= 5 and ((.semantic.vm_step.handler_state_updates | to_entries | map(.value | length) | add) >= 5)' +EOF_CMDS +RUN + +NAME=symbolic_tiny_vm_dispatch_reports_semantic_cache_hit_on_repeat +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sla.sym sym.tiny_vm_dispatch | jq -c '.semantic.execution=="vm" and .semantic.cache_hit==true and .semantic.vm_transfer != null and (.semantic.vm_transfer.transfers | type=="array")' +EOF_CMDS +RUN + +NAME=symbolic_interpret_bytecode_vm_summary_large_cfg +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<=10' +EOF_CMDS +RUN + +NAME=symbolic_interpret_bytecode_vm_summary_stays_query_ready +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<= 8) and ((.semantic.vm_transfer.transfers | map(select(.redispatch)) | length) >= 4) and (((.semantic.vm_transfer.handler_exit_guards // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_read_effects // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_write_effects // {}) | keys | length) >= 1)' +EOF_CMDS +RUN + +NAME=facts_native_symbolic_guard_reports_combined_payload +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<= 1 and .semantic_route.kind=="standard" and .type_writeback.compiled_semantics.execution=="native" and (.type_writeback.compiled_semantics.region_count >= 1) and (.type_writeback.semantics.body.Native.regions | length) >= 1' +EOF_CMDS +RUN + +NAME=plan_tiny_vm_dispatch_reports_vm_summary_route +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<= 8 and .semantic.execution=="vm" and .semantic_route.kind=="vm_summary" and .semantic_build_plan != null and (.prefer_bounded_type_plan | type=="boolean")' +EOF_CMDS +RUN + +NAME=types_tiny_vm_dispatch_reports_canonical_vm_step_payload +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<=8 and (.compiled_semantics.vm_step.loop_latches|length)>=7 and (.compiled_semantics.vm_step.step_blocks|length)>=10 and (.compiled_semantics.vm_step.case_values_by_target | keys | length) >= 8 and (.compiled_semantics.vm_step.handler_state_updates | to_entries | length) >= 5' +EOF_CMDS +RUN + +NAME=types_tiny_vm_dispatch_reports_vm_transfer_payload +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<=8 and (.compiled_semantics.vm_transfer.loop_latches|length)>=7 and (.compiled_semantics.vm_transfer.step_blocks|length)>=10 and (.compiled_semantics.vm_transfer.handler_state_updates | to_entries | length) >= 5 and (.compiled_semantics.vm_transfer.transfers | type=="array") and (((.compiled_semantics.vm_transfer.handler_exit_guards // {}) | keys | length) >= 1) and (((.compiled_semantics.vm_transfer.handler_memory_read_effects // {}) | keys | length) >= 1) and (((.compiled_semantics.vm_transfer.handler_memory_write_effects // {}) | keys | length) >= 1) and (((.compiled_semantics.vm_transfer.transfers // []) | map((.exit_guards // []) | length) | add) > 0) and (((.compiled_semantics.vm_transfer.transfers // []) | map((.memory_reads // []) | length) | add) > 0) and (((.compiled_semantics.vm_transfer.transfers // []) | map((.memory_writes // []) | length) | add) > 0) and (((.compiled_semantics.vm_transfer.transfers // []) | map(((.memory_reads // []) + (.memory_writes // [])) | map(select(.binding != null)) | length) | add) > 0) and (.compiled_semantics.vm_transfer.transfers[0] | has("residual_guards") and has("residual_memory_effects"))' +EOF_CMDS +RUN + +NAME=types_tiny_vm_dispatch_reports_semantic_cache_hit_on_repeat +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sla.types sym.tiny_vm_dispatch | jq -c '.compiled_semantics.execution=="vm" and .compiled_semantics.cache_hit==true and .compiled_semantics.vm_transfer != null' +EOF_CMDS +RUN + +NAME=symbolic_tiny_vm_dispatch_reports_vm_transfer_payload +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<=8 and (.semantic.vm_transfer.loop_latches|length)>=7 and (.semantic.vm_transfer.step_blocks|length)>=10 and (.semantic.vm_transfer.transfers | type=="array") and (((.semantic.vm_transfer.handler_exit_guards // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_read_effects // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_write_effects // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.transfers // []) | map((.exit_guards // []) | length) | add) > 0) and (((.semantic.vm_transfer.transfers // []) | map((.memory_reads // []) | length) | add) > 0) and (((.semantic.vm_transfer.transfers // []) | map((.memory_writes // []) | length) | add) > 0) and (((.semantic.vm_transfer.transfers // []) | map(((.memory_reads // []) + (.memory_writes // [])) | map(select(.binding != null)) | length) | add) > 0) and (.semantic.vm_transfer.transfers[0] | has("residual_guards") and has("residual_memory_effects"))' +EOF_CMDS +RUN + +NAME=types_stack_symbolic_eq_guard_reports_canonical_stack_memory_term +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<=1 and ((.semantics.body.Native.regions|length)>=1) and (((.semantics.body.Native.regions[0].control // []) | map(select(.value.compiled.precision=="Exact" and ((.value.compiled.memory_terms|length)>=1) and .value.compiled.memory_terms[0].region.Region.kind=="Stack" and .value.compiled.memory_terms[0].exact_offset==true)) | length) >= 1)' +EOF_CMDS +RUN + +NAME=types_stack_symbolic_eq_guard_reports_canonical_memory_region +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<= 1 and ((.semantics.body.Native.regions|length) >= 1) and .semantics.body.Native.regions[0].anchor != null and ((.semantics.body.Native.regions[0].memory|length) >= 1) and .semantics.body.Native.regions[0].memory[0].value.term.region.Region.kind=="Stack"' +EOF_CMDS +RUN + +NAME=sym_stack_symbolic_eq_guard_reports_memory_region_count +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<= 1' +EOF_CMDS +RUN + +NAME=decompile_tiny_vm_dispatch_uses_vm_summary_fallback +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sym.replayj {"seed_checkpoint":2,"alphabet":"jk","max_depth":1,"beam_width":4,"frontier":[{"op":"eq","lhs":{"kind":"reg","name":"pc"},"rhs":{"kind":"const","value":"sym.stdin_gate_replay+0x10f"}}],"find":[{"op":"and","args":[{"op":"eq","lhs":{"kind":"reg","name":"pc"},"rhs":{"kind":"const","value":"sym.stdin_gate_replay+0x10f"}},{"op":"eq","lhs":{"kind":"reg","name":"rax"},"rhs":{"kind":"const","value":"0x1337"}}]}],"score":{"order":"max","expr":{"kind":"reg","name":"rax"}}} | head -n1 | jq -c '.seed_checkpoint==2 and .replay_fd==0 and .found==true and .explored_branches==2 and (.matches|length)==1 and .matches[0].input=="k" and .matches[0].score==4919 and (.matches[0].checkpoint>.seed_checkpoint) and .matches[0].hit==.matches[0].snapshot.pc and (.active|length)==1 and .active[0].input=="j" and .active[0].score==0' +EOF_CMDS +RUN + +NAME=sym_explore_replayj_usage +FILE=bins/vuln_test_x86 +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=< +EOF_EXPECT +CMDS=< +EOF_EXPECT +CMDS=</dev/null +a:sym.explore.replayj 0x40132a {"checkpoint":2,"entry":"sym.check_secret","symbolic_registers":["rdi"]} | head -n1 | jq -c '.target=="0x40132a" and .matched_paths>0 and (.paths|length)>0 and any(.paths[]; .final_pc=="0x40132a" and ((.solution.inputs.replay_rdi // .solution.inputs.EDI_0)=="0xdead"))' +EOF_CMDS +RUN + +NAME=sym_solve_replayj_check_secret_rdi_overlay_finds_0xdead +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sym.solve.replayj 0x40132a {"checkpoint":2,"entry":"sym.check_secret","symbolic_registers":["rdi"]} | head -n1 | jq -c '.found==true and .matched_paths>0 and .target=="0x40132a" and (.selected_path|has("solution")) and .selected_path.final_pc=="0x40132a" and ((.selected_path.solution.inputs.replay_rdi // .selected_path.solution.inputs.EDI_0)=="0xdead")' +EOF_CMDS +RUN + +NAME=sym_explore_state_check_secret_rdi_overlay_reaches_win +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<0 and (.paths|length)>0 and any(.paths[]; .final_pc=="0x40132a" and ((.solution.inputs.replay_rdi // .solution.inputs.EDI_0)=="0xdead"))' +EOF_CMDS +RUN + +NAME=sym_solve_state_check_secret_rdi_overlay_finds_0xdead +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<0 and .target=="0x40132a" and (.selected_path|has("solution")) and .selected_path.final_pc=="0x40132a" and ((.selected_path.solution.inputs.replay_rdi // .selected_path.solution.inputs.EDI_0)=="0xdead")' +EOF_CMDS +RUN + NAME=paths_check_secret_structure FILE=bins/vuln_test_x86 ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true @@ -206,7 +546,7 @@ aaa s `afl~check_secret$[0]` >/dev/null s `afl~check_secret$[0]` >/dev/null s `afl~check_secret$[0]` >/dev/null -a:sla.sym.paths | jq -c 'length>0 and (.[0]|has("path_id") and has("feasible") and has("solution") and has("exit_status"))' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (.[0]|has("path_id") and has("feasible") and has("solution") and has("exit_status"))' EOF_CMDS RUN @@ -221,7 +561,7 @@ aaa s `afl~check_secret$[0]` >/dev/null s `afl~check_secret$[0]` >/dev/null s `afl~check_secret$[0]` >/dev/null -a:sla.sym.paths | jq -c 'tostring|contains("0xdead")' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (tostring|contains("0xdead"))' EOF_CMDS RUN @@ -236,7 +576,7 @@ aaa s `afl~solve_equation$[0]` >/dev/null s `afl~solve_equation$[0]` >/dev/null s `afl~solve_equation$[0]` >/dev/null -a:sla.sym.paths | jq -c 'tostring|contains("0xa")' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (tostring|contains("0xa"))' EOF_CMDS RUN @@ -251,7 +591,7 @@ aaa s `afl~bitwise_check$[0]` >/dev/null s `afl~bitwise_check$[0]` >/dev/null s `afl~bitwise_check$[0]` >/dev/null -a:sla.sym.paths | jq -c 'tostring|contains("0x5a")' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (tostring|contains("0x5a"))' EOF_CMDS RUN @@ -309,7 +649,7 @@ aaa s `afl~check_secret$[0]` >/dev/null s `afl~check_secret$[0]` >/dev/null s `afl~check_secret$[0]` >/dev/null -a:sla.sym.paths | jq -c 'type=="array"' +a:sla.sym.paths | jq -c 'type=="array" and length>0 and length<=32' EOF_CMDS RUN @@ -379,7 +719,7 @@ aaa s `afl~process_string$[0]` >/dev/null s `afl~process_string$[0]` >/dev/null s `afl~process_string$[0]` >/dev/null -a:sla.sym.paths | jq -c 'length>0 and (map(has("error")|not)|all)' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (map(has("error")|not)|all)' EOF_CMDS RUN @@ -394,7 +734,7 @@ aaa s `afl~authenticate$[0]` >/dev/null s `afl~authenticate$[0]` >/dev/null s `afl~authenticate$[0]` >/dev/null -a:sla.sym.paths | jq -c 'length>0 and (map(has("error")|not)|all)' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (map(has("error")|not)|all)' EOF_CMDS RUN @@ -409,7 +749,7 @@ aaa s `afl~alloc_and_copy$[0]` >/dev/null s `afl~alloc_and_copy$[0]` >/dev/null s `afl~alloc_and_copy$[0]` >/dev/null -a:sla.sym.paths | jq -c 'length>0 and (map(has("error")|not)|all)' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (map(has("error")|not)|all)' EOF_CMDS RUN @@ -424,7 +764,7 @@ aaa s `afl~vuln_printf$[0]` >/dev/null s `afl~vuln_printf$[0]` >/dev/null s `afl~vuln_printf$[0]` >/dev/null -a:sla.sym.paths | jq -c 'length>0 and (map(has("error")|not)|all)' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (map(has("error")|not)|all)' EOF_CMDS RUN @@ -439,7 +779,7 @@ aaa s `afl~vuln_memcpy$[0]` >/dev/null s `afl~vuln_memcpy$[0]` >/dev/null s `afl~vuln_memcpy$[0]` >/dev/null -a:sla.sym.paths | jq -c 'length>0 and (map(has("error")|not)|all)' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (map(has("error")|not)|all)' EOF_CMDS RUN @@ -458,6 +798,46 @@ a:sym.solve `afl~check_secret$[0]`+0x14 | jq -c '.found==true' EOF_CMDS RUN +NAME=sim_solve_materializes_deleted_current_function +FILE=bins/vuln_test_x86 +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sym.solve `is~check_secret[2]`+0x14 | jq -c '.found==true' +EOF_CMDS +RUN + +NAME=interactive_sym_runj_finds_check_secret_target +FILE=bins/vuln_test_x86 +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sym.runj {\"find\":[{\"kind\":\"address\",\"addr\":\"`afl~check_secret$[0]`+0x14\"}],\"budget\":{\"max_states\":64,\"max_depth\":128,\"max_finds\":1,\"timeout_ms\":5000}} | jq -c '.stash_counts.found == 1 and ((.found_paths[0].solution.inputs | length) > 0)' +EOF_CMDS +RUN + +NAME=interactive_sym_runj_solves_stdin_gate +FILE=bins/vuln_test_x86 +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sym.runj {\"find\":[{\"kind\":\"address\",\"addr\":\"`afl~stdin_gate$[0]`+0x69\"}],\"inputs\":[{\"kind\":\"fd\",\"fd\":0,\"len\":1,\"name\":\"stdin0\",\"alphabet\":\"k\"}],\"runtime\":{\"tty_fds\":[],\"skip_sleep_calls\":true},\"budget\":{\"max_states\":64,\"max_depth\":128,\"max_finds\":1,\"timeout_ms\":5000}} | jq -c '.stash_counts.found == 1 and .found_paths[0].solution.input_buffers.stdin0.hex == "6b" and .found_paths[0].solution.input_buffers.stdin0.ascii == "k"' +EOF_CMDS +RUN + NAME=merge_mode_on FILE=bins/vuln_test ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true @@ -720,6 +1100,180 @@ a:sla.dec entry0 | jq -Rs '(contains(" = eax ^ eax;")|not) and (contains(" = rax EOF_CMDS RUN +NAME=types_symbolic_xor_guard_prunes_dead_branch +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sla.types | jq -c '.compiled_semantics.branches_pruned == 1 and (.compiled_semantics.region_count == 1) and (((.semantics.body.Native.regions[0].targets // []) | map(.value.status) | index("Reachable")) != null) and (((.semantics.body.Native.regions[0].targets // []) | map(.value.status) | index("Unreachable")) != null)' +EOF_CMDS +RUN + +NAME=types_symbolic_xor_guard_materializes_deleted_function_by_addr +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sla.types | jq -c '.compiled_semantics.branches_pruned == 1 and (.compiled_semantics.region_count == 1) and (((.semantics.body.Native.regions[0].targets // []) | map(.value.status) | index("Reachable")) != null) and (((.semantics.body.Native.regions[0].targets // []) | map(.value.status) | index("Unreachable")) != null) and ((((.interproc.scope.payloads // []) | map(.function_name) | any(. == "dbg.helper_symbolic_zero_pure")) or (((.interproc.scope.seeds // []) | map(.name) | any(. == "sym.helper_symbolic_zero_pure")))))' +EOF_CMDS +RUN + +NAME=types_symbolic_helper_scope_materializes_deleted_root_and_helper +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sla.types sym.test_symbolic_helper_pure_guard | jq -c '.compiled_semantics.stage == "compiled" and .compiled_semantics.execution == "native" and (.compiled_semantics.derived_summaries >= 1) and (.compiled_semantics.helper_functions >= 1)' +EOF_CMDS +RUN + +NAME=types_symbolic_helper_scope_roundtrips_persisted_assumptions_key +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +af +a:sla.assumej [{"subject":{"register":{"name":"rdi"}},"value":{"type_hint":{"ty":"int32_t"}}}] >/dev/null +a:sla.types | jq -c 'tostring|contains("\"subject\":{\"register\":{\"name\":\"rdi\"}}") and contains("\"type_hint\":{\"ty\":\"int32_t\"}")' +EOF_CMDS +RUN + +NAME=types_symbolic_helper_scope_assumption_commands_roundtrip +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +af +a:sla.assumej [{"subject":{"register":{"name":"rdi"}},"value":{"type_hint":{"ty":"int32_t"}}}] >/dev/null +a:sla.assumptions | jq -c 'length==1 and .[0].subject.register.name=="rdi" and .[0].value.type_hint.ty=="int32_t"' +a:sla.assumptions- >/dev/null +a:sla.assumptions | jq -c 'type=="array" and length==0' +EOF_CMDS +RUN + +NAME=symbolic_helper_scope_reports_compiled_semantics +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sla.sym sym.test_symbolic_helper_pure_guard | jq -c '.semantic.stage == "compiled" and .semantic.execution == "native" and (.semantic.derived_summaries >= 1) and (.semantic.helper_functions >= 1)' +EOF_CMDS +RUN + +NAME=symbolic_summary_reports_compiled_condition_counts +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sla.sym sym.test_symbolic_xor_guard | jq -c '.semantic.region_count == 1 and (.semantic.control_region_count >= 1) and (.semantic.compiled_condition_count >= 1) and (.semantic.actionable_compiled_condition_count >= 1)' +EOF_CMDS +RUN + +NAME=dec_symbolic_xor_guard_prunes_dead_branch +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<