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,762 @@
1
+ """Definition-use chains, constant propagation and reaching definitions (phase 6).
2
+
3
+ This module turns the phase 2 control-flow graph back into per-instruction data
4
+ flow: which register a definition writes, which constant (if any) it holds, which
5
+ uses read that register, and which definition reached each read. It is the
6
+ foundation for the typed IR step, because a type can only be attached to a value
7
+ once the value's origin is known.
8
+
9
+ The analysis is a *single forward pass*: blocks are visited in ascending address
10
+ order and the instructions of a block in ascending address order. The register
11
+ file is a map from canonical register name to the :class:`Definition` that
12
+ currently holds the value, so a later write *kills* the earlier definition --
13
+ only the most recent definition can be reached by a later read, and the summary
14
+ records at most that one per read in ``reaching_definition``.
15
+
16
+ Folding rules (a folded value is always masked to the width of the written
17
+ register, and constants are computed exactly, never heuristically)::
18
+
19
+ instruction definition kind folded value
20
+ ---------------------------------- ---------------- ---------------------
21
+ mov rax, 5 const 5
22
+ mov eax, 5 const 5 (zero-extends)
23
+ movabs rax, 0x1122334455667788 const that immediate
24
+ mov rax, rdi copy rdi's constant, if any
25
+ lea rax, [rip + 0x10] const next instruction + 0x10
26
+ add rax, 3 (rax known) arithmetic rax + 3
27
+ sub rax, 3 (rax known) arithmetic rax - 3
28
+ xor rax, 3 (rax known) arithmetic rax ^ 3
29
+ xor rax, rax / sub rax, rax arithmetic 0
30
+ add rax, rbx (both known) arithmetic rax + rbx
31
+ any other register write unknown none
32
+
33
+ Everything else stays ``unknown`` on purpose. ``movzx``/``movsx``/``movsxd`` are
34
+ never classed as ``copy`` because the extension changes the value; ``and``, ``or``
35
+ and the shifts keep their ``arithmetic`` kind but no value, and a write narrower
36
+ than four bytes (``al``, ``ax``) drops its value while keeping its kind, because a
37
+ partial write cannot leave a whole-register constant. ``lea`` is only folded when
38
+ the address expression is a RIP-relative displacement; an indexed or
39
+ base-relative ``lea`` is an unknown address.
40
+
41
+ Register names are canonicalized before they are reported: on a 64-bit binary
42
+ ``eax``/``ax``/``al`` are aliases of ``rax``, on a 32-bit one ``ax``/``al`` are
43
+ aliases of ``eax``. A read of a narrow alias is a read of the canonical register,
44
+ but a constant is only propagated through it when the alias is the whole register,
45
+ because the upper bytes of the canonical register are not visible through it.
46
+
47
+ Two instruction shapes are deliberately *not* definitions: a memory destination
48
+ (``mov [rdi], rax`` writes memory, not a register -- its address registers are
49
+ reads) and any mnemonic that does not write its first operand (``cmp``, ``test``,
50
+ ``push``, ``jcc``, ``ret``, ...). A ``call`` additionally invalidates the
51
+ caller-saved registers (``rax``, ``rcx``, ``rdx``, ``rsi``, ``rdi``, ``r8`` ..
52
+ ``r11`` and their 32-bit forms), because whatever the callee left there is not
53
+ the constant the caller stored.
54
+
55
+ Every read whose register has no definition in the analysed region is reported
56
+ through ``DataFlowSummary.warnings`` -- bounded and never silent -- and simply has
57
+ no entry in ``reaching_definition``. A truncated CFG is reported the same way,
58
+ because definitions past the instruction or block limit are not modelled at all.
59
+
60
+ Nothing is executed: decoding comes from
61
+ :func:`codevanta.analysis.cfg.build_recursive_cfg`, operand parsing is shared with
62
+ :mod:`codevanta.analysis.type_propagation`, and the result is a pure function of
63
+ the file bytes, the address and the two limits.
64
+ """
65
+
66
+ from __future__ import annotations
67
+
68
+ import re
69
+ from collections.abc import Mapping, Sequence
70
+ from dataclasses import dataclass
71
+ from pathlib import Path
72
+ from typing import Any
73
+
74
+ from codevanta.analysis.cfg import build_recursive_cfg
75
+ from codevanta.analysis.type_propagation import (
76
+ _ADDRESS_TERM,
77
+ _ARITHMETIC_MNEMONICS,
78
+ _MOV_MNEMONICS,
79
+ _WORD,
80
+ _instruction_parts,
81
+ _is_plain_register,
82
+ _parse_memory,
83
+ _split_operands,
84
+ _writes_first_operand,
85
+ )
86
+ from codevanta.core.loader import BinaryInfo
87
+ from codevanta.core.report import AnalysisError
88
+ from codevanta.evidence.confidence import Confidence
89
+ from codevanta.evidence.models import Evidence
90
+
91
+ __all__ = [
92
+ "DEFAULT_MAX_BLOCKS",
93
+ "DEFAULT_MAX_INSTRUCTIONS",
94
+ "KIND_ARITHMETIC",
95
+ "KIND_CONST",
96
+ "KIND_COPY",
97
+ "KIND_UNKNOWN",
98
+ "MAX_MAX_BLOCKS",
99
+ "MAX_MAX_INSTRUCTIONS",
100
+ "MAX_UNDEFINED_WARNINGS",
101
+ "MIN_MAX_BLOCKS",
102
+ "MIN_MAX_INSTRUCTIONS",
103
+ "READ",
104
+ "WRITE",
105
+ "DataFlowSummary",
106
+ "Definition",
107
+ "Use",
108
+ "analyze_dataflow",
109
+ ]
110
+
111
+
112
+ #: Default instruction budget of one analysis.
113
+ DEFAULT_MAX_INSTRUCTIONS = 2000
114
+
115
+ #: Default ceiling on the number of blocks one analysis will visit.
116
+ DEFAULT_MAX_BLOCKS = 500
117
+
118
+ #: Smallest accepted ``max_instructions`` / ``max_blocks``.
119
+ MIN_MAX_INSTRUCTIONS = 1
120
+ MIN_MAX_BLOCKS = 1
121
+
122
+ #: Hard ceilings on the budgets (bounded work per call).
123
+ MAX_MAX_INSTRUCTIONS = 8192
124
+ MAX_MAX_BLOCKS = 4096
125
+
126
+ #: ``Definition.kind``: the instruction writes an immediate or a RIP-relative
127
+ #: address, so the value is known exactly.
128
+ KIND_CONST = "const"
129
+
130
+ #: ``Definition.kind``: the instruction copies another register.
131
+ KIND_COPY = "copy"
132
+
133
+ #: ``Definition.kind``: the instruction combines its operand with the current
134
+ #: value of the register (a folded value is present only when both are known).
135
+ KIND_ARITHMETIC = "arithmetic"
136
+
137
+ #: ``Definition.kind``: the write is not modelled, so its value is unknown.
138
+ KIND_UNKNOWN = "unknown"
139
+
140
+ #: ``Use.read_or_write``: the instruction reads the register.
141
+ READ = "read"
142
+
143
+ #: ``Use.read_or_write``: the instruction writes the register.
144
+ WRITE = "write"
145
+
146
+ #: How many "no reaching definition" warnings one summary may carry before the
147
+ #: rest are collapsed into a single count (bounded, deterministic prose).
148
+ MAX_UNDEFINED_WARNINGS = 8
149
+
150
+ #: A write of this many bytes or more replaces the whole canonical register
151
+ #: (``eax`` zero-extends into ``rax``), so it may leave a known constant.
152
+ FULL_CONSTANT_WIDTH = 4
153
+
154
+ #: Mnemonics whose value is folded when both sides are known.
155
+ _FOLDING_MNEMONICS = frozenset({"add", "sub", "xor"})
156
+
157
+ #: ``mov``-family mnemonics that change the value even when copying a register.
158
+ _EXTENDING_MNEMONICS = frozenset({"movsx", "movsxd", "movzx"})
159
+
160
+ #: An immediate as Capstone or a MASM-style printer renders it: ``5``, ``-1``,
161
+ #: ``0x10``, ``0ffh``.
162
+ _IMMEDIATE = re.compile(r"^(?P<sign>[+-]?)(?P<body>0x[0-9a-f]+|[0-9]+)(?P<suffix>h?)$", re.IGNORECASE)
163
+
164
+ #: Canonical 64-bit general-purpose registers -> {alias as printed: width}.
165
+ _ALIAS_WIDTHS: dict[str, dict[str, int]] = {
166
+ "rax": {"rax": 8, "eax": 4, "ax": 2, "al": 1, "ah": 1},
167
+ "rbx": {"rbx": 8, "ebx": 4, "bx": 2, "bl": 1, "bh": 1},
168
+ "rcx": {"rcx": 8, "ecx": 4, "cx": 2, "cl": 1, "ch": 1},
169
+ "rdx": {"rdx": 8, "edx": 4, "dx": 2, "dl": 1, "dh": 1},
170
+ "rsi": {"rsi": 8, "esi": 4, "si": 2, "sil": 1},
171
+ "rdi": {"rdi": 8, "edi": 4, "di": 2, "dil": 1},
172
+ "rbp": {"rbp": 8, "ebp": 4, "bp": 2, "bpl": 1},
173
+ "rsp": {"rsp": 8, "esp": 4, "sp": 2, "spl": 1},
174
+ "r8": {"r8": 8, "r8d": 4, "r8w": 2, "r8b": 1},
175
+ "r9": {"r9": 8, "r9d": 4, "r9w": 2, "r9b": 1},
176
+ "r10": {"r10": 8, "r10d": 4, "r10w": 2, "r10b": 1},
177
+ "r11": {"r11": 8, "r11d": 4, "r11w": 2, "r11b": 1},
178
+ "r12": {"r12": 8, "r12d": 4, "r12w": 2, "r12b": 1},
179
+ "r13": {"r13": 8, "r13d": 4, "r13w": 2, "r13b": 1},
180
+ "r14": {"r14": 8, "r14d": 4, "r14w": 2, "r14b": 1},
181
+ "r15": {"r15": 8, "r15d": 4, "r15w": 2, "r15b": 1},
182
+ }
183
+
184
+ #: Registers a ``call`` invalidates, spelled as aliases so the set can be
185
+ #: resolved for whichever pointer width the binary uses.
186
+ _CALL_CLOBBER_ALIASES = (
187
+ "rax",
188
+ "rcx",
189
+ "rdx",
190
+ "rsi",
191
+ "rdi",
192
+ "r8",
193
+ "r9",
194
+ "r10",
195
+ "r11",
196
+ "eax",
197
+ "ecx",
198
+ "edx",
199
+ )
200
+
201
+ #: Mnemonics that transfer control to a subroutine and therefore clobber the
202
+ #: caller-saved registers.
203
+ _CALL_MNEMONICS = frozenset({"call", "callq"})
204
+
205
+
206
+ @dataclass(frozen=True)
207
+ class Definition:
208
+ """One register definition, i.e. the value an instruction leaves behind.
209
+
210
+ Attributes:
211
+ address: Virtual address of the defining instruction.
212
+ register: Canonical register name (``rax``, ``rdi``, ``r8``, ...); aliases
213
+ such as ``eax``/``ax``/``al`` are normalized to it.
214
+ kind: Shape of the definition: :data:`KIND_CONST`, :data:`KIND_COPY`,
215
+ :data:`KIND_ARITHMETIC` or :data:`KIND_UNKNOWN`.
216
+ value: The constant this definition provably writes, or ``None`` when the
217
+ value is not known. Present only for full-width writes, so a
218
+ ``mov al, 1`` keeps its kind but carries no value.
219
+ source_register: Canonical register a :data:`KIND_COPY` definition copies,
220
+ else ``None``.
221
+ """
222
+
223
+ address: int
224
+ register: str
225
+ kind: str
226
+ value: int | None
227
+ source_register: str | None
228
+
229
+ def to_dict(self) -> dict[str, Any]:
230
+ """Serialize the definition for the JSON report."""
231
+ return {
232
+ "address": f"0x{self.address:x}",
233
+ "register": self.register,
234
+ "kind": self.kind,
235
+ "value": self.value,
236
+ "source_register": self.source_register,
237
+ }
238
+
239
+
240
+ @dataclass(frozen=True)
241
+ class Use:
242
+ """One read or write of a register by a single instruction.
243
+
244
+ Attributes:
245
+ address: Virtual address of the instruction that uses the register.
246
+ register: Canonical register name, aliases normalized as in
247
+ :class:`Definition`.
248
+ read_or_write: :data:`READ` or :data:`WRITE`.
249
+ """
250
+
251
+ address: int
252
+ register: str
253
+ read_or_write: str
254
+
255
+ def to_dict(self) -> dict[str, Any]:
256
+ """Serialize the use for the JSON report."""
257
+ return {
258
+ "address": f"0x{self.address:x}",
259
+ "register": self.register,
260
+ "read_or_write": self.read_or_write,
261
+ }
262
+
263
+
264
+ @dataclass(frozen=True)
265
+ class DataFlowSummary:
266
+ """Definition-use chains, constants and reaching definitions of one function.
267
+
268
+ Attributes:
269
+ address: Function address the analysis started from.
270
+ definitions: Every register definition, in ascending address order.
271
+ uses: Every read and write, in ascending address order; within one
272
+ instruction the reads come before the write it makes.
273
+ constant_registers: Canonical register -> constant still known after the
274
+ forward pass, sorted by register name. Only provable constants appear.
275
+ reaching_definition: ``(canonical register, use address)`` -> address of
276
+ the definition that reaches that read. Reads with no definition in
277
+ the analysed region are absent and reported in ``warnings`` instead.
278
+ warnings: Bounded, human-readable caveats (undefined reads, a truncated
279
+ CFG), each stated once.
280
+ evidence: How the summary as a whole was derived.
281
+ """
282
+
283
+ address: int
284
+ definitions: tuple[Definition, ...]
285
+ uses: tuple[Use, ...]
286
+ constant_registers: Mapping[str, int]
287
+ reaching_definition: Mapping[tuple[str, int], int]
288
+ warnings: tuple[str, ...]
289
+ evidence: Evidence
290
+
291
+ def to_dict(self) -> dict[str, Any]:
292
+ """Serialize the summary for the JSON report (JSON-safe, sorted)."""
293
+ return {
294
+ "address": f"0x{self.address:x}",
295
+ "definition_count": len(self.definitions),
296
+ "use_count": len(self.uses),
297
+ "definitions": [definition.to_dict() for definition in self.definitions],
298
+ "uses": [use.to_dict() for use in self.uses],
299
+ "constant_registers": dict(self.constant_registers),
300
+ "reaching_definition": {
301
+ f"{register}@0x{use:x}": f"0x{definition:x}"
302
+ for (register, use), definition in self.reaching_definition.items()
303
+ },
304
+ "warnings": list(self.warnings),
305
+ "evidence": self.evidence.to_dict(),
306
+ }
307
+
308
+
309
+ # ---------------------------------------------------------------------------
310
+ # input validation and register name normalization
311
+ # ---------------------------------------------------------------------------
312
+
313
+
314
+ def _pointer_width(info: BinaryInfo) -> int:
315
+ """Return the pointer width in bytes implied by ``info.bitness``."""
316
+ bitness = int(getattr(info, "bitness", 0) or 0)
317
+ if bitness == 64:
318
+ return 8
319
+ if bitness == 32:
320
+ return 4
321
+ raise AnalysisError(f"data flow needs a 32-bit or 64-bit binary, got bitness {bitness}")
322
+
323
+
324
+ def _require_limit(name: str, value: object, minimum: int, maximum: int) -> int:
325
+ """Validate an integer budget and return it as ``int``."""
326
+ if isinstance(value, bool) or not isinstance(value, int):
327
+ raise AnalysisError(f"{name} must be an integer, got {value!r}")
328
+ if value < minimum:
329
+ raise AnalysisError(f"{name} must be >= {minimum}, got {value}")
330
+ if value > maximum:
331
+ raise AnalysisError(f"{name} too large (limit {maximum}), got {value}")
332
+ return value
333
+
334
+
335
+ def _alias_table(pointer_width: int) -> dict[str, tuple[str, int]]:
336
+ """Map every general-purpose register alias to ``(canonical register, width)``.
337
+
338
+ The table is built per pointer width so the canonical names match the binary:
339
+ ``eax`` is canonical on a 32-bit binary and an alias of ``rax`` on a 64-bit
340
+ one, and ``r8`` .. ``r15`` only exist above 32 bits.
341
+ """
342
+ table: dict[str, tuple[str, int]] = {}
343
+ for canonical, aliases in _ALIAS_WIDTHS.items():
344
+ if pointer_width < 8:
345
+ if canonical[1:].isdigit(): # r8 .. r15 do not exist below 64-bit
346
+ continue
347
+ wide = [alias for alias, width in aliases.items() if width == 4]
348
+ if not wide: # pragma: no cover - every legacy family has a 32-bit name
349
+ continue
350
+ canonical = wide[0]
351
+ aliases = {alias: width for alias, width in aliases.items() if width <= 4}
352
+ for alias, width in aliases.items():
353
+ table.setdefault(alias, (canonical, width))
354
+ return table
355
+
356
+
357
+ def _lookup(
358
+ operand: str, aliases: Mapping[str, tuple[str, int]], pointer_width: int
359
+ ) -> tuple[str, int]:
360
+ """Return ``(canonical register, width)`` of a register operand.
361
+
362
+ Registers outside the general-purpose set (``xmm0``, ``st0``, ...) are still
363
+ tracked, under their own lowercased name at the pointer width, so that a
364
+ write to them kills a previous definition instead of being ignored.
365
+ """
366
+ name = operand.strip().lower()
367
+ hit = aliases.get(name)
368
+ if hit is not None:
369
+ return hit
370
+ return name, pointer_width
371
+
372
+
373
+ def _full_width(register: str, aliases: Mapping[str, tuple[str, int]], pointer_width: int) -> int:
374
+ """Return the whole-register width of a canonical register."""
375
+ hit = aliases.get(register)
376
+ return hit[1] if hit is not None else pointer_width
377
+
378
+
379
+ def _mask(value: int, width: int) -> int:
380
+ """Truncate ``value`` to the ``width``-byte register it is written to."""
381
+ return value & ((1 << (max(width, 1) * 8)) - 1)
382
+
383
+
384
+ def _immediate(text: str) -> int | None:
385
+ """Parse an immediate operand, or return ``None`` when it is not one."""
386
+ match = _IMMEDIATE.match((text or "").strip())
387
+ if match is None:
388
+ return None
389
+ body = match.group("body").lower()
390
+ try:
391
+ value = int(body, 16) if body.startswith("0x") or match.group("suffix") else int(body, 10)
392
+ except ValueError: # pragma: no cover - the pattern already restricts the digits
393
+ return None
394
+ return -value if match.group("sign") == "-" else value
395
+
396
+
397
+ def _rip_displacement(operand: str, pointer_width: int) -> int | None:
398
+ """Return the displacement of a ``[rip +/- disp]`` operand, else ``None``.
399
+
400
+ Only a bare RIP base with a constant displacement is understood -- the shape
401
+ of ``lea rax, [rip + 0x10]`` and of a RIP-relative load. A base register, an
402
+ index, a symbol or a segment override returns ``None`` rather than a guess.
403
+ """
404
+ memory = _parse_memory(operand, pointer_width)
405
+ if memory is None or memory.base != "rip" or memory.index is not None:
406
+ return None
407
+ text = operand.strip()
408
+ body = text[text.index("[") + 1 : text.rindex("]")]
409
+ displacement = 0
410
+ for term in _ADDRESS_TERM.finditer(body):
411
+ value = term.group("value")
412
+ if not value[0].isdigit(): # the RIP term itself
413
+ continue
414
+ parsed = _immediate(value)
415
+ if parsed is None:
416
+ return None
417
+ displacement += -parsed if term.group("sign") == "-" else parsed
418
+ return displacement
419
+
420
+
421
+ # ---------------------------------------------------------------------------
422
+ # reading the decoded instructions
423
+ # ---------------------------------------------------------------------------
424
+
425
+
426
+ def _instruction_address(instruction: Mapping[str, Any]) -> int:
427
+ """Return the integer address of an ``IRInstruction.to_dict()`` payload."""
428
+ raw = instruction.get("address")
429
+ if isinstance(raw, int) and not isinstance(raw, bool):
430
+ return raw
431
+ token = str(raw or "").strip().lower()
432
+ if not token:
433
+ raise AnalysisError(f"instruction has no address: {instruction!r}")
434
+ try:
435
+ return int(token, 16) if token.startswith("0x") else int(token, 10)
436
+ except ValueError as exc:
437
+ raise AnalysisError(f"instruction has an unreadable address: {instruction!r}") from exc
438
+
439
+
440
+ def _instruction_size(instruction: Mapping[str, Any]) -> int:
441
+ """Return the encoded size of an instruction, ``0`` when it is not reported."""
442
+ size = instruction.get("size")
443
+ if isinstance(size, int) and not isinstance(size, bool) and size > 0:
444
+ return size
445
+ return 0
446
+
447
+
448
+ def _read_registers(
449
+ mnemonic: str, parts: Sequence[str], aliases: Mapping[str, tuple[str, int]]
450
+ ) -> tuple[str, ...]:
451
+ """Return the canonical registers one instruction reads, sorted.
452
+
453
+ The first operand is only skipped when the mnemonic writes it *and* it is a
454
+ plain register: ``mov [rdi], rax`` still reads ``rdi``, because that operand
455
+ is an address. Operands are scanned for register tokens, so a bracketed
456
+ expression contributes its address registers and ``qword``/``ptr``/``rip``
457
+ are dropped by the alias table.
458
+ """
459
+ destination_only = (
460
+ mnemonic in _MOV_MNEMONICS or mnemonic in {"lea", "pop"} or mnemonic.startswith("set")
461
+ )
462
+ found: set[str] = set()
463
+ for position, operand in enumerate(parts):
464
+ if position == 0 and destination_only and _is_plain_register(operand):
465
+ continue
466
+ for token in _WORD.findall(operand):
467
+ hit = aliases.get(token.lower())
468
+ if hit is not None:
469
+ found.add(hit[0])
470
+ return tuple(sorted(found))
471
+
472
+
473
+ # ---------------------------------------------------------------------------
474
+ # classification of one write
475
+ # ---------------------------------------------------------------------------
476
+
477
+
478
+ def _copied_value(
479
+ state: Mapping[str, Definition],
480
+ source: str,
481
+ source_width: int,
482
+ width: int,
483
+ aliases: Mapping[str, tuple[str, int]],
484
+ pointer_width: int,
485
+ ) -> int | None:
486
+ """Return the constant a register copy carries, or ``None`` when unknown."""
487
+ if source_width != _full_width(source, aliases, pointer_width):
488
+ return None # a narrow alias does not expose the whole register
489
+ if width < source_width:
490
+ return None # the copy would truncate the source value
491
+ definition = state.get(source)
492
+ if definition is None or definition.value is None:
493
+ return None
494
+ return _mask(definition.value, width)
495
+
496
+
497
+ def _folded_value(
498
+ mnemonic: str,
499
+ register: str,
500
+ width: int,
501
+ previous: int | None,
502
+ source: str,
503
+ state: Mapping[str, Definition],
504
+ aliases: Mapping[str, tuple[str, int]],
505
+ pointer_width: int,
506
+ ) -> int | None:
507
+ """Fold ``add``/``sub``/``xor`` of a known operand, or return ``None``."""
508
+ if mnemonic not in _FOLDING_MNEMONICS:
509
+ return None
510
+ if _is_plain_register(source):
511
+ source_register, source_width = _lookup(source, aliases, pointer_width)
512
+ if source_register == register and mnemonic in {"xor", "sub"}:
513
+ return 0 # a register xor/sub itself
514
+ if source_width != _full_width(source_register, aliases, pointer_width):
515
+ return None
516
+ operand: int | None = None
517
+ definition = state.get(source_register)
518
+ if definition is not None:
519
+ operand = definition.value
520
+ else:
521
+ operand = _immediate(source)
522
+ if previous is None or operand is None:
523
+ return None
524
+ if mnemonic == "add":
525
+ return _mask(previous + operand, width)
526
+ if mnemonic == "sub":
527
+ return _mask(previous - operand, width)
528
+ return _mask(previous ^ operand, width)
529
+
530
+
531
+ def _classify(
532
+ instruction: Mapping[str, Any],
533
+ mnemonic: str,
534
+ parts: Sequence[str],
535
+ address: int,
536
+ state: Mapping[str, Definition],
537
+ aliases: Mapping[str, tuple[str, int]],
538
+ pointer_width: int,
539
+ ) -> Definition | None:
540
+ """Turn one instruction into a :class:`Definition`, or ``None`` if it writes none."""
541
+ if not parts or not _writes_first_operand(mnemonic):
542
+ return None
543
+ destination = parts[0].strip().lower()
544
+ if not _is_plain_register(destination):
545
+ return None # a memory or implicit destination defines no register
546
+
547
+ register, width = _lookup(destination, aliases, pointer_width)
548
+ source = parts[1].strip().lower() if len(parts) > 1 else ""
549
+ kind = KIND_UNKNOWN
550
+ value: int | None = None
551
+ source_register: str | None = None
552
+
553
+ previous = state.get(register)
554
+ previous_value = previous.value if previous is not None else None
555
+
556
+ if mnemonic in _MOV_MNEMONICS and mnemonic not in _EXTENDING_MNEMONICS and source:
557
+ if _is_plain_register(source):
558
+ source_register, source_width = _lookup(source, aliases, pointer_width)
559
+ kind = KIND_COPY
560
+ value = _copied_value(
561
+ state, source_register, source_width, width, aliases, pointer_width
562
+ )
563
+ else:
564
+ immediate = _immediate(source)
565
+ if immediate is not None:
566
+ kind = KIND_CONST
567
+ value = _mask(immediate, width)
568
+ elif mnemonic == "lea" and source:
569
+ displacement = _rip_displacement(source, pointer_width)
570
+ if displacement is not None:
571
+ kind = KIND_CONST
572
+ value = _mask(address + _instruction_size(instruction) + displacement, width)
573
+ elif mnemonic in _ARITHMETIC_MNEMONICS and source:
574
+ kind = KIND_ARITHMETIC
575
+ value = _folded_value(
576
+ mnemonic, register, width, previous_value, source, state, aliases, pointer_width
577
+ )
578
+
579
+ if width < FULL_CONSTANT_WIDTH:
580
+ value = None # a partial write leaves the upper bytes of the value unknown
581
+
582
+ return Definition(
583
+ address=address,
584
+ register=register,
585
+ kind=kind,
586
+ value=value,
587
+ source_register=source_register,
588
+ )
589
+
590
+
591
+ # ---------------------------------------------------------------------------
592
+ # public entry point
593
+ # ---------------------------------------------------------------------------
594
+
595
+
596
+ def analyze_dataflow(
597
+ path: str | Path,
598
+ info: BinaryInfo,
599
+ address: int,
600
+ max_instructions: int = DEFAULT_MAX_INSTRUCTIONS,
601
+ max_blocks: int = DEFAULT_MAX_BLOCKS,
602
+ ) -> DataFlowSummary:
603
+ """Recover definitions, uses, constants and reaching definitions of a function.
604
+
605
+ The CFG is built with :func:`~codevanta.analysis.cfg.build_recursive_cfg` and
606
+ then walked once, forward, block by block in ascending address order and
607
+ instruction by instruction inside a block. Each instruction contributes its
608
+ read uses, which are resolved against the definitions seen so far, and -- when
609
+ it writes a register -- one :class:`Definition` that replaces (kills) whatever
610
+ the register held before. A folded value is masked to the written register's
611
+ width, and only the cases listed in the module docstring are folded.
612
+
613
+ Args:
614
+ path: File the bytes are read from (read-only; never executed).
615
+ info: Loaded metadata used for address mapping, decoding and the pointer
616
+ width; 32-bit and 64-bit binaries are supported.
617
+ address: Virtual address of the first instruction; must be inside an
618
+ executable section.
619
+ max_instructions: Instruction budget
620
+ (``MIN_MAX_INSTRUCTIONS`` .. ``MAX_MAX_INSTRUCTIONS``).
621
+ max_blocks: Block budget (``MIN_MAX_BLOCKS`` .. ``MAX_MAX_BLOCKS``).
622
+
623
+ Returns:
624
+ The :class:`DataFlowSummary` of the region: definitions and uses in
625
+ address order, the constants still known after the pass, one reaching
626
+ definition per resolved read, the bounded warnings and the evidence.
627
+
628
+ Raises:
629
+ AnalysisError: When ``address`` is not a non-negative integer, when a
630
+ budget is out of range or not an integer, when the binary is neither
631
+ 32- nor 64-bit, when ``address`` is not inside an executable section,
632
+ or when the CFG cannot be built.
633
+
634
+ Note:
635
+ The pass is a linearization of the CFG, not a fixpoint: state flows into a
636
+ block from the block that precedes it in address order, so a join is
637
+ approximated by whichever predecessor has the highest address. What is
638
+ reported stays sound one definition at a time: a value is only folded
639
+ from instructions the summary itself proves, and only the *last*
640
+ definition of a register survives, so ``constant_registers`` and
641
+ ``reaching_definition`` never claim two conflicting origins for one
642
+ value. Two calls with the same arguments always produce the same summary.
643
+ """
644
+ if isinstance(address, bool) or not isinstance(address, int):
645
+ raise AnalysisError(f"address must be an integer, got {address!r}")
646
+ if address < 0:
647
+ raise AnalysisError(f"address must be non-negative, got {address}")
648
+ instruction_budget = _require_limit(
649
+ "max_instructions", max_instructions, MIN_MAX_INSTRUCTIONS, MAX_MAX_INSTRUCTIONS
650
+ )
651
+ block_budget = _require_limit("max_blocks", max_blocks, MIN_MAX_BLOCKS, MAX_MAX_BLOCKS)
652
+ pointer_width = _pointer_width(info)
653
+
654
+ section = info.section_for_va(address)
655
+ if section is None or not section.is_executable:
656
+ raise AnalysisError(f"address 0x{address:x} is not inside an executable section")
657
+
658
+ cfg = build_recursive_cfg(
659
+ path,
660
+ info,
661
+ address,
662
+ max_instructions=instruction_budget,
663
+ max_blocks=block_budget,
664
+ )
665
+
666
+ aliases = _alias_table(pointer_width)
667
+ clobbers = frozenset(
668
+ hit[0] for alias in _CALL_CLOBBER_ALIASES if (hit := aliases.get(alias)) is not None
669
+ )
670
+ state: dict[str, Definition] = {}
671
+ definitions: list[Definition] = []
672
+ uses: list[Use] = []
673
+ reaching: dict[tuple[str, int], int] = {}
674
+ undefined: list[tuple[str, int]] = []
675
+
676
+ for block in sorted(cfg.blocks, key=lambda item: item.start):
677
+ for instruction in sorted(block.instructions, key=_instruction_address):
678
+ mnemonic, operands = _instruction_parts(instruction)
679
+ if not mnemonic: # pragma: no cover - the decoder always names a mnemonic
680
+ continue
681
+ current = _instruction_address(instruction)
682
+ parts = _split_operands(operands)
683
+
684
+ for register in _read_registers(mnemonic, parts, aliases):
685
+ uses.append(Use(current, register, READ))
686
+ known = state.get(register)
687
+ if known is None:
688
+ undefined.append((register, current))
689
+ else:
690
+ reaching[(register, current)] = known.address
691
+
692
+ definition = _classify(
693
+ instruction, mnemonic, parts, current, state, aliases, pointer_width
694
+ )
695
+ if definition is not None:
696
+ definitions.append(definition)
697
+ uses.append(Use(current, definition.register, WRITE))
698
+ state[definition.register] = definition
699
+
700
+ if mnemonic in _CALL_MNEMONICS:
701
+ for register in clobbers:
702
+ state.pop(register, None)
703
+
704
+ warnings = _undefined_warnings(undefined)
705
+ if cfg.truncated:
706
+ warnings.append(
707
+ "the CFG was truncated at the instruction or block budget; "
708
+ "definitions past that point are not modelled"
709
+ )
710
+
711
+ constants = {
712
+ register: definition.value
713
+ for register, definition in sorted(state.items(), key=lambda item: item[0])
714
+ if definition.value is not None
715
+ }
716
+ evidence = Evidence(
717
+ source="dataflow.def_use",
718
+ method="forward_propagation",
719
+ confidence=Confidence.MEDIUM,
720
+ address=address,
721
+ detail=(
722
+ f"{len(definitions)} definition(s) and {len(uses)} use(s) over "
723
+ f"{len(cfg.blocks)} block(s); {len(reaching)} read(s) resolved to a "
724
+ f"definition and {len(constants)} register constant(s) survived"
725
+ ),
726
+ extra={
727
+ "pointer_width": pointer_width,
728
+ "blocks": len(cfg.blocks),
729
+ "instructions": cfg.instruction_count,
730
+ "definitions": len(definitions),
731
+ "uses": len(uses),
732
+ "constant_registers": len(constants),
733
+ "constant_definitions": sum(1 for item in definitions if item.value is not None),
734
+ "reaching_definitions": len(reaching),
735
+ "undefined_reads": len(undefined),
736
+ "truncated": cfg.truncated,
737
+ },
738
+ )
739
+ return DataFlowSummary(
740
+ address=address,
741
+ definitions=tuple(definitions),
742
+ uses=tuple(uses),
743
+ constant_registers=constants,
744
+ reaching_definition=reaching,
745
+ warnings=tuple(warnings),
746
+ evidence=evidence,
747
+ )
748
+
749
+
750
+ def _undefined_warnings(undefined: Sequence[tuple[str, int]]) -> list[str]:
751
+ """Render the bounded "no reaching definition" warnings of one summary."""
752
+ warnings = [
753
+ f"read of {register} at 0x{address:x} has no reaching definition "
754
+ "in the analysed region"
755
+ for register, address in undefined[:MAX_UNDEFINED_WARNINGS]
756
+ ]
757
+ remaining = len(undefined) - MAX_UNDEFINED_WARNINGS
758
+ if remaining > 0:
759
+ warnings.append(
760
+ f"{remaining} further read(s) have no reaching definition in the analysed region"
761
+ )
762
+ return warnings