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,1055 @@
1
+ """Heuristic argument and return type inference from register use (phase 4).
2
+
3
+ A mnemonic-level renderer can never recover real C types, but it *can* report
4
+ how each ABI argument register is used, and the use is a type witness.
5
+ :func:`infer_argument_types` walks the decoded instructions of one function,
6
+ records one observation per use and turns the strongest observation into a C
7
+ type; :func:`infer_return_type` does the same for the value left in the return
8
+ register. Nothing is executed and nothing is invented: every emitted type
9
+ carries the observation that produced it.
10
+
11
+ Rule table for one argument register::
12
+
13
+ observation example C type
14
+ ------------------------------------ --------------------------- -----------
15
+ dereferenced at pointer width mov rax, [rdi] uint64_t *
16
+ dereferenced narrower than a pointer mov eax, dword ptr [rdi] void *
17
+ used as a scaled index [rdi + rsi*4] size_t
18
+ only arithmetic on it add rdi, 1 uint64_t
19
+ copied whole, never narrowed mov rax, rdi uint64_t
20
+ compared and branched on test rdi, rdi / je int32_t
21
+ no usable observation never read, or opaque use unknown64
22
+
23
+ The C type of a rule is *width-aware*: the width of the register alias and of
24
+ the memory operand decide the type, so ``add edi, 1`` is ``uint32_t`` and
25
+ ``dword ptr [ecx]`` on a 32-bit binary is a ``uint32_t *``. The pointer forms
26
+ and the ``unknownNN`` placeholder are keyed by the binary's pointer width.
27
+
28
+ When more than one rule fires, the most specific one wins -- pointers before
29
+ plain integers, integers before comparisons -- and every other observation is
30
+ kept in the evidence note together with the type it would have produced, so a
31
+ conflict is never resolved silently. The ranking (:data:`RULE_RANKS`) is::
32
+
33
+ pointer_deref > narrow_deref > index > arithmetic > copy > compare
34
+
35
+ Return type inference is deliberately simpler because there is one register to
36
+ watch: scanning stops at the first ``ret``, the last write to the return
37
+ register decides the width (``rax`` -> ``uint64_t``, ``eax`` -> ``uint32_t``,
38
+ ``ax`` -> ``uint16_t``, ``al`` -> ``uint8_t``), a ``setcc`` write is called out
39
+ as an ``int32_t`` boolean, and a function that never writes the register before
40
+ a ``ret`` returns ``void``. A ``call`` right before the ``ret`` is mentioned in
41
+ the note, because whatever the callee left in the register is not ours.
42
+
43
+ Accepted instruction shapes are the ones the rest of the engine already uses:
44
+ ``IRInstruction.to_dict()`` payloads (``{"mnemonic", "operands", ...}``),
45
+ :class:`~codevanta.ir.instruction.IRInstruction` objects, or a plain
46
+ ``"mnemonic operands"`` string.
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ import re
52
+ from collections.abc import Iterable, Mapping, Sequence
53
+ from dataclasses import dataclass
54
+ from typing import Any
55
+
56
+ from codevanta.core.report import AnalysisError
57
+ from codevanta.evidence.confidence import Confidence
58
+
59
+ __all__ = [
60
+ "ABI_ARGUMENT_REGISTERS",
61
+ "C_TYPE_INT32",
62
+ "C_TYPE_POINTER_32",
63
+ "C_TYPE_POINTER_64",
64
+ "C_TYPE_SIZE_T",
65
+ "C_TYPE_UINT8",
66
+ "C_TYPE_UINT16",
67
+ "C_TYPE_UINT32",
68
+ "C_TYPE_UINT64",
69
+ "C_TYPE_UNKNOWN_32",
70
+ "C_TYPE_UNKNOWN_64",
71
+ "C_TYPE_VOID",
72
+ "C_TYPE_VOID_POINTER",
73
+ "INTEGER_C_TYPES",
74
+ "POINTER_C_TYPES",
75
+ "RULE_RANKS",
76
+ "UNKNOWN_C_TYPES",
77
+ "ArgumentType",
78
+ "ReturnType",
79
+ "infer_argument_types",
80
+ "infer_return_type",
81
+ ]
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # emitted C type names
85
+ # ---------------------------------------------------------------------------
86
+
87
+ #: Unsigned integer of exactly one byte.
88
+ C_TYPE_UINT8 = "uint8_t"
89
+
90
+ #: Unsigned integer of exactly two bytes.
91
+ C_TYPE_UINT16 = "uint16_t"
92
+
93
+ #: Unsigned integer of exactly four bytes.
94
+ C_TYPE_UINT32 = "uint32_t"
95
+
96
+ #: Unsigned integer of exactly eight bytes.
97
+ C_TYPE_UINT64 = "uint64_t"
98
+
99
+ #: Signed 32-bit integer, emitted for comparison/boolean evidence.
100
+ C_TYPE_INT32 = "int32_t"
101
+
102
+ #: ``size_t``, emitted for a register used as an address index.
103
+ C_TYPE_SIZE_T = "size_t"
104
+
105
+ #: ``void``, emitted for a function that never writes its return register.
106
+ C_TYPE_VOID = "void"
107
+
108
+ #: A pointer to nothing in particular, emitted for a sub-pointer-width deref.
109
+ C_TYPE_VOID_POINTER = "void *"
110
+
111
+ #: Pointer type of a 64-bit binary.
112
+ C_TYPE_POINTER_64 = "uint64_t *"
113
+
114
+ #: Pointer type of a 32-bit binary.
115
+ C_TYPE_POINTER_32 = "uint32_t *"
116
+
117
+ #: Placeholder emitted when no rule fires on a 64-bit binary.
118
+ C_TYPE_UNKNOWN_64 = "unknown64"
119
+
120
+ #: Placeholder emitted when no rule fires on a 32-bit binary.
121
+ C_TYPE_UNKNOWN_32 = "unknown32"
122
+
123
+ #: Access width in bytes -> the unsigned C integer of that width.
124
+ INTEGER_C_TYPES: dict[int, str] = {
125
+ 1: C_TYPE_UINT8,
126
+ 2: C_TYPE_UINT16,
127
+ 4: C_TYPE_UINT32,
128
+ 8: C_TYPE_UINT64,
129
+ }
130
+
131
+ #: Pointer width in bytes -> the pointer type of a pointer-width dereference.
132
+ POINTER_C_TYPES: dict[int, str] = {
133
+ 1: "uint8_t *",
134
+ 2: "uint16_t *",
135
+ 4: C_TYPE_POINTER_32,
136
+ 8: C_TYPE_POINTER_64,
137
+ }
138
+
139
+ #: Pointer width in bytes -> the placeholder type of an unproven argument.
140
+ UNKNOWN_C_TYPES: dict[int, str] = {
141
+ 4: C_TYPE_UNKNOWN_32,
142
+ 8: C_TYPE_UNKNOWN_64,
143
+ }
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # ABI tables
147
+ # ---------------------------------------------------------------------------
148
+
149
+ #: Argument registers per ABI, in argument order, for a 64-bit binary.
150
+ ABI_ARGUMENT_REGISTERS: dict[str, tuple[str, ...]] = {
151
+ "sysv": ("rdi", "rsi", "rdx", "rcx", "r8", "r9"),
152
+ "ms64": ("rcx", "rdx", "r8", "r9"),
153
+ }
154
+
155
+ #: Argument registers used for a 32-bit binary, whatever the ABI is called.
156
+ _ARGUMENTS_32: tuple[str, ...] = ("ecx", "edx")
157
+
158
+ #: Accepted spellings of an ABI name, lowercased and without separators.
159
+ _ABI_SPELLINGS: dict[str, str] = {
160
+ "sysv": "sysv",
161
+ "systemv": "sysv",
162
+ "amd64": "sysv",
163
+ "ms": "ms64",
164
+ "ms64": "ms64",
165
+ "win64": "ms64",
166
+ "microsoft": "ms64",
167
+ }
168
+
169
+ #: Canonical argument register -> {alias as Capstone prints it: width in bytes}.
170
+ _REGISTER_ALIAS_WIDTHS: dict[str, dict[str, int]] = {
171
+ "rdi": {"rdi": 8, "edi": 4, "di": 2, "dil": 1},
172
+ "rsi": {"rsi": 8, "esi": 4, "si": 2, "sil": 1},
173
+ "rdx": {"rdx": 8, "edx": 4, "dx": 2, "dl": 1, "dh": 1},
174
+ "rcx": {"rcx": 8, "ecx": 4, "cx": 2, "cl": 1, "ch": 1},
175
+ "r8": {"r8": 8, "r8d": 4, "r8w": 2, "r8b": 1},
176
+ "r9": {"r9": 8, "r9d": 4, "r9w": 2, "r9b": 1},
177
+ "ecx": {"ecx": 4, "cx": 2, "cl": 1, "ch": 1},
178
+ "edx": {"edx": 4, "dx": 2, "dl": 1, "dh": 1},
179
+ }
180
+
181
+ #: Alias -> width in bytes, unioned over every supported register set. Widths
182
+ #: do not conflict between sets (``edx`` is four bytes either way), only the
183
+ #: canonical name does, which is why :func:`_alias_table` is built per ABI.
184
+ _ARGUMENT_ALIAS_WIDTHS: dict[str, int] = {
185
+ alias: width
186
+ for aliases in _REGISTER_ALIAS_WIDTHS.values()
187
+ for alias, width in aliases.items()
188
+ }
189
+
190
+ #: Return register aliases, keyed by pointer width.
191
+ _RETURN_ALIAS_WIDTHS: dict[int, dict[str, int]] = {
192
+ 8: {"rax": 8, "eax": 4, "ax": 2, "al": 1, "ah": 1},
193
+ 4: {"eax": 4, "ax": 2, "al": 1, "ah": 1},
194
+ 2: {"ax": 2, "al": 1, "ah": 1},
195
+ 1: {"al": 1, "ah": 1},
196
+ }
197
+
198
+ #: Name used in evidence prose for the return register, keyed by pointer width.
199
+ _RETURN_REGISTER: dict[int, str] = {1: "al", 2: "ax", 4: "eax", 8: "rax"}
200
+
201
+ # ---------------------------------------------------------------------------
202
+ # mnemonic tables
203
+ # ---------------------------------------------------------------------------
204
+
205
+ #: ``mov``-style mnemonics: the first operand is only ever written.
206
+ _MOV_MNEMONICS = frozenset({"mov", "movabs", "movbe", "movsx", "movsxd", "movzx", "movq"})
207
+
208
+ #: Mnemonics that transfer control to a subroutine.
209
+ _CALL_MNEMONICS = frozenset({"call", "callq", "bl", "blx", "blr"})
210
+
211
+ #: Mnemonics that end the scanned region with a plain return.
212
+ _RET_MNEMONICS = frozenset({"ret", "retn", "retf", "retq", "iret", "iretd", "iretq", "eret"})
213
+
214
+ #: Mnemonics that only compare their operands and set flags.
215
+ _COMPARE_MNEMONICS = frozenset({"cmp", "test", "comiss", "comisd", "ucomiss", "ucomisd"})
216
+
217
+ #: Mnemonics that never write their first operand.
218
+ _READ_ONLY_MNEMONICS = frozenset(
219
+ {
220
+ "bt",
221
+ "call",
222
+ "callq",
223
+ "cmp",
224
+ "comisd",
225
+ "comiss",
226
+ "int",
227
+ "int3",
228
+ "jmp",
229
+ "ljmp",
230
+ "nop",
231
+ "push",
232
+ "ret",
233
+ "test",
234
+ "ucomisd",
235
+ "ucomiss",
236
+ "ud2",
237
+ "xlat",
238
+ }
239
+ )
240
+
241
+ #: Unconditional jumps; any other ``j*`` mnemonic branches on the flags.
242
+ _UNCONDITIONAL_JUMPS = frozenset({"jmp", "ljmp", "b", "br", "bx"})
243
+
244
+ #: Integer operations a value can take part in without leaving its width.
245
+ _ARITHMETIC_MNEMONICS = frozenset(
246
+ {
247
+ "adc",
248
+ "add",
249
+ "and",
250
+ "dec",
251
+ "div",
252
+ "idiv",
253
+ "imul",
254
+ "inc",
255
+ "mul",
256
+ "neg",
257
+ "not",
258
+ "or",
259
+ "rol",
260
+ "ror",
261
+ "sal",
262
+ "sar",
263
+ "sbb",
264
+ "shl",
265
+ "shr",
266
+ "sub",
267
+ "xor",
268
+ }
269
+ )
270
+
271
+ # ---------------------------------------------------------------------------
272
+ # rule names and ranking
273
+ # ---------------------------------------------------------------------------
274
+
275
+ _RULE_POINTER_DEREF = "pointer_deref"
276
+ _RULE_NARROW_DEREF = "narrow_deref"
277
+ _RULE_INDEX = "index"
278
+ _RULE_ARITHMETIC = "arithmetic"
279
+ _RULE_COPY = "copy"
280
+ _RULE_COMPARE = "compare"
281
+
282
+ #: Specificity of every rule: the highest rank wins when several rules fire.
283
+ RULE_RANKS: dict[str, int] = {
284
+ _RULE_POINTER_DEREF: 7,
285
+ _RULE_NARROW_DEREF: 6,
286
+ _RULE_INDEX: 5,
287
+ _RULE_ARITHMETIC: 4,
288
+ _RULE_COPY: 3,
289
+ _RULE_COMPARE: 2,
290
+ }
291
+
292
+
293
+ @dataclass(frozen=True)
294
+ class ArgumentType:
295
+ """An inferred C type for one argument of the rendered function.
296
+
297
+ Attributes:
298
+ register: Canonical ABI argument register, e.g. ``"rdi"``.
299
+ name: Parameter name used by the pseudo-C renderer, e.g. ``"a0"``.
300
+ c_type: Emitted C type, e.g. :data:`C_TYPE_POINTER_64`.
301
+ evidence_note: The observation that produced ``c_type``; when several
302
+ rules fired, the losing observations and their types are listed
303
+ too, so a conflict is never hidden.
304
+ confidence: How strongly ``c_type`` is supported. ``MEDIUM`` for a use
305
+ that fixes a width (dereference, index, arithmetic), ``LOW`` for a
306
+ weaker hint (a whole-register copy, a comparison, no evidence).
307
+ """
308
+
309
+ register: str
310
+ name: str
311
+ c_type: str
312
+ evidence_note: str
313
+ confidence: Confidence
314
+
315
+ def to_dict(self) -> dict[str, Any]:
316
+ """Serialize the inferred argument type for the JSON report."""
317
+ return {
318
+ "register": self.register,
319
+ "name": self.name,
320
+ "c_type": self.c_type,
321
+ "evidence_note": self.evidence_note,
322
+ "confidence": self.confidence.value,
323
+ }
324
+
325
+
326
+ @dataclass(frozen=True)
327
+ class ReturnType:
328
+ """An inferred C return type for the rendered function.
329
+
330
+ Attributes:
331
+ c_type: Emitted C type: ``void``, a width-matched unsigned integer or
332
+ :data:`C_TYPE_INT32` for a ``setcc`` boolean.
333
+ evidence_note: The observation that produced ``c_type``.
334
+ confidence: ``MEDIUM`` when an instruction writes the return register
335
+ with an observable width, ``LOW`` for ``void`` (absence of a write
336
+ is weaker evidence than a write).
337
+ """
338
+
339
+ c_type: str
340
+ evidence_note: str
341
+ confidence: Confidence
342
+
343
+ def to_dict(self) -> dict[str, Any]:
344
+ """Serialize the inferred return type for the JSON report."""
345
+ return {
346
+ "c_type": self.c_type,
347
+ "evidence_note": self.evidence_note,
348
+ "confidence": self.confidence.value,
349
+ }
350
+
351
+
352
+ def _unsigned_type(width: int) -> str:
353
+ """Return the unsigned C integer of ``width`` bytes."""
354
+ return INTEGER_C_TYPES.get(width, C_TYPE_UINT64)
355
+
356
+
357
+ def _pointer_type(width: int) -> str:
358
+ """Return the pointer C type of a ``width``-byte pointer."""
359
+ return POINTER_C_TYPES.get(width, f"{_unsigned_type(width)} *")
360
+
361
+
362
+ def _unknown_type(width: int) -> str:
363
+ """Return the placeholder type for a pointer width with no evidence."""
364
+ return UNKNOWN_C_TYPES.get(width, f"unknown{width * 8}")
365
+
366
+
367
+ # ---------------------------------------------------------------------------
368
+ # input normalization and operand parsing
369
+ # ---------------------------------------------------------------------------
370
+
371
+ #: Intel size keyword -> access width in bytes. Keywords that are not listed
372
+ #: (``tbyte``, ``xmmword``, ...) fall back to the pointer width.
373
+ _SIZE_KEYWORD_WIDTHS: dict[str, int] = {
374
+ "byte": 1,
375
+ "word": 2,
376
+ "dword": 4,
377
+ "qword": 8,
378
+ }
379
+
380
+ #: A memory operand: an optional size keyword plus a bracketed expression.
381
+ _MEMORY_OPERAND = re.compile(
382
+ r"^(?:(?P<size>byte|word|dword|qword|tbyte|xmmword|oword|fword)\s+ptr\s+)?"
383
+ r"\[\s*(?P<body>[^]]*?)\s*\]$",
384
+ re.IGNORECASE,
385
+ )
386
+
387
+ #: One term of a bracketed address expression.
388
+ _ADDRESS_TERM = re.compile(
389
+ r"(?P<sign>[+-])?\s*(?P<value>0x[0-9a-fA-F]+|\d+|[a-zA-Z][a-zA-Z0-9]*)"
390
+ r"(?:\s*\*\s*(?P<scale>\d+))?"
391
+ )
392
+
393
+ #: A whole identifier-ish token, used to find register mentions.
394
+ _WORD = re.compile(r"[a-zA-Z][a-zA-Z0-9]*")
395
+
396
+ #: Immediates that mean "zero" once Capstone has printed them.
397
+ _ZERO_TOKENS = frozenset({"0", "0x0", "0h", "-0"})
398
+
399
+ #: How many touched instructions one "no evidence" note may remember.
400
+ _MAX_USES = 4
401
+
402
+
403
+ def _instruction_parts(instruction: Any) -> tuple[str, str]:
404
+ """Return the lowercased mnemonic and the operand text of one instruction.
405
+
406
+ ``IRInstruction.to_dict()`` payloads, :class:`~codevanta.ir.instruction.
407
+ IRInstruction` objects and plain ``"mnemonic operands"`` strings are all
408
+ accepted, because that is what the callers in this package already hold.
409
+ """
410
+ if isinstance(instruction, str):
411
+ head, _, tail = instruction.strip().partition(" ")
412
+ return head.lower(), tail.strip()
413
+ mnemonic: Any = None
414
+ operands: Any = None
415
+ if isinstance(instruction, Mapping):
416
+ mnemonic = instruction.get("mnemonic")
417
+ operands = instruction.get("operands")
418
+ else:
419
+ mnemonic = getattr(instruction, "mnemonic", None)
420
+ operands = getattr(instruction, "operands", None)
421
+ return str(mnemonic or "").strip().lower(), str(operands or "").strip()
422
+
423
+
424
+ def _split_operands(text: str) -> list[str]:
425
+ """Split Capstone operand text on top-level commas."""
426
+ text = (text or "").strip()
427
+ if not text:
428
+ return []
429
+ parts: list[str] = []
430
+ current: list[str] = []
431
+ depth = 0
432
+ for character in text:
433
+ if character == "[":
434
+ depth += 1
435
+ elif character == "]":
436
+ depth = max(0, depth - 1)
437
+ if character == "," and depth == 0:
438
+ parts.append("".join(current).strip())
439
+ current = []
440
+ continue
441
+ current.append(character)
442
+ parts.append("".join(current).strip())
443
+ return [part for part in parts if part]
444
+
445
+
446
+ def _stream(instructions: Iterable[Any]) -> list[tuple[str, str]]:
447
+ """Materialize the instruction stream as ``(mnemonic, operands)`` pairs."""
448
+ if isinstance(instructions, (str, bytes)):
449
+ raise AnalysisError("instructions must be an iterable of instructions, not a string")
450
+ stream: list[tuple[str, str]] = []
451
+ for instruction in instructions:
452
+ mnemonic, operands = _instruction_parts(instruction)
453
+ if mnemonic:
454
+ stream.append((mnemonic, operands))
455
+ return stream
456
+
457
+
458
+ def _text(mnemonic: str, operands: str) -> str:
459
+ """Return ``"mnemonic operands"`` of one instruction, for evidence prose."""
460
+ return f"{mnemonic} {operands}".strip()
461
+
462
+
463
+ @dataclass(frozen=True)
464
+ class _Memory:
465
+ """A parsed bracketed address expression.
466
+
467
+ Attributes:
468
+ base: Base register, lowercased, or ``None`` for an index-only or
469
+ absolute operand.
470
+ index: Index register, lowercased, or ``None``.
471
+ scale: Index scale, ``1`` when the operand carries none.
472
+ width: Access width in bytes: the size keyword, else the pointer width.
473
+ text: The operand as it was printed, for evidence prose.
474
+ """
475
+
476
+ base: str | None
477
+ index: str | None
478
+ scale: int
479
+ width: int
480
+ text: str
481
+
482
+
483
+ def _parse_memory(operand: str, pointer_width: int) -> _Memory | None:
484
+ """Parse a bracketed address expression, or return ``None`` if unhandled.
485
+
486
+ ``[base]``, ``[base +/- disp]``, ``[index]``, ``[index*scale]`` and
487
+ ``[base + index*scale +/- disp]`` are understood. Segment prefixes and
488
+ expressions with more than a base and an index are skipped rather than
489
+ guessed at.
490
+ """
491
+ match = _MEMORY_OPERAND.match(operand.strip())
492
+ if match is None:
493
+ return None
494
+ body = match.group("body")
495
+ if ":" in body: # a segment override is never guessed at
496
+ return None
497
+ base: str | None = None
498
+ index: str | None = None
499
+ scale = 1
500
+ for term in _ADDRESS_TERM.finditer(body):
501
+ value = term.group("value")
502
+ if value[0].isdigit(): # a displacement, not a register
503
+ continue
504
+ register = value.lower()
505
+ term_scale = int(term.group("scale")) if term.group("scale") else 1
506
+ if base is None and term_scale == 1:
507
+ base = register
508
+ elif index is None:
509
+ index = register
510
+ scale = term_scale
511
+ else: # more than a base and an index: not modelled
512
+ return None
513
+ keyword = match.group("size")
514
+ width = _SIZE_KEYWORD_WIDTHS.get(keyword.lower(), 0) if keyword else 0
515
+ return _Memory(base, index, scale, width or pointer_width, operand.strip())
516
+
517
+
518
+ # ---------------------------------------------------------------------------
519
+ # register aliases and operand scanning
520
+ # ---------------------------------------------------------------------------
521
+
522
+
523
+ def _alias_table(registers: Sequence[str]) -> dict[str, tuple[str, int]]:
524
+ """Map every alias of ``registers`` to ``(canonical register, width)``.
525
+
526
+ The table is built per register set, so ``edx`` is *canonical* on a 32-bit
527
+ binary (``ecx``/``edx``) and an alias of ``rdx`` on a 64-bit one.
528
+ """
529
+ table: dict[str, tuple[str, int]] = {}
530
+ for canonical in registers:
531
+ for alias, width in _REGISTER_ALIAS_WIDTHS.get(canonical, {canonical: 8}).items():
532
+ table.setdefault(alias, (canonical, width))
533
+ return table
534
+
535
+
536
+ def _register_read_tokens(mnemonic: str, parts: Sequence[str]) -> set[str]:
537
+ """Return the lowercased tokens an instruction *reads*.
538
+
539
+ The first operand is skipped for the mnemonics that only write it
540
+ (``mov``-family, ``lea``, ``pop``, ``setcc``), so ``mov rdi, rax`` does not
541
+ count as a read of ``rdi``. This mirrors the argument counter in
542
+ :mod:`codevanta.analysis.decompile`, so the signature keeps agreeing with
543
+ the rendered body.
544
+ """
545
+ destination_only = (
546
+ mnemonic in _MOV_MNEMONICS or mnemonic in {"lea", "pop"} or mnemonic.startswith("set")
547
+ )
548
+ tokens: set[str] = set()
549
+ for position, operand in enumerate(parts):
550
+ if position == 0 and destination_only:
551
+ continue
552
+ tokens.update(token.lower() for token in _WORD.findall(operand))
553
+ return tokens
554
+
555
+
556
+ def _plain_operands(parts: Sequence[str], *, skip_first: bool) -> list[tuple[str, int]]:
557
+ """Return ``(alias, width)`` for every register mentioned outside brackets.
558
+
559
+ Bracket expressions are handled by the dereference and index rules; a token
560
+ inside them is an address component, not the value itself.
561
+ """
562
+ hits: list[tuple[str, int]] = []
563
+ for position, operand in enumerate(parts):
564
+ if position == 0 and skip_first:
565
+ continue
566
+ if "[" in operand:
567
+ continue
568
+ for token in _WORD.findall(operand):
569
+ name = token.lower()
570
+ width = _ARGUMENT_ALIAS_WIDTHS.get(name)
571
+ if width is not None:
572
+ hits.append((name, width))
573
+ return hits
574
+
575
+
576
+ def _is_plain_register(operand: str) -> bool:
577
+ """Whether an operand is a bare register name."""
578
+ return bool(_WORD.fullmatch(operand.strip()))
579
+
580
+
581
+ def _compare_targets(mnemonic: str, parts: Sequence[str]) -> list[tuple[str, bool]]:
582
+ """Return ``(alias, compares_against_zero)`` for a comparison's operands."""
583
+ if mnemonic not in _COMPARE_MNEMONICS or len(parts) != 2:
584
+ return []
585
+ left, right = (part.strip().lower() for part in parts)
586
+ left_is_zero = left in _ZERO_TOKENS
587
+ right_is_zero = right in _ZERO_TOKENS
588
+ if mnemonic == "test" and (left == right or (left_is_zero and right_is_zero)):
589
+ return [(left, True)] if _is_plain_register(left) else []
590
+ if left_is_zero and not right_is_zero:
591
+ return [(right, True)] if _is_plain_register(right) else []
592
+ if right_is_zero and not left_is_zero:
593
+ return [(left, True)] if _is_plain_register(left) else []
594
+ targets: list[tuple[str, bool]] = []
595
+ for operand in (left, right):
596
+ if _is_plain_register(operand):
597
+ targets.append((operand, False))
598
+ return targets
599
+
600
+
601
+ # ---------------------------------------------------------------------------
602
+ # observations
603
+ # ---------------------------------------------------------------------------
604
+
605
+
606
+ @dataclass(frozen=True)
607
+ class _Observation:
608
+ """One use of an argument register, as a candidate type.
609
+
610
+ Attributes:
611
+ rule: Which inference rule fired, a key of :data:`RULE_RANKS`.
612
+ c_type: The C type this observation alone would emit.
613
+ note: The observation itself, e.g. ``"dereferenced at pointer width ..."``.
614
+ confidence: How strong this kind of evidence is on its own.
615
+ width: Width in bytes the observation was made at; breaks rank ties.
616
+ """
617
+
618
+ rule: str
619
+ c_type: str
620
+ note: str
621
+ confidence: Confidence
622
+ width: int
623
+
624
+ @property
625
+ def rank(self) -> int:
626
+ """Specificity of the rule that produced this observation."""
627
+ return RULE_RANKS[self.rule]
628
+
629
+
630
+ def _deref_observation(memory: _Memory, pointer_width: int, text: str) -> _Observation:
631
+ """Turn a pointer-width or narrow dereference into one observation."""
632
+ if memory.width >= pointer_width:
633
+ return _Observation(
634
+ rule=_RULE_POINTER_DEREF,
635
+ c_type=_pointer_type(pointer_width),
636
+ note=f"dereferenced at pointer width ({memory.text}) in '{text}'",
637
+ confidence=Confidence.MEDIUM,
638
+ width=pointer_width,
639
+ )
640
+ return _Observation(
641
+ rule=_RULE_NARROW_DEREF,
642
+ c_type=C_TYPE_VOID_POINTER,
643
+ note=(
644
+ f"dereferenced at {memory.width} byte(s), narrower than the "
645
+ f"{pointer_width}-byte pointer ({memory.text}) in '{text}'"
646
+ ),
647
+ confidence=Confidence.MEDIUM,
648
+ width=memory.width,
649
+ )
650
+
651
+
652
+ def _index_observation(memory: _Memory, text: str) -> _Observation:
653
+ """Turn a register used as an address index into a ``size_t`` observation."""
654
+ scale = f"x{memory.scale}" if memory.scale > 1 else "x1"
655
+ return _Observation(
656
+ rule=_RULE_INDEX,
657
+ c_type=C_TYPE_SIZE_T,
658
+ note=f"used as a scaled address index ({scale}) in '{text}'",
659
+ confidence=Confidence.MEDIUM,
660
+ width=4,
661
+ )
662
+
663
+
664
+ def _address_arithmetic(text: str, pointer_width: int) -> _Observation:
665
+ """Turn a base register of a ``lea`` (no dereference) into an observation."""
666
+ return _Observation(
667
+ rule=_RULE_ARITHMETIC,
668
+ c_type=_unsigned_type(pointer_width),
669
+ note=f"used in address arithmetic only, never dereferenced ({text})",
670
+ confidence=Confidence.MEDIUM,
671
+ width=pointer_width,
672
+ )
673
+
674
+
675
+ def _arithmetic_observation(text: str, width: int) -> _Observation:
676
+ """Turn integer arithmetic on a register into a width-matched observation."""
677
+ return _Observation(
678
+ rule=_RULE_ARITHMETIC,
679
+ c_type=_unsigned_type(width),
680
+ note=f"only integer arithmetic on it at {width * 8} bits ({text})",
681
+ confidence=Confidence.MEDIUM,
682
+ width=width,
683
+ )
684
+
685
+
686
+ def _copy_observation(text: str, alias: str, width: int) -> _Observation:
687
+ """Turn a whole-register copy into a width-matched observation.
688
+
689
+ A copy narrows nothing on its own: ``mov rax, rdi`` moves eight bytes, so the
690
+ value is at least a 64-bit integer. It is weaker evidence than arithmetic,
691
+ hence ``Confidence.LOW``.
692
+ """
693
+ return _Observation(
694
+ rule=_RULE_COPY,
695
+ c_type=_unsigned_type(width),
696
+ note=f"copied whole through {alias} at {width * 8} bits ({text})",
697
+ confidence=Confidence.LOW,
698
+ width=width,
699
+ )
700
+
701
+
702
+ class _RegisterEvidence:
703
+ """Everything observed about one argument register."""
704
+
705
+ def __init__(self) -> None:
706
+ self.observations: dict[tuple[str, str], tuple[_Observation, int]] = {}
707
+ self.uses: list[str] = []
708
+
709
+ def record(self, observation: _Observation) -> None:
710
+ """Add one observation, counting repeats of the same rule and type."""
711
+ key = (observation.rule, observation.c_type)
712
+ existing = self.observations.get(key)
713
+ if existing is None:
714
+ self.observations[key] = (observation, 1)
715
+ else:
716
+ self.observations[key] = (existing[0], existing[1] + 1)
717
+
718
+ def touch(self, text: str) -> None:
719
+ """Remember an instruction that mentions the register."""
720
+ if text and text not in self.uses and len(self.uses) < _MAX_USES:
721
+ self.uses.append(text)
722
+
723
+ def best(self) -> _Observation | None:
724
+ """Return the winning observation: highest rank, then widest, then first."""
725
+ winner: _Observation | None = None
726
+ best_key: tuple[int, int, int] | None = None
727
+ for order, (observation, _count) in enumerate(self.observations.values()):
728
+ key = (observation.rank, observation.width, -order)
729
+ if best_key is None or key > best_key:
730
+ best_key = key
731
+ winner = observation
732
+ return winner
733
+
734
+ def describe(self, primary: _Observation) -> str:
735
+ """Render the primary observation together with every conflicting one."""
736
+ note = ""
737
+ others: list[str] = []
738
+ for observation, count in self.observations.values():
739
+ label = observation.note if count == 1 else f"{observation.note} (x{count})"
740
+ if observation is primary:
741
+ note = label
742
+ else:
743
+ others.append(f"{label} -> {observation.c_type}")
744
+ if not others:
745
+ return note
746
+ listed = "; ".join(others[:2])
747
+ if len(others) > 2:
748
+ listed = f"{listed}; +{len(others) - 2} more"
749
+ return f"{note}; also observed: {listed}"
750
+
751
+
752
+ # ---------------------------------------------------------------------------
753
+ # validation
754
+ # ---------------------------------------------------------------------------
755
+
756
+
757
+ def _require_pointer_width(pointer_width: object) -> int:
758
+ """Validate the pointer width in bytes (1, 2, 4 or 8)."""
759
+ if isinstance(pointer_width, bool) or not isinstance(pointer_width, int):
760
+ raise AnalysisError(f"pointer_width must be an integer, got {pointer_width!r}")
761
+ if pointer_width not in INTEGER_C_TYPES:
762
+ raise AnalysisError(
763
+ f"pointer_width must be one of {sorted(INTEGER_C_TYPES)}, got {pointer_width}"
764
+ )
765
+ return pointer_width
766
+
767
+
768
+ def _argument_registers(abi: object, pointer_width: int) -> tuple[str, ...]:
769
+ """Return the argument registers of ``abi`` for a binary of that width.
770
+
771
+ A 32-bit binary always uses ``ecx``/``edx``, whatever the ABI is called: the
772
+ register file only has the 32-bit aliases, and those are what the renderer
773
+ names ``a0``/``a1``.
774
+ """
775
+ if pointer_width < 8:
776
+ return _ARGUMENTS_32
777
+ spelling = str(abi).strip().lower().replace("-", "").replace("_", "").replace(" ", "")
778
+ name = _ABI_SPELLINGS.get(spelling)
779
+ if name is None:
780
+ accepted = ", ".join(sorted(ABI_ARGUMENT_REGISTERS))
781
+ raise AnalysisError(f"unknown ABI {abi!r}; expected one of {accepted}")
782
+ return ABI_ARGUMENT_REGISTERS[name]
783
+
784
+
785
+ # ---------------------------------------------------------------------------
786
+ # argument inference
787
+ # ---------------------------------------------------------------------------
788
+
789
+
790
+ def _collect(
791
+ stream: Sequence[tuple[str, str]],
792
+ pointer_width: int,
793
+ registers: Sequence[str],
794
+ ) -> tuple[dict[str, _RegisterEvidence], int]:
795
+ """Scan the stream once and return the per-register evidence and the count.
796
+
797
+ The argument count is the highest argument register read *before the first
798
+ ``call``, plus one -- exactly the rule the pseudo-C renderer uses -- so the
799
+ signature and the rendered body always name the same parameters. Registers
800
+ below the count that are never read are still reported, as ``unknown64``.
801
+ """
802
+ evidence = {register: _RegisterEvidence() for register in registers}
803
+ aliases = _alias_table(registers)
804
+ positions = {register: index for index, register in enumerate(registers)}
805
+ count = 0
806
+ call_seen = False
807
+ for position, (mnemonic, operands) in enumerate(stream):
808
+ parts = _split_operands(operands)
809
+ text = _text(mnemonic, operands)
810
+ if mnemonic in _CALL_MNEMONICS:
811
+ call_seen = True
812
+ elif not call_seen:
813
+ for token in _register_read_tokens(mnemonic, parts):
814
+ resolved = aliases.get(token)
815
+ if resolved is not None:
816
+ count = max(count, positions[resolved[0]] + 1)
817
+
818
+ for token in set(_WORD.findall(operands.lower())):
819
+ resolved = aliases.get(token)
820
+ if resolved is not None:
821
+ evidence[resolved[0]].touch(text)
822
+
823
+ for operand in parts:
824
+ memory = _parse_memory(operand, pointer_width)
825
+ if memory is None:
826
+ continue
827
+ if memory.base is not None:
828
+ resolved = aliases.get(memory.base)
829
+ if resolved is not None:
830
+ if mnemonic == "lea":
831
+ evidence[resolved[0]].record(_address_arithmetic(text, pointer_width))
832
+ else:
833
+ evidence[resolved[0]].record(
834
+ _deref_observation(memory, pointer_width, text)
835
+ )
836
+ if memory.index is not None:
837
+ resolved = aliases.get(memory.index)
838
+ if resolved is not None:
839
+ evidence[resolved[0]].record(_index_observation(memory, text))
840
+
841
+ if mnemonic in _ARITHMETIC_MNEMONICS:
842
+ for alias, width in _plain_operands(parts, skip_first=False):
843
+ resolved = aliases.get(alias)
844
+ if resolved is not None:
845
+ evidence[resolved[0]].record(_arithmetic_observation(text, width))
846
+ elif mnemonic in _MOV_MNEMONICS:
847
+ for alias, width in _plain_operands(parts, skip_first=True):
848
+ resolved = aliases.get(alias)
849
+ if resolved is not None:
850
+ evidence[resolved[0]].record(_copy_observation(text, alias, width))
851
+
852
+ if mnemonic in _COMPARE_MNEMONICS:
853
+ following = stream[position + 1][0] if position + 1 < len(stream) else ""
854
+ if following.startswith("j") and following not in _UNCONDITIONAL_JUMPS:
855
+ for alias, zero in _compare_targets(mnemonic, parts):
856
+ resolved = aliases.get(alias)
857
+ if resolved is not None:
858
+ if zero:
859
+ note = (
860
+ f"compared with zero and branched on "
861
+ f"({text} then {following})"
862
+ )
863
+ else:
864
+ note = (
865
+ f"compared with another operand and branched on "
866
+ f"({text} then {following})"
867
+ )
868
+ evidence[resolved[0]].record(
869
+ _Observation(
870
+ rule=_RULE_COMPARE,
871
+ c_type=C_TYPE_INT32,
872
+ note=note,
873
+ confidence=Confidence.LOW,
874
+ width=4,
875
+ )
876
+ )
877
+ return evidence, count
878
+
879
+
880
+ def _argument_type(
881
+ register: str, index: int, evidence: _RegisterEvidence, pointer_width: int
882
+ ) -> ArgumentType:
883
+ """Turn one register's evidence into the reported argument type."""
884
+ best = evidence.best()
885
+ if best is None:
886
+ if evidence.uses:
887
+ note = (
888
+ f"read without a usable type witness (first use: '{evidence.uses[0]}'); "
889
+ "no rule applies"
890
+ )
891
+ else:
892
+ note = f"argument register {register} is never read; nothing to infer from"
893
+ return ArgumentType(
894
+ register=register,
895
+ name=f"a{index}",
896
+ c_type=_unknown_type(pointer_width),
897
+ evidence_note=note,
898
+ confidence=Confidence.LOW,
899
+ )
900
+ return ArgumentType(
901
+ register=register,
902
+ name=f"a{index}",
903
+ c_type=best.c_type,
904
+ evidence_note=evidence.describe(best),
905
+ confidence=best.confidence,
906
+ )
907
+
908
+
909
+ def infer_argument_types(
910
+ instructions: Iterable[Any],
911
+ pointer_width: int,
912
+ *,
913
+ abi: str = "sysv",
914
+ ) -> tuple[ArgumentType, ...]:
915
+ """Infer the C type of every argument of one function.
916
+
917
+ Args:
918
+ instructions: The decoded instructions of the function, in ascending
919
+ address order: ``IRInstruction.to_dict()`` payloads,
920
+ :class:`~codevanta.ir.instruction.IRInstruction` objects or
921
+ ``"mnemonic operands"`` strings.
922
+ pointer_width: Pointer width of the binary in bytes (``8`` for x86_64,
923
+ ``4`` for 32-bit code). It decides the pointer type and the
924
+ ``unknownNN`` placeholder.
925
+ abi: Calling convention naming the argument registers. ``"sysv"`` (the
926
+ default) and its spellings use ``rdi``/``rsi``/``rdx``/``rcx``/
927
+ ``r8``/``r9``; ``"ms64"``/``"win64"`` use ``rcx``/``rdx``/``r8``/
928
+ ``r9``. A 32-bit binary always uses ``ecx``/``edx``.
929
+
930
+ Returns:
931
+ One :class:`ArgumentType` per argument, in argument order, named
932
+ ``a0``..``a5``. The count is the highest argument register read before
933
+ the first ``call`` plus one, i.e. the same rule the pseudo-C renderer
934
+ uses for the signature; a lower register that is never read is still
935
+ reported, as the ``unknownNN`` placeholder with ``Confidence.LOW``.
936
+
937
+ Raises:
938
+ AnalysisError: When ``pointer_width`` is not 1, 2, 4 or 8, when ``abi``
939
+ is not a known calling convention, or when ``instructions`` is a
940
+ string instead of an iterable of instructions.
941
+
942
+ Note:
943
+ The same instructions always give the same result: the scan is linear,
944
+ observations are aggregated per rule and the winner is chosen by
945
+ :data:`RULE_RANKS` with deterministic tie-breaks.
946
+ """
947
+ width = _require_pointer_width(pointer_width)
948
+ registers = _argument_registers(abi, width)
949
+ stream = _stream(instructions)
950
+ evidence, count = _collect(stream, width, registers)
951
+ return tuple(
952
+ _argument_type(register, index, evidence[register], width)
953
+ for index, register in enumerate(registers[:count])
954
+ )
955
+
956
+
957
+ def _writes_first_operand(mnemonic: str) -> bool:
958
+ """Whether a mnemonic writes its first operand."""
959
+ if mnemonic in _READ_ONLY_MNEMONICS or mnemonic in _RET_MNEMONICS:
960
+ return False
961
+ return not (mnemonic.startswith("j") or mnemonic.startswith("loop"))
962
+
963
+
964
+ def _last_return_write(
965
+ stream: Sequence[tuple[str, str]], pointer_width: int
966
+ ) -> tuple[int, str, str, bool] | None:
967
+ """Return the last write to the return register before the first ``ret``.
968
+
969
+ Returns ``(width, alias, instruction text, is_setcc)`` or ``None`` when no
970
+ instruction writes the register.
971
+ """
972
+ aliases = _RETURN_ALIAS_WIDTHS.get(pointer_width, _RETURN_ALIAS_WIDTHS[8])
973
+ last: tuple[int, str, str, bool] | None = None
974
+ for mnemonic, operands in stream:
975
+ if mnemonic in _RET_MNEMONICS:
976
+ break
977
+ if not _writes_first_operand(mnemonic):
978
+ continue
979
+ parts = _split_operands(operands)
980
+ if not parts:
981
+ continue
982
+ destination = parts[0].strip().lower()
983
+ if "[" in destination: # a memory destination only *reads* the register
984
+ continue
985
+ width = aliases.get(destination)
986
+ if width is None:
987
+ continue
988
+ last = (width, destination, _text(mnemonic, operands), mnemonic.startswith("set"))
989
+ return last
990
+
991
+
992
+ def _first_call_before_ret(stream: Sequence[tuple[str, str]]) -> str:
993
+ """Return the text of the first ``call`` before the first ``ret``, if any."""
994
+ for mnemonic, operands in stream:
995
+ if mnemonic in _RET_MNEMONICS:
996
+ return ""
997
+ if mnemonic in _CALL_MNEMONICS:
998
+ return _text(mnemonic, operands)
999
+ return ""
1000
+
1001
+
1002
+ def infer_return_type(instructions: Iterable[Any], pointer_width: int) -> ReturnType:
1003
+ """Infer the C return type of one function.
1004
+
1005
+ The scan stops at the first ``ret`` and the **last write** to the return
1006
+ register decides the type: ``rax`` -> ``uint64_t``, ``eax`` -> ``uint32_t``,
1007
+ ``ax`` -> ``uint16_t``, ``al`` -> ``uint8_t``. A ``setcc`` write is reported
1008
+ as :data:`C_TYPE_INT32`, because it leaves a 0/1 boolean that C returns as an
1009
+ ``int``. A function that never writes the register returns
1010
+ :data:`C_TYPE_VOID` with ``Confidence.LOW`` -- the absence of a write is
1011
+ weaker evidence than a write -- and a ``call`` before the ``ret`` is named in
1012
+ the note, because the value would then be the callee's.
1013
+
1014
+ Args:
1015
+ instructions: The decoded instructions of the function, in ascending
1016
+ address order (see :func:`infer_argument_types` for the accepted
1017
+ shapes).
1018
+ pointer_width: Pointer width of the binary in bytes; it selects the
1019
+ return register (``rax`` on 64-bit, ``eax`` on 32-bit) and its
1020
+ aliases.
1021
+
1022
+ Returns:
1023
+ The :class:`ReturnType` together with the observation that produced it.
1024
+
1025
+ Raises:
1026
+ AnalysisError: When ``pointer_width`` is not 1, 2, 4 or 8, or when
1027
+ ``instructions`` is a string instead of an instruction iterable.
1028
+ """
1029
+ width = _require_pointer_width(pointer_width)
1030
+ stream = _stream(instructions)
1031
+ register = _RETURN_REGISTER.get(width, "rax")
1032
+ decision = _last_return_write(stream, width)
1033
+ if decision is None:
1034
+ note = f"no instruction writes {register} before the first ret"
1035
+ call = _first_call_before_ret(stream)
1036
+ if call:
1037
+ note = (
1038
+ f"no instruction writes {register} before the first ret; the value "
1039
+ f"would come from '{call}' and is left to the callee"
1040
+ )
1041
+ return ReturnType(c_type=C_TYPE_VOID, evidence_note=note, confidence=Confidence.LOW)
1042
+ write_width, alias, text, boolean = decision
1043
+ if boolean:
1044
+ return ReturnType(
1045
+ c_type=C_TYPE_INT32,
1046
+ evidence_note=f"last write to {register} is the setcc boolean '{text}' ({alias})",
1047
+ confidence=Confidence.MEDIUM,
1048
+ )
1049
+ return ReturnType(
1050
+ c_type=_unsigned_type(write_width),
1051
+ evidence_note=(
1052
+ f"last write to {register} before the ret is {write_width * 8}-bit: '{text}'"
1053
+ ),
1054
+ confidence=Confidence.MEDIUM,
1055
+ )