codevanta 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. codevanta/__init__.py +1 -0
  2. codevanta/analysis/__init__.py +1 -0
  3. codevanta/analysis/binary_diff.py +1168 -0
  4. codevanta/analysis/callgraph.py +542 -0
  5. codevanta/analysis/cfg.py +1201 -0
  6. codevanta/analysis/dataflow.py +762 -0
  7. codevanta/analysis/debugger.py +289 -0
  8. codevanta/analysis/debuginfo.py +304 -0
  9. codevanta/analysis/decompile.py +1094 -0
  10. codevanta/analysis/decompilers/__init__.py +250 -0
  11. codevanta/analysis/decompilers/angr_backend.py +251 -0
  12. codevanta/analysis/decompilers/base.py +278 -0
  13. codevanta/analysis/decompilers/ghidra_backend.py +316 -0
  14. codevanta/analysis/dynamic.py +756 -0
  15. codevanta/analysis/emulate.py +1349 -0
  16. codevanta/analysis/evidence_bundle.py +736 -0
  17. codevanta/analysis/function_hash.py +562 -0
  18. codevanta/analysis/graph_export.py +441 -0
  19. codevanta/analysis/html_view.py +695 -0
  20. codevanta/analysis/jump_tables.py +546 -0
  21. codevanta/analysis/solver.py +715 -0
  22. codevanta/analysis/symbolic.py +1809 -0
  23. codevanta/analysis/trace.py +646 -0
  24. codevanta/analysis/type_propagation.py +1055 -0
  25. codevanta/analysis/type_recovery.py +1214 -0
  26. codevanta/analysis/vtables.py +572 -0
  27. codevanta/analysis/xrefs.py +434 -0
  28. codevanta/api/__init__.py +1 -0
  29. codevanta/cli/__init__.py +1 -0
  30. codevanta/cli/main.py +3038 -0
  31. codevanta/core/__init__.py +31 -0
  32. codevanta/core/disassembler.py +121 -0
  33. codevanta/core/entropy.py +237 -0
  34. codevanta/core/functions.py +176 -0
  35. codevanta/core/loader.py +1351 -0
  36. codevanta/core/report.py +306 -0
  37. codevanta/core/strings.py +109 -0
  38. codevanta/evidence/__init__.py +7 -0
  39. codevanta/evidence/confidence.py +36 -0
  40. codevanta/evidence/models.py +43 -0
  41. codevanta/evidence/provenance.py +37 -0
  42. codevanta/gui/__init__.py +46 -0
  43. codevanta/gui/layout.py +1384 -0
  44. codevanta/gui/server.py +1305 -0
  45. codevanta/ir/__init__.py +6 -0
  46. codevanta/ir/adapters/__init__.py +31 -0
  47. codevanta/ir/base.py +31 -0
  48. codevanta/ir/instruction.py +42 -0
  49. codevanta/mcp_server/__init__.py +5 -0
  50. codevanta/mcp_server/server.py +710 -0
  51. codevanta/mcp_server/tools.py +1364 -0
  52. codevanta/plugins/__init__.py +748 -0
  53. codevanta/setup/__init__.py +28 -0
  54. codevanta/setup/apply.py +22 -0
  55. codevanta/setup/clients.py +285 -0
  56. codevanta/setup/plan.py +79 -0
  57. codevanta/setup/verify.py +43 -0
  58. codevanta/storage/__init__.py +1 -0
  59. codevanta/storage/session.py +514 -0
  60. codevanta-0.1.0.dist-info/METADATA +688 -0
  61. codevanta-0.1.0.dist-info/RECORD +65 -0
  62. codevanta-0.1.0.dist-info/WHEEL +5 -0
  63. codevanta-0.1.0.dist-info/entry_points.txt +2 -0
  64. codevanta-0.1.0.dist-info/licenses/LICENSE +21 -0
  65. codevanta-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,715 @@
1
+ """Solver-based symbolic execution for one function: concrete inputs per path.
2
+
3
+ The unicorn explorer (:mod:`codevanta.analysis.symbolic`) enumerates branch
4
+ decisions and replays them on a virtual CPU, deliberately solving nothing. This
5
+ module is the complement: it hands the function to `angr <https://angr.io/>`_,
6
+ which runs the code on *symbolic* states, forks at every conditional branch,
7
+ and accumulates one constraint per fork. Each reported path therefore carries
8
+ the *condition* that reaches it plus a concrete assignment of the six integer
9
+ argument registers that satisfies it (claripy solves the entry register
10
+ variables under the path's constraints). Nothing is executed on the host: angr
11
+ loads only the file bytes into its own simulation (``auto_load_libs`` is always
12
+ false), and hard caps bound the work.
13
+
14
+ Guarantees
15
+ ----------
16
+ * **Lazy.** angr is *optional* and imported inside :func:`explore_symbolic`,
17
+ never at module import time. :data:`SOLVER_AVAILABLE` only records whether a
18
+ module called ``angr`` is findable -- the same probe
19
+ :mod:`codevanta.analysis.symbolic` uses -- and :func:`solver_status` returns
20
+ the reason when it is absent. Without angr the function raises
21
+ :class:`~codevanta.core.report.AnalysisError` naming angr and the extra that
22
+ installs it. Only x86_64 is supported: the register vocabulary, the ABI seed
23
+ list and the capstone sweep are all 64-bit x86.
24
+ * **Bounded.** At most ``max_paths`` states stay active at any tick (extras are
25
+ dropped and the result says so), the run stops once ``max_steps`` executed
26
+ instructions are accounted for -- the simulation manager is stepped one
27
+ *instruction* per state per tick and every tick's instruction executions are
28
+ charged to the budget -- and at most ``max_paths`` paths are reported.
29
+ :attr:`SymbolicResult.truncated` is true exactly when a cap cut the
30
+ exploration short.
31
+ * **Honest.** Every path ends with one of three ``stopped_reason`` values:
32
+ ``return`` (a ``ret`` was reached), ``budget`` (the step budget ran out while
33
+ the state was still going) or ``error`` (the state faulted under angr). A
34
+ ``budget``/``error`` path reports ``return_value=None`` because it never
35
+ reached a ``ret`` -- a register snapshot would be dressed up as a return
36
+ value. And the evidence states the real limit of the method: the paths angr
37
+ found are real, but *finding every* path is not guaranteed.
38
+
39
+ Path semantics
40
+ --------------
41
+ The simulation manager is stepped tick by tick with ``find`` semantics, but the
42
+ ticks are applied here rather than delegated, so the instruction budget is
43
+ exact and the active-state cap is enforced between ticks. One tick executes one
44
+ instruction in every active state; afterwards every state whose instruction
45
+ pointer is (possibly, under its own constraints) a ``ret`` address is harvested
46
+ as a ``return`` path -- the same thing ``SimulationManager.explore`` does with
47
+ ``find``, with ``avoid`` being nothing. States that fault move to angr's
48
+ errored records and become ``error`` paths; states that land on a symbolic jump
49
+ or return target become *unconstrained*, cannot be enumerated this way, and are
50
+ counted in a warning. What remains stays active until a cap or the budget ends
51
+ it as a ``budget`` path.
52
+
53
+ The ``find`` set is the ``ret`` instructions of a bounded linear sweep from the
54
+ start address (the same sweep the angr bridge in ``symbolic`` uses), so a
55
+ ``ret`` the sweep cannot see is never treated as found and the sweep's limits
56
+ are written into the evidence.
57
+
58
+ Constraint and input extraction
59
+ -------------------------------
60
+ Every state starts from :meth:`angr.project.Project.factory.entry_state` with
61
+ each of the six SysV x86-64 integer argument registers seeded: concretely when
62
+ the caller passed ``initial_registers``, otherwise as a named symbolic variable
63
+ (``codevanta_initial_<register>``). At each stopped state the path's
64
+ ``constraints`` are ``state.solver.constraints`` stringified (capped at
65
+ :data:`_MAX_CONSTRAINTS` entries), and ``input_registers`` solves each entry
66
+ variable under *that state's* constraints -- so the values are concrete inputs
67
+ that genuinely reach the path (a path constrained to ``rdi == 0`` reports a
68
+ zero ``rdi``). ``return_value`` is one concrete solution for ``rax`` at the
69
+ ``ret``, not every value ``rax`` can take; the evidence says so.
70
+ """
71
+
72
+ from __future__ import annotations
73
+
74
+ import importlib.util
75
+ from collections.abc import Mapping
76
+ from dataclasses import dataclass
77
+ from pathlib import Path
78
+ from typing import Any
79
+
80
+ import capstone
81
+
82
+ from codevanta.core.loader import BinaryInfo
83
+ from codevanta.core.report import AnalysisError, read_code
84
+ from codevanta.evidence.confidence import Confidence
85
+ from codevanta.evidence.models import Evidence
86
+
87
+ __all__ = [
88
+ "DEFAULT_MAX_PATHS",
89
+ "DEFAULT_MAX_STEPS",
90
+ "MAX_MAX_PATHS",
91
+ "MAX_MAX_STEPS",
92
+ "SOLVER_AVAILABLE",
93
+ "SolverPath",
94
+ "SymbolicResult",
95
+ "explore_symbolic",
96
+ "solver_status",
97
+ ]
98
+
99
+ # ---- limits ---------------------------------------------------------------
100
+
101
+ #: Paths reported when the caller does not pass ``max_paths``.
102
+ DEFAULT_MAX_PATHS = 8
103
+
104
+ #: Hard ceiling on ``max_paths`` (bounded work, whatever the caller asks).
105
+ MAX_MAX_PATHS = 4096
106
+
107
+ #: Instruction budget used when the caller does not pass ``max_steps``.
108
+ DEFAULT_MAX_STEPS = 2000
109
+
110
+ #: Hard ceiling on ``max_steps``.
111
+ MAX_MAX_STEPS = 200_000
112
+
113
+ #: Instructions a linear sweep reads at most when collecting a function's
114
+ #: ``ret`` sites for the find set.
115
+ MAX_SCAN_INSTRUCTIONS = 4096
116
+
117
+ #: Solver constraints stringified per path before the rest are dropped.
118
+ _MAX_CONSTRAINTS = 32
119
+
120
+ #: Bytes read per disassembly step in the ``ret`` sweep.
121
+ _MAX_INSTRUCTION_BYTES = 16
122
+
123
+ #: Cap on the raw section copy angr falls back to when it cannot load the file.
124
+ _MAX_BLOB_BYTES = 1 << 20
125
+
126
+ #: Errored states described individually in warnings; the rest stay counted.
127
+ _MAX_ERROR_NOTES = 4
128
+
129
+ #: The six SysV x86-64 integer argument registers -- the only seed names this
130
+ #: module accepts, and the registers solved concretely for every path.
131
+ _ARG_REGISTERS_64 = ("rdi", "rsi", "rdx", "rcx", "r8", "r9")
132
+
133
+ #: ``BinaryInfo.architecture`` spellings accepted as 64-bit x86.
134
+ _X86_64_NAMES = frozenset({"x86_64", "x86-64", "amd64", "em_amd64"})
135
+
136
+ #: Instruction mnemonics that leave the explored function (Capstone's naming).
137
+ _RET_MNEMONICS = frozenset({"ret", "retf", "retn", "iret", "iretd", "iretq"})
138
+
139
+ # ---- evidence identity ------------------------------------------------------
140
+
141
+ #: Evidence identity for the solver-based explorer.
142
+ _EVIDENCE_SOURCE = "solver.angr"
143
+ _EVIDENCE_METHOD = "solver_symbolic_execution"
144
+
145
+ #: The constraint solver behind claripy on every angr this module supports.
146
+ _SOLVER_NAME = "z3"
147
+
148
+ #: What a caller needs to install angr.
149
+ _ANGR_HINT = "pip install 'codevanta[decompilers]'"
150
+
151
+ #: Every AnalysisError from this module names angr and the way to install it.
152
+ _ANGR_SUFFIX = f"angr is required for solver-based exploration: {_ANGR_HINT}"
153
+
154
+ #: What solver-based exploration proved versus what it only reports.
155
+ _ASSUMPTIONS: tuple[str, ...] = (
156
+ "solver-based exploration; concrete inputs extracted per path; no proof all paths were found",
157
+ "each path carries the constraints angr accumulated up to its end, stringified and capped at "
158
+ f"{_MAX_CONSTRAINTS} entries",
159
+ "return_value is one concrete solution for rax, not every value rax can take on that path",
160
+ "the find set is the ret instructions of a bounded linear sweep; a ret the sweep cannot see is "
161
+ "never treated as found, so a path can end as 'budget' although a ret exists ahead of it",
162
+ "input_registers solve the entry values of the six integer argument registers under the "
163
+ "path's constraints; the code may overwrite them before it returns",
164
+ )
165
+
166
+ # ---- stop reasons -----------------------------------------------------------
167
+
168
+ #: Why one path ended. ``return``: the state reached a ``ret``; ``budget``: the
169
+ #: step budget ran out while the state was still going; ``error``: the state
170
+ #: faulted under angr.
171
+ _STOP_RETURN = "return"
172
+ _STOP_BUDGET = "budget"
173
+ _STOP_ERROR = "error"
174
+
175
+ # ---- availability -----------------------------------------------------------
176
+
177
+
178
+ def _probe_angr() -> bool:
179
+ """True when a module called ``angr`` is findable; never imports angr."""
180
+ try:
181
+ return importlib.util.find_spec("angr") is not None
182
+ except Exception: # pragma: no cover - depends on the installation
183
+ return False
184
+
185
+
186
+ #: True when a module named ``angr`` is findable. This is a *probe*, not an
187
+ #: import: angr is imported only inside :func:`explore_symbolic`, so importing
188
+ #: this module stays cheap and keeps working on a machine without angr
189
+ #: (mirrors :data:`codevanta.analysis.symbolic.ANGR_AVAILABLE`).
190
+ SOLVER_AVAILABLE = _probe_angr()
191
+
192
+
193
+ def solver_status() -> str:
194
+ """Return why solver-based exploration is (un)available on this machine.
195
+
196
+ A sentence either way: with angr installed it says so; without, it is the
197
+ exact reason, naming angr and the extra that installs it.
198
+ """
199
+ if SOLVER_AVAILABLE:
200
+ return (
201
+ "ok: 'angr' is findable, so explore_symbolic can run solver-based "
202
+ f"symbolic execution (claripy/{_SOLVER_NAME})"
203
+ )
204
+ return (
205
+ "'angr' is not installed, so explore_symbolic cannot run: solver-based exploration is "
206
+ "optional in CODEVANTA and angr is never imported at module import time; install it "
207
+ f"with {_ANGR_HINT}"
208
+ )
209
+
210
+
211
+ def _import_angr() -> Any:
212
+ """Import angr lazily, or raise :class:`AnalysisError` naming it and the extra."""
213
+ if not SOLVER_AVAILABLE:
214
+ raise AnalysisError(
215
+ "'angr' is not installed, so explore_symbolic cannot run: solver-based exploration "
216
+ "is optional in CODEVANTA and angr is never imported at module import time; "
217
+ f"install it with {_ANGR_HINT}"
218
+ )
219
+ try:
220
+ import angr
221
+ except Exception as exc:
222
+ raise AnalysisError(
223
+ f"importing 'angr' failed ({type(exc).__name__}: {exc}); reinstall it with {_ANGR_HINT}"
224
+ ) from exc
225
+ return angr
226
+
227
+
228
+ # ---- results ---------------------------------------------------------------
229
+
230
+
231
+ @dataclass(frozen=True)
232
+ class SolverPath:
233
+ """One path the solver explored, with the concrete inputs that reach it.
234
+
235
+ Attributes:
236
+ return_value: One concrete solution for ``rax`` at the ``ret`` -- or
237
+ ``None`` when the path never reached a ``ret``, because a register
238
+ snapshot must not be dressed up as a return value.
239
+ constraints: The solver constraints angr accumulated up to the path's
240
+ end, stringified, capped at :data:`_MAX_CONSTRAINTS` entries.
241
+ input_registers: Concrete entry values of the six integer argument
242
+ registers, solved under *this path's* constraints, so the values
243
+ genuinely reach the path.
244
+ stopped_reason: ``"return"``, ``"budget"`` or ``"error"``.
245
+ steps: Instructions executed along this path: one per tick angr stepped
246
+ it, the ``ret`` itself never executed.
247
+ """
248
+
249
+ return_value: int | None
250
+ constraints: tuple[str, ...]
251
+ input_registers: Mapping[str, int]
252
+ stopped_reason: str
253
+ steps: int
254
+
255
+ def to_dict(self) -> dict[str, Any]:
256
+ """Serialize to a JSON-safe dict (hex values, lists, no solver objects)."""
257
+ return {
258
+ "return_value": (
259
+ f"0x{self.return_value:x}" if self.return_value is not None else None
260
+ ),
261
+ "constraints": list(self.constraints),
262
+ "input_registers": {
263
+ name: f"0x{value:x}" for name, value in sorted(self.input_registers.items())
264
+ },
265
+ "stopped_reason": self.stopped_reason,
266
+ "steps": self.steps,
267
+ }
268
+
269
+
270
+ @dataclass(frozen=True)
271
+ class SymbolicResult:
272
+ """Every path one solver-based exploration produced, plus its limits.
273
+
274
+ Attributes:
275
+ address: Virtual address the exploration started at.
276
+ paths: The paths, in the order angr's solver stopped on them.
277
+ solver: Which constraint solver produced the concrete models (``z3``).
278
+ truncated: ``True`` exactly when a cap -- the path cap, the
279
+ active-state cap or the step budget -- cut the exploration short.
280
+ warnings: What was left unexplored, dropped or unconstrained.
281
+ evidence: Provenance for the whole exploration, including what the
282
+ solver proved and what it only reports.
283
+ """
284
+
285
+ address: int
286
+ paths: tuple[SolverPath, ...]
287
+ solver: str
288
+ truncated: bool
289
+ warnings: tuple[str, ...]
290
+ evidence: Evidence
291
+
292
+ def to_dict(self) -> dict[str, Any]:
293
+ """Serialize to a JSON-safe dict (hex address, lists, no enums)."""
294
+ return {
295
+ "address": f"0x{self.address:x}",
296
+ "solver": self.solver,
297
+ "truncated": self.truncated,
298
+ "path_count": len(self.paths),
299
+ "paths": [entry.to_dict() for entry in self.paths],
300
+ "warnings": list(self.warnings),
301
+ "evidence": self.evidence.to_dict(),
302
+ }
303
+
304
+
305
+ # ---- small helpers ---------------------------------------------------------
306
+
307
+
308
+ def _validated_address(address: int) -> int:
309
+ """Reject a negative start address; everything else is the loader's job."""
310
+ if address < 0:
311
+ raise AnalysisError(f"address must be non-negative, got {address}; {_ANGR_SUFFIX}")
312
+ return address
313
+
314
+
315
+ def _validated_limit(name: str, value: int, ceiling: int) -> int:
316
+ """Validate one caller-supplied budget: at least 1, at most ``ceiling``."""
317
+ if value < 1:
318
+ raise AnalysisError(f"{name} must be at least 1, got {value}; {_ANGR_SUFFIX}")
319
+ if value > ceiling:
320
+ raise AnalysisError(f"{name} too large ({value}); limit is {ceiling}; {_ANGR_SUFFIX}")
321
+ return value
322
+
323
+
324
+ def _validated_registers(initial_registers: Mapping[str, int] | None) -> dict[str, int]:
325
+ """Validate concrete seeds: only the six ABI registers, ints in [0, 2**64)."""
326
+ if initial_registers is None:
327
+ return {}
328
+ seeds: dict[str, int] = {}
329
+ for name, value in initial_registers.items():
330
+ if name not in _ARG_REGISTERS_64:
331
+ raise AnalysisError(
332
+ f"unknown seed register {name!r}: explore_symbolic seeds only the six x86-64 "
333
+ f"integer argument registers {list(_ARG_REGISTERS_64)}; {_ANGR_SUFFIX}"
334
+ )
335
+ if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value < (1 << 64):
336
+ raise AnalysisError(
337
+ f"seed value for {name!r} must be an int in [0, 2**64), got {value!r}; "
338
+ f"{_ANGR_SUFFIX}"
339
+ )
340
+ seeds[name] = value
341
+ return seeds
342
+
343
+
344
+ def _is_x86_64(info: BinaryInfo) -> bool:
345
+ """True when the binary is 64-bit x86, the only architecture this module runs."""
346
+ return info.bitness == 64 and str(info.architecture).lower() in _X86_64_NAMES
347
+
348
+
349
+ def _ret_addresses(target: Path, info: BinaryInfo, start: int, section: Any) -> tuple[int, ...]:
350
+ """Collect ``ret`` addresses in ``section`` by a bounded linear sweep.
351
+
352
+ The sweep only builds the find set, so a byte that cannot be decoded costs
353
+ one resynchronisation step rather than a wrong target -- the same approach
354
+ the angr bridge in :mod:`codevanta.analysis.symbolic` takes.
355
+ """
356
+ decoder = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64)
357
+ found: list[int] = []
358
+ end = int(section.virtual_address) + int(section.size)
359
+ pc = start
360
+ for _ in range(MAX_SCAN_INSTRUCTIONS):
361
+ if pc >= end:
362
+ break
363
+ try:
364
+ data = read_code(target, info, pc, _MAX_INSTRUCTION_BYTES)
365
+ except AnalysisError:
366
+ break
367
+ insn = next(iter(decoder.disasm(bytes(data), pc)), None)
368
+ if insn is None:
369
+ pc += 1
370
+ continue
371
+ if str(insn.mnemonic).lower() in _RET_MNEMONICS:
372
+ found.append(pc)
373
+ pc += int(insn.size)
374
+ return tuple(found)
375
+
376
+
377
+ def _build_project(module: Any, target: Path, info: BinaryInfo, section: Any) -> tuple[Any, list[str]]:
378
+ """Create the angr project, falling back to a raw section copy if needed.
379
+
380
+ The file is loaded as an executable first (full fidelity, ``auto_load_libs``
381
+ always false). Hand-written ELFs without program headers can defeat angr's
382
+ loader, so on failure the module retries with the section's bytes mapped as
383
+ a raw blob at the section's virtual address -- and says so in a warning.
384
+ """
385
+ warnings: list[str] = []
386
+ try:
387
+ return module.Project(str(target), auto_load_libs=False), warnings
388
+ except Exception as exc:
389
+ warnings.append(
390
+ f"angr could not load {target.name} as an executable ({type(exc).__name__}: {exc}); "
391
+ f"exploring a raw copy of section {section.name!r} instead"
392
+ )
393
+ base = int(section.virtual_address)
394
+ size = min(int(section.size), _MAX_BLOB_BYTES)
395
+ if size <= 0:
396
+ raise AnalysisError(f"section {section.name!r} has no bytes to map for angr; {_ANGR_SUFFIX}")
397
+ try:
398
+ blob = read_code(target, info, base, size)
399
+ except AnalysisError as exc:
400
+ raise AnalysisError(
401
+ f"could not read the bytes of section {section.name!r} for the angr blob fallback "
402
+ f"({exc}); {_ANGR_SUFFIX}"
403
+ ) from exc
404
+ try:
405
+ project = module.Project(
406
+ blob,
407
+ main_kwargs={"backend": "blob", "arch": "AMD64", "base_addr": base},
408
+ auto_load_libs=False,
409
+ )
410
+ except Exception as exc:
411
+ raise AnalysisError(
412
+ f"creating the angr project for {target.name} failed ({type(exc).__name__}: {exc}); "
413
+ f"{_ANGR_SUFFIX}"
414
+ ) from exc
415
+ if size < int(section.size):
416
+ warnings.append(
417
+ f"only the first {size} byte(s) of section {section.name!r} were mapped for angr"
418
+ )
419
+ return project, warnings
420
+
421
+
422
+ def _seed_registers(module: Any, state: Any, seeds: Mapping[str, int]) -> dict[str, Any]:
423
+ """Seed the entry state; return, per register, what to solve per path.
424
+
425
+ Registers the caller seeded concretely keep their int; the rest get a named
426
+ symbolic variable (``codevanta_initial_<register>``) so every stopped state
427
+ can solve for a concrete entry value under its own constraints.
428
+ """
429
+ entry_values: dict[str, Any] = {}
430
+ for name in _ARG_REGISTERS_64:
431
+ if name in seeds:
432
+ entry_values[name] = int(seeds[name])
433
+ setattr(state.regs, name, entry_values[name])
434
+ else:
435
+ entry_values[name] = module.claripy.BVS(f"codevanta_initial_{name}", 64)
436
+ setattr(state.regs, name, entry_values[name])
437
+ return entry_values
438
+
439
+
440
+ def _at_ret(state: Any, find_set: frozenset[int]) -> bool:
441
+ """True when reaching a ``ret`` is possible for ``state`` (find semantics).
442
+
443
+ Concrete instruction pointers are compared directly; a symbolic pointer
444
+ counts when one of its first two solutions is a ``ret`` address, which is
445
+ how ``SimulationManager.explore`` treats a find set.
446
+ """
447
+ try:
448
+ values = state.solver.eval_upto(state.ip, 2)
449
+ except Exception: # pragma: no cover - angr owns the solver vocabulary
450
+ return False
451
+ return any(int(value) in find_set for value in values)
452
+
453
+
454
+ def _path_from_state(
455
+ state: Any, stopped_reason: str, steps: int, entry_values: Mapping[str, Any]
456
+ ) -> SolverPath:
457
+ """Extract one :class:`SolverPath` from a state angr stopped on.
458
+
459
+ ``constraints`` stringifies ``state.solver.constraints`` (capped);
460
+ ``input_registers`` solves each entry register under this state's solver, so
461
+ the concrete values satisfy the path's constraints; ``return_value`` is one
462
+ concrete solution for ``rax``, and only for ``return`` paths.
463
+ """
464
+ constraints = tuple(str(item) for item in tuple(state.solver.constraints)[:_MAX_CONSTRAINTS])
465
+ inputs: dict[str, int] = {}
466
+ for name, entry in entry_values.items():
467
+ if isinstance(entry, int):
468
+ inputs[name] = entry
469
+ continue
470
+ try:
471
+ inputs[name] = int(state.solver.eval(entry))
472
+ except Exception: # pragma: no cover - angr owns the solver vocabulary
473
+ continue
474
+ return_value: int | None = None
475
+ if stopped_reason == _STOP_RETURN:
476
+ try:
477
+ return_value = int(state.solver.eval(state.regs.rax))
478
+ except Exception: # pragma: no cover - angr owns the solver vocabulary
479
+ return_value = None
480
+ return SolverPath(
481
+ return_value=return_value,
482
+ constraints=constraints,
483
+ input_registers=inputs,
484
+ stopped_reason=stopped_reason,
485
+ steps=steps,
486
+ )
487
+
488
+
489
+ def _result_evidence(
490
+ *, start: int, paths: list[SolverPath], truncated: bool, max_paths: int, max_steps: int
491
+ ) -> Evidence:
492
+ """Describe what the solver explored versus what it only reports."""
493
+ stopped: dict[str, int] = {}
494
+ for entry in paths:
495
+ stopped[entry.stopped_reason] = stopped.get(entry.stopped_reason, 0) + 1
496
+ return Evidence(
497
+ source=_EVIDENCE_SOURCE,
498
+ method=_EVIDENCE_METHOD,
499
+ confidence=Confidence.MEDIUM,
500
+ address=start,
501
+ detail=(
502
+ "solver-based exploration; concrete inputs extracted per path; no proof all paths "
503
+ "were found"
504
+ ),
505
+ extra={
506
+ "engine": f"angr/claripy with the {_SOLVER_NAME} solver",
507
+ "paths": len(paths),
508
+ "truncated": truncated,
509
+ "max_paths": max_paths,
510
+ "max_steps": max_steps,
511
+ "stopped_reasons": stopped,
512
+ "seed_registers": list(_ARG_REGISTERS_64),
513
+ "assumptions": list(_ASSUMPTIONS),
514
+ },
515
+ )
516
+
517
+
518
+ # ---- the explorer ----------------------------------------------------------
519
+
520
+
521
+ def explore_symbolic(
522
+ path: str | Path,
523
+ info: BinaryInfo,
524
+ address: int,
525
+ max_paths: int = DEFAULT_MAX_PATHS,
526
+ max_steps: int = DEFAULT_MAX_STEPS,
527
+ initial_registers: Mapping[str, int] | None = None,
528
+ ) -> SymbolicResult:
529
+ """Explore one function with solver-based symbolic execution (angr).
530
+
531
+ angr is imported here, never at module import time, and only x86_64 is
532
+ supported. The simulation manager is stepped one instruction per state per
533
+ tick with ``find`` semantics -- a state whose instruction pointer is
534
+ (possibly) a ``ret`` address becomes a path -- and ``avoid`` is nothing.
535
+ At most ``max_paths`` states stay active at any tick, the instruction
536
+ executions of every tick are charged against ``max_steps``, and every path
537
+ carries its solver constraints plus concrete argument-register values that
538
+ reach it.
539
+
540
+ Args:
541
+ path: Binary file.
542
+ info: Loader metadata for that file.
543
+ address: Start address of the function.
544
+ max_paths: Cap on reported paths *and* on concurrently active states.
545
+ max_steps: Instruction budget, charged per executed instruction.
546
+ initial_registers: Concrete seeds for up to six ABI argument registers
547
+ (``rdi``, ``rsi``, ``rdx``, ``rcx``, ``r8``, ``r9``), each an int
548
+ in ``[0, 2**64)``. Unseeded registers become symbolic variables.
549
+
550
+ Returns:
551
+ A :class:`SymbolicResult`; ``truncated`` is true when a cap cut the
552
+ exploration short.
553
+
554
+ Raises:
555
+ AnalysisError: When angr is not installed (the message names angr and
556
+ the extra that installs it), when angr fails to load or explore the
557
+ file, when the machine is not x86_64, on a bad address, seed or
558
+ limit, or when the address is not in an executable section.
559
+ """
560
+ target = Path(path)
561
+ start = _validated_address(address)
562
+ path_limit = _validated_limit("max_paths", max_paths, MAX_MAX_PATHS)
563
+ step_budget = _validated_limit("max_steps", max_steps, MAX_MAX_STEPS)
564
+ if not _is_x86_64(info):
565
+ raise AnalysisError(
566
+ "solver-based symbolic execution supports x86_64 only: "
567
+ f"{info.architecture!r} (bitness {info.bitness}) is not supported, so nothing was "
568
+ f"explored; {_ANGR_SUFFIX}"
569
+ )
570
+ seeds = _validated_registers(initial_registers)
571
+ module = _import_angr()
572
+ section = info.section_for_va(start)
573
+ if section is None:
574
+ raise AnalysisError(
575
+ f"address 0x{start:x} is not mapped by any section, so angr has nothing to explore; "
576
+ f"{_ANGR_SUFFIX}"
577
+ )
578
+ if not section.is_executable:
579
+ raise AnalysisError(
580
+ f"address 0x{start:x} is inside section {section.name!r}, which is not executable, "
581
+ f"so angr cannot execute it; {_ANGR_SUFFIX}"
582
+ )
583
+ find_set = frozenset(_ret_addresses(target, info, start, section))
584
+ if not find_set:
585
+ raise AnalysisError(
586
+ f"no ret instruction was found scanning section {section.name!r} from 0x{start:x}, "
587
+ f"so explore_symbolic has nothing to use as find; {_ANGR_SUFFIX}"
588
+ )
589
+ project, notes = _build_project(module, target, info, section)
590
+ try:
591
+ state = project.factory.entry_state(addr=start)
592
+ entry_values = _seed_registers(module, state, seeds)
593
+ simgr = project.factory.simgr(state)
594
+ except AnalysisError: # pragma: no cover - nothing above raises this
595
+ raise
596
+ except Exception as exc:
597
+ raise AnalysisError(
598
+ f"angr could not prepare an entry state at 0x{start:x} in {target.name} "
599
+ f"({type(exc).__name__}: {exc}); {_ANGR_SUFFIX}"
600
+ ) from exc
601
+
602
+ paths: list[SolverPath] = []
603
+ truncated = False
604
+ steps_used = 0
605
+ tick = 0
606
+ error_cursor = 0
607
+ dropped_found = 0
608
+ dropped_errors = 0
609
+ error_notes: list[str] = []
610
+ unconstrained_before = len(simgr.stashes.get("unconstrained", ()))
611
+ try:
612
+ while simgr.active:
613
+ if len(paths) >= path_limit:
614
+ truncated = True
615
+ notes.append(
616
+ f"path cap reached (max_paths={path_limit}) with {len(simgr.active)} "
617
+ "state(s) still unexplored; those paths are not reported"
618
+ )
619
+ break
620
+ if steps_used >= step_budget:
621
+ truncated = True
622
+ take = min(len(simgr.active), path_limit - len(paths))
623
+ for stopped_state in simgr.active[:take]:
624
+ paths.append(
625
+ _path_from_state(stopped_state, _STOP_BUDGET, tick, entry_values)
626
+ )
627
+ notes.append(
628
+ f"step budget ran out (max_steps={step_budget}) after {tick} tick(s) with "
629
+ f"{len(simgr.active)} state(s) still active; they are reported as "
630
+ "'budget' paths and never reached a ret"
631
+ )
632
+ if len(simgr.active) > take:
633
+ notes.append(
634
+ f"{len(simgr.active) - take} budget-stopped state(s) were not reported "
635
+ f"because the path cap ({path_limit}) was hit"
636
+ )
637
+ break
638
+ if len(simgr.active) > path_limit:
639
+ dropped = len(simgr.active) - path_limit
640
+ truncated = True
641
+ simgr.stashes["active"] = simgr.stashes["active"][:path_limit]
642
+ notes.append(
643
+ f"the active-state cap (max_paths={path_limit}) dropped {dropped} state(s) "
644
+ "to bound memory; those paths were not explored"
645
+ )
646
+ running = len(simgr.active)
647
+ tick += 1
648
+ try:
649
+ simgr.step(num_inst=1)
650
+ except TypeError: # pragma: no cover - angr version drift
651
+ simgr.step() # one basic block per tick; the budget still bounds the run
652
+ steps_used += running
653
+ still_active: list[Any] = []
654
+ for found_state in simgr.active:
655
+ if _at_ret(found_state, find_set):
656
+ if len(paths) < path_limit:
657
+ paths.append(
658
+ _path_from_state(found_state, _STOP_RETURN, tick, entry_values)
659
+ )
660
+ else:
661
+ dropped_found += 1
662
+ truncated = True
663
+ else:
664
+ still_active.append(found_state)
665
+ simgr.stashes["active"] = still_active
666
+ new_errors = simgr.errored[error_cursor:]
667
+ error_cursor = len(simgr.errored)
668
+ for record in new_errors:
669
+ if len(paths) < path_limit:
670
+ paths.append(_path_from_state(record.state, _STOP_ERROR, tick, entry_values))
671
+ else:
672
+ dropped_errors += 1
673
+ truncated = True
674
+ if len(error_notes) < _MAX_ERROR_NOTES:
675
+ error_notes.append(f"{type(record.error).__name__}: {record.error}")
676
+ except AnalysisError: # pragma: no cover - nothing above raises this
677
+ raise
678
+ except Exception as exc:
679
+ raise AnalysisError(
680
+ f"angr exploration of 0x{start:x} in {target.name} failed after {steps_used} "
681
+ f"executed instruction(s) ({type(exc).__name__}: {exc}); {_ANGR_SUFFIX}"
682
+ ) from exc
683
+
684
+ unconstrained_delta = len(simgr.stashes.get("unconstrained", ())) - unconstrained_before
685
+ if unconstrained_delta > 0:
686
+ notes.append(
687
+ f"{unconstrained_delta} state(s) became unconstrained (a symbolic jump or return "
688
+ "target angr cannot enumerate here) and are not reported"
689
+ )
690
+ if dropped_found:
691
+ notes.append(
692
+ f"{dropped_found} found path(s) were dropped because the path cap "
693
+ f"(max_paths={path_limit}) was reached"
694
+ )
695
+ if dropped_errors:
696
+ notes.append(
697
+ f"{dropped_errors} errored state(s) were not reported because the path cap "
698
+ f"(max_paths={path_limit}) was reached"
699
+ )
700
+ for note in error_notes[:_MAX_ERROR_NOTES]:
701
+ notes.append(f"a state errored ({note}); that path is reported with stopped_reason 'error'")
702
+ if not paths and not truncated and not notes:
703
+ notes.append(
704
+ "angr stopped without finding a ret, an error or an active state; nothing to report"
705
+ )
706
+ return SymbolicResult(
707
+ address=start,
708
+ paths=tuple(paths),
709
+ solver=_SOLVER_NAME,
710
+ truncated=truncated,
711
+ warnings=tuple(notes),
712
+ evidence=_result_evidence(
713
+ start=start, paths=paths, truncated=truncated, max_paths=path_limit, max_steps=step_budget
714
+ ),
715
+ )