Skip to content

Commit d491c5e

Browse files
committed
Clean up striga.domains and add tests
1 parent 70f46d8 commit d491c5e

12 files changed

Lines changed: 740 additions & 388 deletions

docs/interpreter-plan.md

Lines changed: 33 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## 1. Motivation
44

5-
The `vmentry_concolic.py` tool contains a ~400-line LLVM IR interpreter tightly coupled to a single value domain (`SymVal` with provenance tracking). The interpreter's opcode dispatch, register routing, pointer resolution, and block/terminator handling are generic — they depend on Striga's lifting conventions, not on the provenance domain. Extracting this interpreter into Striga as a reusable primitive enables multiple analysis backends (concrete emulation, taint tracking, symbolic execution, interval analysis, instruction counting) without reimplementing the same LLVM IR walking logic.
5+
The `vmentry_concolic.py` tool contains a ~400-line LLVM IR interpreter tightly coupled to a single value domain (`SymVal` with provenance tracking). The interpreter's opcode dispatch, register routing, pointer resolution, and block/terminator handling are generic — they depend on Striga's lifting conventions, not on the provenance domain. Extracting this interpreter into Striga as a reusable primitive enables multiple analysis backends (concrete emulation, taint tracking, symbolic execution, interval analysis, and hook-based profiling) without reimplementing the same LLVM IR walking logic.
66

77
This document specifies the API shape, the required changes to `Semantics`, the interpreter core, the domain protocols, and concrete examples for each planned use case. Phases 1–3 are intended as the complete implementation specification for the first merge. Phase 4 domain examples are design sketches for follow-up work.
88

@@ -149,7 +149,7 @@ class AbstractValueDomain(ValueDomain[T], Protocol[T]):
149149
"""Extended domain protocol for multi-path fixed-point analysis (future work).
150150
151151
Separated from ValueDomain so that single-trace domains (Concrete, Provenance,
152-
Counting) do not need to stub out methods they will never use. The worklist
152+
ProfilingHooks) do not need to stub out methods they will never use. The worklist
153153
driver (section 11) requires this protocol; the single-trace Interpreter does not.
154154
See section 11 for design context and planned use cases.
155155
"""
@@ -1381,73 +1381,30 @@ def bound_jump_table(container, dispatch_rip):
13811381
rip = result
13821382
```
13831383

1384-
### 6.5. Instruction Counting / Profiling
1384+
### 6.5. Profiling via hooks
13851385

1386-
**Purpose.** Count operations by type to identify expensive handlers and measure optimization effectiveness.
1387-
1388-
**Domain.**
1386+
**Purpose.** Count executed LLVM operations, stores, and boundary events to identify expensive handlers and measure optimization effectiveness. This is intentionally hook-based rather than a value domain: profiling is about instructions that executed, not about the expression tree that contributed to one result value.
13891387

13901388
```python
1391-
@dataclass(frozen=True)
1392-
class Counted:
1393-
value: int
1394-
width: int | None
1395-
ops: dict[str, int] # opcode name → count
1396-
1397-
@staticmethod
1398-
def const(value: int, width: int | None) -> Counted:
1399-
return Counted(mask_value(value, width), width, {})
1400-
1401-
def merge_ops(self, *others: Counted, op_name: str) -> dict[str, int]:
1402-
merged = dict(self.ops)
1403-
for other in others:
1404-
for k, v in other.ops.items():
1405-
merged[k] = merged.get(k, 0) + v
1406-
merged[op_name] = merged.get(op_name, 0) + 1
1407-
return merged
1408-
1409-
1410-
class CountingDomain:
1411-
"""ValueDomain[Counted] — concrete execution with operation counting."""
1412-
1413-
def constant(self, value, width):
1414-
return Counted.const(value, width)
1415-
1416-
def unknown(self, text, width):
1417-
return Counted(0, width, {})
1418-
1419-
def binary(self, op, lhs, rhs, width):
1420-
concrete = eval_binary(op, lhs.value, rhs.value, width)
1421-
return Counted(concrete, width, lhs.merge_ops(rhs, op_name=op.name))
1422-
1423-
def icmp(self, predicate, lhs, rhs, width):
1424-
concrete = int(eval_icmp(predicate, lhs.value, rhs.value, lhs.width))
1425-
return Counted(concrete, 1, lhs.merge_ops(rhs, op_name=f"icmp_{predicate.name}"))
1389+
@dataclass
1390+
class ProfilingHooks:
1391+
instructions: int = 0
1392+
opcodes: dict[str, int] = field(default_factory=dict)
1393+
stores: int = 0
1394+
boundaries: dict[str, int] = field(default_factory=dict)
14261395

1427-
def select(self, cond, tv, fv, width):
1428-
chosen = tv if cond.value else fv
1429-
return Counted(chosen.value, width, cond.merge_ops(tv, fv, op_name="select"))
1430-
1431-
def cast(self, op, val, from_width, to_width):
1432-
if op == Opcode.Trunc:
1433-
concrete = mask_value(val.value, to_width)
1434-
elif op == Opcode.SExt:
1435-
concrete = sext_value(val.value, from_width, to_width)
1436-
else:
1437-
concrete = mask_value(val.value, to_width)
1438-
return Counted(concrete, to_width, {**val.ops, op.name: val.ops.get(op.name, 0) + 1})
1439-
1440-
def funnel_shift(self, high, low, amount, width, *, left):
1441-
concrete = eval_funnel_shift_value(high.value, low.value, amount.value, width, left=left)
1442-
return Counted(concrete, width, high.merge_ops(low, amount, op_name="funnel_shift"))
1396+
def pre_instruction(self, inst):
1397+
self.instructions += 1
1398+
name = inst.opcode.name
1399+
self.opcodes[name] = self.opcodes.get(name, 0) + 1
14431400

1444-
def concrete_bool(self, val):
1445-
return bool(val.value)
1401+
def post_store(self, inst, value, ptr):
1402+
self.stores += 1
1403+
return value
14461404

1447-
def with_width(self, val, width, *, signed=False):
1448-
if signed:
1449-
return Counted(sext_value(val.value, val.width, width), width, val.ops)
1450-
return Counted(mask_value(val.value, width), width, val.ops)
1405+
def post_boundary(self, inst, name, target, target_arg, target_ptr):
1406+
self.boundaries[name] = self.boundaries.get(name, 0) + 1
1407+
return target
14511408
```
14521409

14531410
**Usage: profile a handler.**
@@ -1459,21 +1416,23 @@ def profile_handler(container, handler_rip):
14591416
sem = Semantics(module)
14601417
sem.begin(handler_rip)
14611418

1462-
regs = CountingRegisters(sem.reg_sizes)
1463-
mem = CountingMemory(container)
1464-
interp = Interpreter(CountingDomain(), regs, mem, sem.reg_sizes,
1465-
sem.state_ty, sem.reg_indices)
1419+
regs = ConcreteRegisters(sem.reg_sizes)
1420+
mem = ConcreteMemory(bytearray(container.raw_bytes()))
1421+
hooks = ProfilingHooks()
1422+
interp = Interpreter(ConcreteDomain(), regs, mem, sem.reg_sizes,
1423+
sem.state_ty, sem.reg_indices, hooks=hooks)
14661424

14671425
rip = handler_rip
1468-
total_blocks = 0
14691426
for _ in range(10_000):
14701427
insn = sem.cs_disasm(rip, container.get_data(rip, 15))
14711428
sem.lift_instruction(insn)
14721429
result = interp.execute_block(sem.insn_blocks[rip])
1473-
total_blocks += 1
1430+
14741431
if isinstance(result, BoundaryResult):
1475-
target = result.target
1476-
print(f"Handler {handler_rip:#x}: {total_blocks} blocks, ops: {target.ops}")
1432+
print(f"Handler {handler_rip:#x}: {hooks.instructions} LLVM instructions")
1433+
print(f" opcodes: {hooks.opcodes}")
1434+
print(f" stores: {hooks.stores}")
1435+
print(f" boundaries: {hooks.boundaries}")
14771436
break
14781437
if isinstance(result, (StopResult, SymbolicBranch)):
14791438
break
@@ -1564,7 +1523,7 @@ def simulate_patch(container, rip, patch_offset, patch_bytes, input_regs):
15641523
16. `TaintDomain` + `TaintMemory`.
15651524
17. `SmtDomain` + `SmtMemory`.
15661525
18. `IntervalDomain`.
1567-
19. `CountingDomain`.
1526+
19. Hook-based profiling helpers or examples, if useful.
15681527

15691528
---
15701529

@@ -1580,7 +1539,7 @@ This suite covers BinaryShield, VMProtect, Themida `example2-virt.bin`, `minivm-
15801539

15811540
Byte-identical `summary.md` comparisons are useful as an optional review aid, but they should not be the primary gate. Refactoring may legitimately improve expression rendering or seed ordering while preserving semantics.
15821541

1583-
Broader interpreter correctness testing (lifting individual instructions, cross-validating against Unicorn, domain protocol compliance) is out of scope for the first implementation. The smoke suite plus `uv run ruff check .` and `uvx ty check` is the acceptance gate.
1542+
Domain smoke tests should execute lifted instruction sequences, not just call domain helper methods directly. At minimum, cover register writes, memory round-trips, boundary hooks, and one expression-building case for SMT. Cross-validating broad instruction semantics against Unicorn remains out of scope for the first implementation. The smoke suite plus `uv run python tests/test_interpreter_domains.py`, `uv run ruff check .`, and `uvx ty check` is the acceptance gate.
15841543

15851544
---
15861545

@@ -1601,7 +1560,6 @@ tools/
16011560
taint.py # TaintDomain, TaintRegisters, TaintMemory
16021561
smt.py # SmtDomain, SmtRegisters, SmtMemory
16031562
interval.py # IntervalDomain, IntervalRegisters
1604-
counting.py # CountingDomain, CountingRegisters
16051563
```
16061564

16071565
---
@@ -1714,7 +1672,7 @@ Not all domains are useful in a multi-path setting:
17141672
| Taint (`Tainted`) | Yes | Join is label set union; concrete value is lost but taint propagation remains sound |
17151673
| Interval | Yes | Join is enclosing interval; this is the classical use case for abstract interpretation |
17161674
| SMT | Theoretically | Join is disjunction (`ite`); expressions grow exponentially without simplification |
1717-
| Counting | No | Cost metrics are path-specific; joining counts from different paths is not meaningful |
1675+
| Profiling hooks | No | Cost metrics are path-specific; aggregate in the driver instead of joining values |
17181676

17191677
The practical multi-path domains are intervals and taint. The SMT domain can work but needs aggressive expression simplification at join points to avoid blowup.
17201678

src/striga/domains/__init__.py

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,16 @@
1-
from .concrete import ConcreteDomain, ConcreteMemory, ConcreteRegisters, ConcreteValue
2-
from .counting import Counted, CountingDomain, CountingMemory, CountingRegisters
3-
from .interval import Interval, IntervalDomain, IntervalMemory, IntervalRegisters
4-
from .smt import SmtDomain, SmtMemory, SmtRegisters, SmtTerm
5-
from .taint import TaintDomain, TaintMemory, TaintRegisters, Tainted
6-
7-
__all__ = [
8-
"ConcreteDomain",
9-
"ConcreteMemory",
10-
"ConcreteRegisters",
11-
"ConcreteValue",
12-
"Counted",
13-
"CountingDomain",
14-
"CountingMemory",
15-
"CountingRegisters",
16-
"Interval",
17-
"IntervalDomain",
18-
"IntervalMemory",
19-
"IntervalRegisters",
20-
"SmtDomain",
21-
"SmtMemory",
22-
"SmtRegisters",
23-
"SmtTerm",
24-
"TaintDomain",
25-
"TaintMemory",
26-
"TaintRegisters",
27-
"Tainted",
28-
]
1+
from .concrete import ConcreteDomain as ConcreteDomain
2+
from .concrete import ConcreteMemory as ConcreteMemory
3+
from .concrete import ConcreteRegisters as ConcreteRegisters
4+
from .concrete import ConcreteValue as ConcreteValue
5+
from .interval import Interval as Interval
6+
from .interval import IntervalDomain as IntervalDomain
7+
from .interval import IntervalMemory as IntervalMemory
8+
from .interval import IntervalRegisters as IntervalRegisters
9+
from .smt import SmtDomain as SmtDomain
10+
from .smt import SmtMemory as SmtMemory
11+
from .smt import SmtRegisters as SmtRegisters
12+
from .smt import SmtTerm as SmtTerm
13+
from .taint import TaintDomain as TaintDomain
14+
from .taint import TaintMemory as TaintMemory
15+
from .taint import TaintRegisters as TaintRegisters
16+
from .taint import Tainted as Tainted

src/striga/domains/concrete.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,3 @@ def write(self, name: str, value: ConcreteValue) -> None:
175175

176176
def width(self, name: str) -> int:
177177
return self._sizes[name]
178-
179-
180-
__all__ = ["ConcreteDomain", "ConcreteMemory", "ConcreteRegisters", "ConcreteValue"]

src/striga/domains/counting.py

Lines changed: 0 additions & 166 deletions
This file was deleted.

0 commit comments

Comments
 (0)