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,546 @@
1
+ """Jump-table resolution for absolute pointer arrays and signed 32-bit offsets.
2
+
3
+ A jump table is a run of fixed-width slots that a dispatcher indexes to reach a
4
+ case target. Two layouts are supported, selected by ``mode``:
5
+
6
+ * ``"absolute"`` (the default) -- each slot is a ``pointer_size``-wide absolute
7
+ virtual address a dispatcher branches to.
8
+ * ``"relative"`` -- each slot is a *signed 32-bit* offset added to a
9
+ caller-supplied ``base_address``, so ``target = base_address + offset``. This
10
+ is the shape compilers emit for position-independent switch tables, where the
11
+ slots stay valid under relocation because they store deltas, not addresses.
12
+
13
+ Both modes share the same guarantees:
14
+
15
+ * Every slot is decoded with :func:`codevanta.core.report.read_code`, so all
16
+ reads are bounded by the containing section's file bytes and nothing is ever
17
+ mapped or executed. The whole window is ``max_entries * pointer_size`` bytes
18
+ and the table address is validated against the binary map before a byte is
19
+ read.
20
+ * The scan stops at the first slot that is *not* a usable target: a value
21
+ outside the caller's ``min_target``/``max_target`` window, a target no section
22
+ maps, or -- when ``executable_only`` is true -- a target inside a
23
+ non-executable section. Every such stop is recorded in
24
+ :attr:`JumpTableResult.warnings`, so a caller can tell a short table from one
25
+ that ran into something unexpected.
26
+ * In absolute mode a zero pointer (the conventional table terminator) also ends
27
+ the scan. In relative mode a zero offset is *not* a terminator: it is a
28
+ legitimate delta that resolves to ``base_address``, so only the rules above
29
+ can end such a table.
30
+ * ``truncated`` is true only when the entry ceiling was reached *and* a further
31
+ slot was still usable: in absolute mode the next slot still holds a non-zero
32
+ pointer, in relative mode the next slot decodes to a target that passes the
33
+ same stop rules.
34
+
35
+ Entries carry :attr:`codevanta.evidence.confidence.Confidence.MEDIUM`: a run of
36
+ same-width slots found by shape is evidence of a dispatch table, not proof of
37
+ one, and nothing here shows that a dispatcher actually indexes the array.
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ from dataclasses import dataclass
43
+ from pathlib import Path
44
+ from typing import Any, Literal
45
+
46
+ from codevanta.core.loader import BinaryInfo, SectionInfo
47
+ from codevanta.core.report import AnalysisError, read_code
48
+ from codevanta.evidence.confidence import Confidence
49
+ from codevanta.evidence.models import Evidence
50
+
51
+ __all__ = [
52
+ "DEFAULT_MAX_ENTRIES",
53
+ "EVIDENCE_METHOD",
54
+ "EVIDENCE_METHOD_RELATIVE",
55
+ "EVIDENCE_SOURCE",
56
+ "EVIDENCE_SOURCE_RELATIVE",
57
+ "MAX_ENTRIES",
58
+ "MODES",
59
+ "MODE_ABSOLUTE",
60
+ "MODE_RELATIVE",
61
+ "RELATIVE_OFFSET_SIZE",
62
+ "JumpTableEntry",
63
+ "JumpTableResult",
64
+ "resolve_jump_table",
65
+ ]
66
+
67
+ #: Default number of slots a single resolution will decode.
68
+ DEFAULT_MAX_ENTRIES = 256
69
+
70
+ #: Hard ceiling on ``max_entries`` (bounded work: at most 32 KiB per call).
71
+ MAX_ENTRIES = 4096
72
+
73
+ #: Pointer widths accepted for ``pointer_size`` in absolute mode.
74
+ _ALLOWED_POINTER_SIZES = (4, 8)
75
+
76
+ #: Table layout used when each slot is an absolute virtual address.
77
+ MODE_ABSOLUTE = "absolute"
78
+
79
+ #: Table layout used when each slot is a signed 32-bit offset from a base.
80
+ MODE_RELATIVE = "relative"
81
+
82
+ #: Table layouts accepted for ``mode``.
83
+ MODES = (MODE_ABSOLUTE, MODE_RELATIVE)
84
+
85
+ #: Slot width, in bytes, of a relative table (one signed 32-bit offset).
86
+ RELATIVE_OFFSET_SIZE = 4
87
+
88
+ #: Evidence source label shared by every absolute-pointer record.
89
+ EVIDENCE_SOURCE = "jump_table.pointer_array"
90
+
91
+ #: Evidence method label shared by every absolute-pointer record.
92
+ EVIDENCE_METHOD = "absolute_pointers"
93
+
94
+ #: Evidence source label shared by every relative-offset record.
95
+ EVIDENCE_SOURCE_RELATIVE = "jump_table.relative_offsets"
96
+
97
+ #: Evidence method label shared by every relative-offset record.
98
+ EVIDENCE_METHOD_RELATIVE = "signed_offsets"
99
+
100
+ #: Endianness words that mean big-endian; everything else decodes little-endian.
101
+ _BIG_ENDIAN_MARKERS = ("big", "msb", "be")
102
+
103
+
104
+ @dataclass(frozen=True)
105
+ class JumpTableEntry:
106
+ """One decoded slot of a jump table.
107
+
108
+ Attributes:
109
+ index: Zero-based slot index inside the table, relative to
110
+ ``table_address``.
111
+ target: Absolute virtual address the slot resolves to. In absolute mode
112
+ that is the value stored in the slot; in relative mode it is
113
+ ``base_address + offset``.
114
+ evidence: Evidence record for this slot, pinned to the slot address and
115
+ carrying the decoded index and target.
116
+ """
117
+
118
+ index: int
119
+ target: int
120
+ evidence: Evidence
121
+
122
+ def to_dict(self) -> dict[str, Any]:
123
+ return {
124
+ "index": self.index,
125
+ "target": f"0x{self.target:x}",
126
+ "evidence": self.evidence.to_dict(),
127
+ }
128
+
129
+
130
+ @dataclass(frozen=True)
131
+ class JumpTableResult:
132
+ """Outcome of one jump-table resolution.
133
+
134
+ Attributes:
135
+ table_address: Absolute virtual address of the first decoded slot.
136
+ entries: Decoded entries in slot order (possibly empty).
137
+ pointer_size: Slot width in bytes used to decode the table (``4`` for a
138
+ relative table).
139
+ truncated: ``True`` when ``max_entries`` cut the table short even though
140
+ a further usable slot was still available.
141
+ evidence: Evidence describing the resolution as a whole.
142
+ warnings: Non-fatal reasons that stopped the scan early, or that made
143
+ the result incomplete.
144
+ """
145
+
146
+ table_address: int
147
+ entries: list[JumpTableEntry]
148
+ pointer_size: int
149
+ truncated: bool
150
+ evidence: Evidence
151
+ warnings: tuple[str, ...] = ()
152
+
153
+ def to_dict(self) -> dict[str, Any]:
154
+ return {
155
+ "table_address": f"0x{self.table_address:x}",
156
+ "entry_count": len(self.entries),
157
+ "entries": [entry.to_dict() for entry in self.entries],
158
+ "pointer_size": self.pointer_size,
159
+ "truncated": self.truncated,
160
+ "warnings": list(self.warnings),
161
+ "evidence": self.evidence.to_dict(),
162
+ }
163
+
164
+
165
+ def _byteorder(info: BinaryInfo) -> Literal["little", "big"]:
166
+ """Return the ``int.from_bytes`` byte order implied by ``info.endianness``."""
167
+ text = str(getattr(info, "endianness", "") or "").strip().lower()
168
+ return "big" if any(text.startswith(marker) for marker in _BIG_ENDIAN_MARKERS) else "little"
169
+
170
+
171
+ def _int_argument(name: str, value: Any) -> int:
172
+ """Return ``value`` when it is a plain integer, else raise."""
173
+ if isinstance(value, bool) or not isinstance(value, int):
174
+ raise AnalysisError(f"{name} must be an integer, got {value!r}")
175
+ return value
176
+
177
+
178
+ def _target_section(info: BinaryInfo, target: int) -> Any | None:
179
+ """Return the section that maps ``target`` for jump-table purposes.
180
+
181
+ Sections whose ``virtual_address`` is zero are ignored: non-allocated
182
+ sections (symbol and string tables) are commonly reported at address 0 and
183
+ would otherwise swallow small bogus targets.
184
+ """
185
+ section = info.section_for_va(target)
186
+ if section is None or section.virtual_address == 0:
187
+ return None
188
+ return section
189
+
190
+
191
+ def _resolve_pointer_size(info: BinaryInfo, pointer_size: int | None) -> int:
192
+ """Validate ``pointer_size``, defaulting it from the binary's bitness."""
193
+ if pointer_size is None:
194
+ bitness = int(getattr(info, "bitness", 0) or 0)
195
+ if bitness == 64:
196
+ return 8
197
+ if bitness == 32:
198
+ return 4
199
+ raise AnalysisError(
200
+ f"cannot infer pointer size from bitness {bitness}; pass pointer_size explicitly"
201
+ )
202
+ size = _int_argument("pointer_size", pointer_size)
203
+ if size not in _ALLOWED_POINTER_SIZES:
204
+ raise AnalysisError(f"pointer_size must be 4 or 8, got {pointer_size}")
205
+ return size
206
+
207
+
208
+ def _resolve_mode(mode: str) -> str:
209
+ """Normalise ``mode`` to one of :data:`MODES` or raise :class:`AnalysisError`."""
210
+ if not isinstance(mode, str):
211
+ raise AnalysisError(f"mode must be one of {MODES}, got {mode!r}")
212
+ text = mode.strip().lower()
213
+ if text not in MODES:
214
+ raise AnalysisError(f"mode must be one of {MODES}, got {mode!r}")
215
+ return text
216
+
217
+
218
+ def _resolve_relative_pointer_size(pointer_size: int | None) -> int:
219
+ """Return :data:`RELATIVE_OFFSET_SIZE`, rejecting any other explicit width."""
220
+ if pointer_size is None:
221
+ return RELATIVE_OFFSET_SIZE
222
+ size = _int_argument("pointer_size", pointer_size)
223
+ if size != RELATIVE_OFFSET_SIZE:
224
+ raise AnalysisError(
225
+ f"pointer_size must be {RELATIVE_OFFSET_SIZE} in relative mode "
226
+ f"(signed 32-bit offsets), got {pointer_size}"
227
+ )
228
+ return size
229
+
230
+
231
+ def _resolve_base_address(base_address: int | None) -> int:
232
+ """Return the required origin of a relative table or raise :class:`AnalysisError`."""
233
+ if base_address is None:
234
+ raise AnalysisError("relative mode requires base_address")
235
+ base = _int_argument("base_address", base_address)
236
+ if base < 0:
237
+ raise AnalysisError(f"invalid base_address 0x{base:x}")
238
+ return base
239
+
240
+
241
+ def _stop_reason(
242
+ index: int,
243
+ target: int,
244
+ info: BinaryInfo,
245
+ low: int | None,
246
+ high: int | None,
247
+ executable_only: bool,
248
+ ) -> str | None:
249
+ """Return why ``target`` cannot extend the table, or ``None`` when it can.
250
+
251
+ The checks are purely metadata lookups: the target is never read, mapped or
252
+ executed. They are shared by both table layouts, so a relative table stops on
253
+ exactly the same boundaries as an absolute one.
254
+ """
255
+ if low is not None and target < low:
256
+ return (
257
+ f"index {index}: target 0x{target:x} is below min_target 0x{low:x}; "
258
+ "table scan stopped"
259
+ )
260
+ if high is not None and target > high:
261
+ return (
262
+ f"index {index}: target 0x{target:x} is above max_target 0x{high:x}; "
263
+ "table scan stopped"
264
+ )
265
+ target_section = _target_section(info, target)
266
+ if target_section is None:
267
+ return (
268
+ f"index {index}: target 0x{target:x} is not mapped by any section; "
269
+ "table scan stopped"
270
+ )
271
+ if executable_only and not target_section.is_executable:
272
+ return (
273
+ f"index {index}: target 0x{target:x} is inside non-executable section "
274
+ f"{target_section.name!r}; table scan stopped"
275
+ )
276
+ return None
277
+
278
+
279
+ def _validate_table_address(info: BinaryInfo, table_address: int) -> SectionInfo:
280
+ """Return the section backing ``table_address`` or raise :class:`AnalysisError`."""
281
+ address = _int_argument("table_address", table_address)
282
+ if address < 0:
283
+ raise AnalysisError(f"invalid table_address 0x{address:x}")
284
+ section = info.section_for_va(address)
285
+ if section is None:
286
+ raise AnalysisError(f"table address 0x{address:x} is not inside any section")
287
+ if info.va_to_offset(address) is None:
288
+ raise AnalysisError(f"table address 0x{address:x} is not mapped to file bytes")
289
+ return section
290
+
291
+
292
+ def _entry_evidence(index: int, slot_address: int, target: int) -> Evidence:
293
+ """Build the evidence record for one decoded pointer slot."""
294
+ return Evidence(
295
+ source=EVIDENCE_SOURCE,
296
+ method=EVIDENCE_METHOD,
297
+ confidence=Confidence.MEDIUM,
298
+ address=slot_address,
299
+ detail=f"pointer #{index} at 0x{slot_address:x} -> 0x{target:x}",
300
+ extra={"index": index, "target": f"0x{target:x}"},
301
+ )
302
+
303
+
304
+ def _result_evidence(
305
+ table_address: int, entry_count: int, pointer_size: int, truncated: bool
306
+ ) -> Evidence:
307
+ """Build the evidence record describing the resolution as a whole."""
308
+ return Evidence(
309
+ source=EVIDENCE_SOURCE,
310
+ method=EVIDENCE_METHOD,
311
+ confidence=Confidence.MEDIUM,
312
+ address=table_address,
313
+ detail=(
314
+ f"decoded {entry_count} absolute pointer(s) of {pointer_size} byte(s) "
315
+ f"from 0x{table_address:x}"
316
+ ),
317
+ extra={
318
+ "entries": entry_count,
319
+ "pointer_size": pointer_size,
320
+ "truncated": truncated,
321
+ },
322
+ )
323
+
324
+
325
+ def _relative_entry_evidence(
326
+ index: int, slot_address: int, target: int, base_address: int
327
+ ) -> Evidence:
328
+ """Build the evidence record for one signed-offset slot."""
329
+ return Evidence(
330
+ source=EVIDENCE_SOURCE_RELATIVE,
331
+ method=EVIDENCE_METHOD_RELATIVE,
332
+ confidence=Confidence.MEDIUM,
333
+ address=slot_address,
334
+ detail=(
335
+ f"offset #{index} at 0x{slot_address:x} against base 0x{base_address:x} "
336
+ f"-> 0x{target:x}"
337
+ ),
338
+ extra={
339
+ "index": index,
340
+ "target": f"0x{target:x}",
341
+ "base": f"0x{base_address:x}",
342
+ },
343
+ )
344
+
345
+
346
+ def _relative_result_evidence(
347
+ table_address: int, entry_count: int, base_address: int, truncated: bool
348
+ ) -> Evidence:
349
+ """Build the evidence record describing a whole relative resolution."""
350
+ return Evidence(
351
+ source=EVIDENCE_SOURCE_RELATIVE,
352
+ method=EVIDENCE_METHOD_RELATIVE,
353
+ confidence=Confidence.MEDIUM,
354
+ address=table_address,
355
+ detail=(
356
+ f"resolved {entry_count} signed 32-bit offset(s) against base "
357
+ f"0x{base_address:x} from 0x{table_address:x}"
358
+ ),
359
+ extra={
360
+ "entries": entry_count,
361
+ "pointer_size": RELATIVE_OFFSET_SIZE,
362
+ "truncated": truncated,
363
+ "base": f"0x{base_address:x}",
364
+ },
365
+ )
366
+
367
+
368
+ def resolve_jump_table(
369
+ path: str | Path,
370
+ info: BinaryInfo,
371
+ table_address: int,
372
+ max_entries: int = DEFAULT_MAX_ENTRIES,
373
+ pointer_size: int | None = None,
374
+ executable_only: bool = True,
375
+ min_target: int | None = None,
376
+ max_target: int | None = None,
377
+ mode: str = MODE_ABSOLUTE,
378
+ base_address: int | None = None,
379
+ ) -> JumpTableResult:
380
+ """Decode a jump table starting at ``table_address``.
381
+
382
+ With ``mode="absolute"`` (the default) the table is read as ``max_entries``
383
+ consecutive ``pointer_size``-wide absolute addresses. With
384
+ ``mode="relative"`` it is read as consecutive *signed 32-bit* offsets, each
385
+ resolved against ``base_address``.
386
+
387
+ Values are decoded with ``info.endianness``. In both modes the scan stops at
388
+ the first slot that cannot be a target: a value outside
389
+ ``[min_target, max_target]``, a target no section maps, or -- with
390
+ ``executable_only`` -- a target inside a non-executable section. Each stop is
391
+ explained in :attr:`JumpTableResult.warnings`. A zero slot additionally
392
+ terminates an absolute table; a zero offset in a relative table is a real
393
+ delta that resolves to ``base_address`` and terminates nothing.
394
+
395
+ Args:
396
+ path: Binary to read.
397
+ info: Metadata for ``path``; its ``bitness``, ``endianness`` and
398
+ ``sections`` drive the decode.
399
+ table_address: Absolute virtual address of the first slot.
400
+ max_entries: Maximum number of slots to decode, ``1 .. MAX_ENTRIES``.
401
+ pointer_size: Slot width in bytes. In absolute mode ``4`` or ``8``, with
402
+ ``None`` selecting 8 for a 64-bit binary and 4 for a 32-bit one. In
403
+ relative mode only ``4`` is valid, and ``None`` selects it.
404
+ executable_only: When true (default), a target inside a non-executable
405
+ section ends the scan with a warning.
406
+ min_target: Optional inclusive lower bound for accepted targets, in
407
+ absolute address space in both modes.
408
+ max_target: Optional inclusive upper bound for accepted targets.
409
+ mode: ``"absolute"`` (default) or ``"relative"``; case-insensitive.
410
+ base_address: Origin the signed offsets of a relative table are added to.
411
+ Required in relative mode; ignored in absolute mode.
412
+
413
+ Returns:
414
+ The decoded table with per-slot and summary evidence. Relative records
415
+ use source ``jump_table.relative_offsets`` and method
416
+ ``signed_offsets``, with the base in the detail.
417
+
418
+ Raises:
419
+ AnalysisError: If ``mode`` is unknown, if ``max_entries``,
420
+ ``pointer_size``, ``table_address`` or the
421
+ ``min_target``/``max_target`` window is invalid, if the table
422
+ address is not backed by file bytes, or if relative mode is
423
+ requested without a usable ``base_address``.
424
+
425
+ Note:
426
+ **Bounded work.** At most ``max_entries * pointer_size`` bytes are read
427
+ for the table itself, plus a single slot-sized peek used to decide
428
+ ``truncated``. Nothing is loaded, mapped or executed; the file is only
429
+ read through :func:`codevanta.core.report.read_code`, which never
430
+ returns more bytes than the containing section maps. Relative targets
431
+ are validated by metadata lookup only, so a table cannot make this
432
+ function read code at the addresses it names.
433
+ """
434
+ relative = _resolve_mode(mode) == MODE_RELATIVE
435
+ limit = _int_argument("max_entries", max_entries)
436
+ if limit < 1:
437
+ raise AnalysisError("max_entries must be >= 1")
438
+ if limit > MAX_ENTRIES:
439
+ raise AnalysisError(f"max_entries too large (limit {MAX_ENTRIES})")
440
+ # ``base`` is not None exactly when the table is relative; that single fact
441
+ # selects signed decoding, offset arithmetic and the evidence labels below.
442
+ base: int | None = None
443
+ if relative:
444
+ size = _resolve_relative_pointer_size(pointer_size)
445
+ base = _resolve_base_address(base_address)
446
+ else:
447
+ size = _resolve_pointer_size(info, pointer_size)
448
+ low = None if min_target is None else _int_argument("min_target", min_target)
449
+ high = None if max_target is None else _int_argument("max_target", max_target)
450
+ if low is not None and high is not None and low > high:
451
+ raise AnalysisError("min_target must be <= max_target")
452
+ section = _validate_table_address(info, table_address)
453
+ table = _int_argument("table_address", table_address)
454
+ byteorder = _byteorder(info)
455
+
456
+ window = limit * size
457
+ data = read_code(path, info, table, window)
458
+ slots = len(data) // size
459
+ if slots < 1:
460
+ raise AnalysisError(
461
+ f"table address 0x{table:x} has fewer than {size} file bytes "
462
+ f"in section {section.name!r}"
463
+ )
464
+
465
+ entries: list[JumpTableEntry] = []
466
+ warnings: list[str] = []
467
+ stopped = False
468
+
469
+ for index in range(slots):
470
+ slot_address = table + index * size
471
+ raw = data[index * size : (index + 1) * size]
472
+ value = (
473
+ int.from_bytes(raw, byteorder, signed=True)
474
+ if relative
475
+ else int.from_bytes(raw, byteorder)
476
+ )
477
+ target = value if base is None else base + value
478
+ if not relative and target == 0:
479
+ warnings.append(
480
+ f"zero pointer at index {index} (0x{slot_address:x}) terminates the table"
481
+ )
482
+ stopped = True
483
+ break
484
+ reason = _stop_reason(index, target, info, low, high, executable_only)
485
+ if reason is not None:
486
+ warnings.append(reason)
487
+ stopped = True
488
+ break
489
+ entries.append(
490
+ JumpTableEntry(
491
+ index=index,
492
+ target=target,
493
+ evidence=(
494
+ _entry_evidence(index, slot_address, target)
495
+ if base is None
496
+ else _relative_entry_evidence(index, slot_address, target, base)
497
+ ),
498
+ )
499
+ )
500
+
501
+ truncated = False
502
+ if not stopped and slots >= limit:
503
+ # The ceiling ended the scan; only call it truncated when a further slot
504
+ # is actually readable past the requested window and still usable.
505
+ try:
506
+ peek = read_code(path, info, table + limit * size, size)
507
+ except (AnalysisError, OSError):
508
+ peek = b""
509
+ if len(peek) == size:
510
+ if base is None:
511
+ truncated = int.from_bytes(peek, byteorder) != 0
512
+ else:
513
+ # No zero terminator exists here: only a delta that resolves to
514
+ # an acceptable target proves the table continues.
515
+ truncated = (
516
+ _stop_reason(
517
+ limit,
518
+ base + int.from_bytes(peek, byteorder, signed=True),
519
+ info,
520
+ low,
521
+ high,
522
+ executable_only,
523
+ )
524
+ is None
525
+ )
526
+
527
+ if not stopped and len(data) < window:
528
+ # The section ended before the requested window: a short table is
529
+ # indistinguishable from one that is cut off, so say so explicitly.
530
+ warnings.append(
531
+ f"table scan reached the end of section {section.name!r} after {slots} "
532
+ "slot(s); the table may be cut off"
533
+ )
534
+
535
+ return JumpTableResult(
536
+ table_address=table,
537
+ entries=entries,
538
+ pointer_size=size,
539
+ truncated=truncated,
540
+ evidence=(
541
+ _result_evidence(table, len(entries), size, truncated)
542
+ if base is None
543
+ else _relative_result_evidence(table, len(entries), base, truncated)
544
+ ),
545
+ warnings=tuple(warnings),
546
+ )