Module refinery.lib.scripts.au3.analysis.interpreter
One script function of an AutoIt3 tree, run over the value domain. The Phase 3 transform calls it on a call whose every argument is a literal, and replaces the call with the spelled result — which is what resolves a script-defined decoder function, the shape the obfuscator hides its payload behind.
The interpreter owns no semantics of its own: the operators and coercions are
refinery.lib.scripts.au3.analysis.values, the builtins its table, the functions the scope
model, and the expression evaluation is the constant evaluator
refinery.lib.scripts.au3.analysis.constants, asked with a frame-backed lookup and a call hook
that reaches the script functions the interpreter runs. What it owns is the run: the storage
rules a function body follows (a read finds the local first and a global second, and a write
stores a local of the run — the storage AutoIt writes wherever the gate below lets a function
run), the control flow, and the budget that keeps a runaway loop from running forever. Every
refusal and every budget overrun answers None from call — the call site stands unchanged. A
loop or case signal that reaches a function boundary is one of those refusals: no construct
consumed it, which is a shape AutoIt refuses to load.
The gate is the may-write summary: only a function whose summary is the empty set runs, because
only then does nothing it reaches change a global, and only then do the values the walk proved
at the call site hold for the whole run. The globals the run may read are exactly what
Au3PropagationModel.entry_globals hands over.
probe is the second entry point, and it replaces nothing: it runs one function body over an
empty world to learn a value — the payload of an Execute, the name a computed Call spells —
so it needs no summary and no entry globals. Its soundness is the one frame it runs: a read of
a name the run never wrote refuses, and a read after a write sees the value the run itself
stored either way, whatever storage an implicit write targets in the script. A nested call
into a script function refuses there — a callee's implicit write lands in the callee's frame
under the probe but in a global in the script, so the caller's later read of that name diverges.
Expand source code Browse git
"""
One script function of an AutoIt3 tree, run over the value domain. The Phase 3 transform calls
it on a call whose every argument is a literal, and replaces the call with the spelled result —
which is what resolves a script-defined decoder function, the shape the obfuscator hides its
payload behind.
The interpreter owns no semantics of its own: the operators and coercions are
`refinery.lib.scripts.au3.analysis.values`, the builtins its table, the functions the scope
model, and the expression evaluation is the constant evaluator
`refinery.lib.scripts.au3.analysis.constants`, asked with a frame-backed lookup and a call hook
that reaches the script functions the interpreter runs. What it owns is the run: the storage
rules a function body follows (a read finds the local first and a global second, and a write
stores a local of the run — the storage AutoIt writes wherever the gate below lets a function
run), the control flow, and the budget that keeps a runaway loop from running forever. Every
refusal and every budget overrun answers `None` from `call` — the call site stands unchanged. A
loop or case signal that reaches a function boundary is one of those refusals: no construct
consumed it, which is a shape AutoIt refuses to load.
The gate is the may-write summary: only a function whose summary is the empty set runs, because
only then does nothing it reaches change a global, and only then do the values the walk proved
at the call site hold for the whole run. The globals the run may read are exactly what
`Au3PropagationModel.entry_globals` hands over.
`probe` is the second entry point, and it replaces nothing: it runs one function body over an
empty world to learn a value — the payload of an `Execute`, the name a computed `Call` spells —
so it needs no summary and no entry globals. Its soundness is the one frame it runs: a read of
a name the run never wrote refuses, and a read after a write sees the value the run itself
stored either way, whatever storage an implicit write targets in the script. A nested call
into a script function refuses there — a callee's implicit write lands in the callee's frame
under the probe but in a global in the script, so the caller's later read of that name diverges.
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import cast
from refinery.lib.scripts import Node, Statement
from refinery.lib.scripts.au3 import ast
from refinery.lib.scripts.au3.analysis.builtins import evaluate_builtin
from refinery.lib.scripts.au3.analysis.constants import Policy, constant_value, within_limit
from refinery.lib.scripts.au3.analysis.scopes import Au3ScopeModel
from refinery.lib.scripts.au3.analysis.values import (
Au3Value,
Kind,
apply_binary,
compare,
literal_value,
of_array,
of_int,
of_number,
of_string,
to_number,
truthy,
)
from refinery.lib.scripts.au3.model import (
Au3ArrayAccess,
Au3ArrayLiteral,
Au3Assignment,
Au3CaseRange,
Au3ContinueCaseStatement,
Au3ContinueLoopStatement,
Au3DoUntilStatement,
Au3EnumDeclaration,
Au3ErrorNode,
Au3ExitLoopStatement,
Au3ExitStatement,
Au3ForInStatement,
Au3ForStatement,
Au3FunctionDeclaration,
Au3IfStatement,
Au3ReDimStatement,
Au3ReturnStatement,
Au3ScopeKind,
Au3SelectStatement,
Au3SwitchCase,
Au3SwitchStatement,
Au3Variable,
Au3VariableDeclaration,
Au3VariableDeclarator,
Au3WhileStatement,
Au3WithStatement,
Expression,
)
_MAX_INTERPRETER_ITERATIONS = 100_000
_MAX_INTERPRETER_PAYLOAD_LEN = 1_000_000
_MAX_INTERPRETER_ARRAY_LEN = 1_000_000
_MAX_INTERPRETER_DEPTH = 64
def _nested_array(value: Au3Value) -> bool:
"""
Whether *value* is an array holding an array — the one array shape the run refuses wherever
it reaches one: an argument, a ternary branch, or a result the evaluator's literal path
hands in, exactly as the array literal the run builds itself is refused.
"""
return (
value.kind is Kind.ARRAY
and any(
element.kind is Kind.ARRAY
for element in cast(tuple[Au3Value, ...], value.payload)
)
)
class _Refusal(Exception):
"""
The one way out of a run the interpreter cannot finish: an unpinned semantics, a shape the
domain does not model, or a budget overrun.
"""
class _Return(Exception):
def __init__(self, value: Au3Value):
self.value = value
class _ExitLoop(Exception):
def __init__(self, level: int):
self.level = level
class _ContinueLoop(Exception):
def __init__(self, level: int):
self.level = level
class _ContinueCase(Exception):
pass
@dataclass
class _Array:
"""
One array a run holds mutable, converted to the domain's value — a fresh copy — wherever a
read, an argument, or a result carries it out.
"""
elements: list[Au3Value]
@dataclass
class _Budget:
"""
What one top-level call may spend: loop iterations and calls together, and recursion depth.
"""
steps: int = 0
depth: int = 0
def spend(self) -> None:
self.steps += 1
if self.steps > _MAX_INTERPRETER_ITERATIONS:
raise _Refusal
class _Frame:
"""
The storage one running function sees: its locals and the globals the call site proved, and
the evaluation policy over them, its hooks bound to this frame alone. A write to a name the
run never declared declares it — the local AutoIt itself creates — and a read of a name no
storage holds refuses.
"""
def __init__(
self,
globals: Mapping[str, Au3Value],
budget: _Budget,
probing: bool = False,
):
self.globals = globals
self.budget = budget
self.probing = probing
self.locals: dict[str, Au3Value | _Array] = {}
self.policy: Policy = Policy(frozenset())
def read(self, name: str) -> Au3Value:
local = self.locals.get(name)
if local is not None:
if isinstance(local, Au3Value):
return local
return of_array(tuple(local.elements))
value = self.globals.get(name)
if value is None:
raise _Refusal
return value
def write(self, name: str, value: Au3Value) -> None:
self.locals[name] = _stored(value)
def array(self, name: str) -> _Array:
local = self.locals.get(name)
if not isinstance(local, _Array):
raise _Refusal
return local
def _stored(value: Au3Value) -> Au3Value | _Array:
if value.kind is Kind.ARRAY:
return _Array(list(cast(tuple[Au3Value, ...], value.payload)))
return value
@dataclass(frozen=True)
class Au3Probe:
"""
What one probe learned: the value its run answered — `None` where the run refused — and the
steps the run spent, which a caller that owns many probes bounds in total.
"""
value: Au3Value | None
steps: int
class Au3Interpreter:
"""
Runs one script function of a tree over the value domain. Constructed once per tree version
from the scope model and the may-write summaries the propagation walk built.
"""
def __init__(
self,
scope_model: Au3ScopeModel,
summaries: Mapping[str, frozenset[str] | None],
):
self.functions = scope_model.functions
self.function_names = scope_model.function_names
self.summaries = summaries
def call(
self,
function: Au3FunctionDeclaration,
arguments: Sequence[Au3Value],
globals: Mapping[str, Au3Value],
) -> Au3Value | None:
try:
return self._call(function, list(arguments), globals, _Budget())
except _Refusal:
return None
def probe(
self,
function: Au3FunctionDeclaration,
arguments: Sequence[Au3Value],
) -> Au3Probe:
"""
One ungated run of *function*'s body over an empty world: no summary gate, no globals,
and a nested script-function call refusing. The run computes a value — it replaces
nothing — so it needs no effect soundness: a read of a name the run never wrote refuses,
and a read after a write sees the value the run itself stored, whatever storage an
implicit write targets in the script.
"""
budget = _Budget()
try:
value = self._call(function, list(arguments), {}, budget, probing=True)
except _Refusal:
value = None
return Au3Probe(value, budget.steps)
def _call(
self,
function: Au3FunctionDeclaration,
arguments: list[Au3Value],
globals: Mapping[str, Au3Value],
budget: _Budget,
probing: bool = False,
) -> Au3Value:
name = function.name.name if function.name is not None else None
if name is None or (
not probing
and self.summaries.get(name) != frozenset()
):
raise _Refusal
if len(arguments) > len(function.params):
raise _Refusal
budget.spend()
budget.depth += 1
frame = _Frame(globals, budget, probing)
frame.policy = self._policy(frame)
try:
if budget.depth > _MAX_INTERPRETER_DEPTH:
raise _Refusal
for index, parameter in enumerate(function.params):
variable = parameter.variable
if variable is None or parameter.by_ref:
raise _Refusal
if index < len(arguments):
value = arguments[index]
else:
if parameter.default is None:
raise _Refusal
value = literal_value(parameter.default, self.function_names)
if value is None:
raise _Refusal
frame.write(variable.name, value)
self._run_body(function.body, frame)
return of_int(0)
except _Return as returned:
return returned.value
except (_ExitLoop, _ContinueLoop, _ContinueCase):
raise _Refusal
finally:
budget.depth -= 1
def _run_body(self, body: Sequence[Statement], frame: _Frame) -> None:
for statement in body:
self._run_statement(statement, frame)
def _run_statement(self, statement: Statement, frame: _Frame) -> None:
if isinstance(statement, Au3VariableDeclaration):
self._run_declaration(statement, frame)
return
if isinstance(statement, Au3EnumDeclaration):
self._run_enum(statement, frame)
return
if isinstance(statement, Au3ReDimStatement):
self._run_redim(statement, frame)
return
if isinstance(statement, Au3IfStatement):
self._run_if(statement, frame)
return
if isinstance(statement, Au3WhileStatement):
self._run_while(statement, frame)
return
if isinstance(statement, Au3DoUntilStatement):
self._run_do_until(statement, frame)
return
if isinstance(statement, Au3ForStatement):
self._run_for(statement, frame)
return
if isinstance(statement, Au3ForInStatement):
self._run_for_in(statement, frame)
return
if isinstance(statement, Au3SwitchStatement):
self._run_switch(statement, frame)
return
if isinstance(statement, Au3SelectStatement):
self._run_select(statement, frame)
return
if isinstance(statement, Au3ReturnStatement):
raise _Return(
of_int(0) if statement.value is None else self._eval(statement.value, frame))
if isinstance(statement, Au3ExitLoopStatement):
raise _ExitLoop(self._loop_level(statement.level, frame))
if isinstance(statement, Au3ContinueLoopStatement):
raise _ContinueLoop(self._loop_level(statement.level, frame))
if isinstance(statement, Au3ContinueCaseStatement):
raise _ContinueCase
if isinstance(
statement,
(
Au3ExitStatement,
Au3WithStatement,
Au3ErrorNode,
),
):
raise _Refusal
for child in statement.children():
if isinstance(child, Expression):
self._eval(child, frame)
def _run_declaration(self, statement: Au3VariableDeclaration, frame: _Frame) -> None:
if not _declares_into_local(statement.is_static, statement.scope, statement.is_const):
raise _Refusal
for declarator in statement.declarations:
self._run_declarator(declarator, frame)
def _run_declarator(self, declarator: Au3VariableDeclarator, frame: _Frame) -> None:
variable = declarator.variable
if variable is None:
raise _Refusal
if not declarator.dimensions:
value = of_string('')
if declarator.initializer is not None:
value = self._eval(declarator.initializer, frame)
frame.write(variable.name, value)
return
if len(declarator.dimensions) != 1:
raise _Refusal
dimension = declarator.dimensions[0]
if dimension is None:
initializer = self._eval(declarator.initializer, frame)
if initializer.kind is not Kind.ARRAY:
raise _Refusal
frame.write(variable.name, initializer)
return
size = self._subscript(self._eval(dimension, frame))
if not 1 <= size <= _MAX_INTERPRETER_ARRAY_LEN:
raise _Refusal
elements = [of_string('') for _ in range(size)]
if declarator.initializer is not None:
initializer = self._eval(declarator.initializer, frame)
if initializer.kind is not Kind.ARRAY:
raise _Refusal
for index, element in enumerate(cast(tuple[Au3Value, ...], initializer.payload)):
if index >= size:
break
elements[index] = element
frame.write(variable.name, of_array(tuple(elements)))
def _run_enum(self, statement: Au3EnumDeclaration, frame: _Frame) -> None:
if statement.scope is Au3ScopeKind.GLOBAL:
raise _Refusal
counter: Au3Value | None = of_int(0)
step = of_int(statement.step_value)
for declarator in statement.declarations:
variable = declarator.variable
if variable is None:
raise _Refusal
if declarator.initializer is not None:
value = self._eval(declarator.initializer, frame)
frame.write(variable.name, value)
counter = apply_binary(statement.step_operator, value, step)
continue
if counter is None:
raise _Refusal
frame.write(variable.name, counter)
counter = apply_binary(statement.step_operator, counter, step)
def _run_redim(self, statement: Au3ReDimStatement, frame: _Frame) -> None:
for target in statement.targets:
variable = target.variable
if variable is None or len(target.dimensions) != 1:
raise _Refusal
array = frame.array(variable.name)
size = self._subscript(self._eval(target.dimensions[0], frame))
if not 1 <= size <= _MAX_INTERPRETER_ARRAY_LEN:
raise _Refusal
while len(array.elements) < size:
array.elements.append(of_string(''))
del array.elements[size:]
def _run_if(self, statement: Au3IfStatement, frame: _Frame) -> None:
if self._decides(statement.test, frame):
self._run_body(statement.body, frame)
return
for elseif in statement.elseifs:
if self._decides(elseif.test, frame):
self._run_body(elseif.body, frame)
return
if statement.else_body is not None:
self._run_body(statement.else_body, frame)
def _run_while(self, statement: Au3WhileStatement, frame: _Frame) -> None:
while self._decides(statement.test, frame):
frame.budget.spend()
if not self._run_loop_body(statement.body, frame):
return
def _run_do_until(self, statement: Au3DoUntilStatement, frame: _Frame) -> None:
while True:
frame.budget.spend()
if not self._run_loop_body(statement.body, frame):
return
if self._decides(statement.test, frame):
return
def _run_for(self, statement: Au3ForStatement, frame: _Frame) -> None:
variable = statement.variable
if variable is None:
raise _Refusal
start = self._number(statement.start, frame)
limit = self._number(statement.limit, frame)
step = (
of_int(1)
if statement.step is None
else of_number(self._number(statement.step, frame))
)
number = to_number(step)
if number is None or number == 0:
raise _Refusal
frame.write(variable.name, of_number(start))
while True:
relation = '<=' if number > 0 else '>='
within = compare(relation, frame.read(variable.name), of_number(limit))
if within is None or not truthy(within):
return
frame.budget.spend()
if not self._run_loop_body(statement.body, frame):
return
advanced = apply_binary('+', frame.read(variable.name), step)
if advanced is None:
raise _Refusal
frame.write(variable.name, advanced)
def _run_for_in(self, statement: Au3ForInStatement, frame: _Frame) -> None:
variable = statement.variable
if variable is None:
raise _Refusal
iterable = self._eval(statement.iterable, frame)
if iterable.kind is not Kind.ARRAY:
raise _Refusal
for element in cast(tuple[Au3Value, ...], iterable.payload):
frame.budget.spend()
frame.write(variable.name, element)
if not self._run_loop_body(statement.body, frame):
return
def _run_switch(self, statement: Au3SwitchStatement, frame: _Frame) -> None:
discriminant = self._eval(statement.discriminant, frame)
index = self._matching_case(statement.cases, discriminant, frame)
if index is None:
return
while index < len(statement.cases):
try:
self._run_body(statement.cases[index].body, frame)
return
except _ContinueCase:
index += 1
if index >= len(statement.cases):
raise _Refusal
def _run_select(self, statement: Au3SelectStatement, frame: _Frame) -> None:
for case in statement.cases:
if case.test is None or self._decides(case.test, frame):
self._run_body(case.body, frame)
return
def _matching_case(
self,
cases: Sequence[Au3SwitchCase],
discriminant: Au3Value,
frame: _Frame,
) -> int | None:
for index, case in enumerate(cases):
if case.values is None:
return index
for value in case.values:
if self._matches(discriminant, value, frame):
return index
return None
def _matches(self, discriminant: Au3Value, value: Node, frame: _Frame) -> bool:
if isinstance(value, Au3CaseRange):
low = self._eval(value.start, frame)
high = self._eval(value.end, frame)
within = compare('<=', low, discriminant)
ending = compare('<=', discriminant, high)
if within is None or ending is None:
raise _Refusal
return truthy(within) is True and truthy(ending) is True
equal = compare('=', discriminant, self._eval(cast(Expression, value), frame))
if equal is None:
raise _Refusal
return truthy(equal) is True
def _run_loop_body(self, body: Sequence[Statement], frame: _Frame) -> bool:
"""
One iteration of a loop body: `False` when an `ExitLoop` ended the loop, `True` when the
loop continues — a `ContinueLoop` of this level among them. A signal of a deeper level
travels on, one level cheaper.
"""
try:
self._run_body(body, frame)
except _ExitLoop as signal:
if signal.level > 1:
raise _ExitLoop(signal.level - 1)
return False
except _ContinueLoop as signal:
if signal.level > 1:
raise _ContinueLoop(signal.level - 1)
return True
def _policy(self, frame: _Frame) -> Policy:
"""
The evaluation over one frame: its lookup reads the frame, its call hook runs the script
functions the callee names, and its fallback answers the shapes only the run holds.
"""
return Policy(
self.function_names,
lookup=lambda name: self._frame_value(name, frame),
limit=_MAX_INTERPRETER_PAYLOAD_LEN,
call=lambda name, arguments: self._run_call(name, arguments, frame),
fallback=lambda expression: self._eval_special(expression, frame),
)
def _eval(self, node: Expression | None, frame: _Frame) -> Au3Value:
value = self._eval_expression(node, frame)
if not within_limit(value, _MAX_INTERPRETER_PAYLOAD_LEN) or _nested_array(value):
raise _Refusal
return value
def _eval_expression(self, node: Expression | None, frame: _Frame) -> Au3Value:
if node is None:
raise _Refusal
if isinstance(node, (Au3Assignment, Au3ArrayAccess, Au3ArrayLiteral)):
special = self._eval_special(node, frame)
if special is None:
raise _Refusal
return special
value = constant_value(node, frame.policy)
if value is None:
raise _Refusal
return value
def _eval_special(self, node: Expression, frame: _Frame) -> Au3Value | None:
"""
The value of an expression only the run can hold — an assignment, an array access, an
array literal — asked where it stands inside an expression the constant evaluator walks,
or `None` where the run refuses it.
"""
try:
if isinstance(node, Au3Assignment):
return self._eval_assignment(node, frame)
if isinstance(node, Au3ArrayAccess):
return self._element(self._eval(node.array, frame), self._eval(node.index, frame))
if isinstance(node, Au3ArrayLiteral):
elements = [self._eval(element, frame) for element in node.elements]
if any(element.kind is Kind.ARRAY for element in elements):
return None
return of_array(tuple(elements))
except _Refusal:
return None
return None
def _frame_value(self, name: str, frame: _Frame) -> Au3Value | None:
"""
The value a variable read holds in *frame*, or `None` where the run stored none.
"""
try:
return frame.read(name)
except _Refusal:
return None
def _run_call(
self,
name: str,
arguments: tuple[Au3Value, ...],
frame: _Frame,
) -> Au3Value | None:
"""
The value of one call inside a run: a script function the interpreter runs — the
precedence a call site has over the builtin of the same name — then the builtin table.
Every argument answers the bounds first: the payload bound the run holds over every
value it evaluates, and the refusal of an array holding an array.
"""
if any(
not within_limit(argument, _MAX_INTERPRETER_PAYLOAD_LEN)
or _nested_array(argument)
for argument in arguments
):
return None
function = self.functions.get(name)
if function is not None:
if frame.probing:
return None
try:
return self._call(function, list(arguments), frame.globals, frame.budget)
except _Refusal:
return None
return evaluate_builtin(name, tuple(arguments))
def _eval_assignment(self, node: Au3Assignment, frame: _Frame) -> Au3Value:
variable = ast.store_target_variable(node.target)
if variable is None:
raise _Refusal
target = ast.unwrap_parens(node.target)
if isinstance(target, Au3ArrayAccess):
return self._eval_element_assignment(node, target, variable, frame)
if not isinstance(target, Au3Variable):
raise _Refusal
if node.operator == '=':
value = self._eval(node.value, frame)
else:
current = frame.read(variable.name)
operand = self._eval(node.value, frame)
value = apply_binary(node.operator.rstrip('='), current, operand)
if value is None:
raise _Refusal
frame.write(variable.name, value)
return value
def _eval_element_assignment(
self,
node: Au3Assignment,
target: Au3ArrayAccess,
variable: Au3Variable,
frame: _Frame,
) -> Au3Value:
if not isinstance(ast.unwrap_parens(target.array), Au3Variable):
raise _Refusal
array = frame.array(variable.name)
subscript = self._subscript(self._eval(target.index, frame))
if not 0 <= subscript < len(array.elements):
raise _Refusal
if node.operator == '=':
value = self._eval(node.value, frame)
else:
operand = self._eval(node.value, frame)
value = apply_binary(
node.operator.rstrip('='), array.elements[subscript], operand)
if value is None:
raise _Refusal
if value.kind is Kind.ARRAY:
raise _Refusal
array.elements[subscript] = value
return value
def _element(self, array: Au3Value, index: Au3Value) -> Au3Value:
if array.kind is not Kind.ARRAY:
raise _Refusal
elements = cast(tuple[Au3Value, ...], array.payload)
subscript = self._subscript(index)
if not 0 <= subscript < len(elements):
raise _Refusal
return elements[subscript]
def _subscript(self, index: Au3Value) -> int:
number = to_number(index)
if number is None:
raise _Refusal
return int(number)
def _number(self, node: Expression | None, frame: _Frame) -> int | float:
number = to_number(self._eval(node, frame))
if number is None:
raise _Refusal
return number
def _decides(self, node: Expression | None, frame: _Frame) -> bool:
verdict = truthy(self._eval(node, frame))
if verdict is None:
raise _Refusal
return verdict
def _loop_level(self, node: Expression | None, frame: _Frame) -> int:
if node is None:
return 1
number = to_number(self._eval(node, frame))
if number is None or not isinstance(number, int):
raise _Refusal
return max(1, number)
def _declares_into_local(is_static: bool, scope: Au3ScopeKind | None, is_const: bool) -> bool:
"""
Whether a variable declaration of this shape creates a local of the running function:
`Local`, and a `Const` of function scope. `Static` keeps its value across calls, `Global`
writes the whole script, and a `Dim` forces nothing, so each refuses.
"""
if is_static or scope is Au3ScopeKind.GLOBAL:
return False
return scope is Au3ScopeKind.LOCAL or is_const
Classes
class Au3Probe (value, steps)-
What one probe learned: the value its run answered —
Nonewhere the run refused — and the steps the run spent, which a caller that owns many probes bounds in total.Expand source code Browse git
@dataclass(frozen=True) class Au3Probe: """ What one probe learned: the value its run answered — `None` where the run refused — and the steps the run spent, which a caller that owns many probes bounds in total. """ value: Au3Value | None steps: intInstance variables
var value-
The type of the None singleton.
var steps-
The type of the None singleton.
class Au3Interpreter (scope_model, summaries)-
Runs one script function of a tree over the value domain. Constructed once per tree version from the scope model and the may-write summaries the propagation walk built.
Expand source code Browse git
class Au3Interpreter: """ Runs one script function of a tree over the value domain. Constructed once per tree version from the scope model and the may-write summaries the propagation walk built. """ def __init__( self, scope_model: Au3ScopeModel, summaries: Mapping[str, frozenset[str] | None], ): self.functions = scope_model.functions self.function_names = scope_model.function_names self.summaries = summaries def call( self, function: Au3FunctionDeclaration, arguments: Sequence[Au3Value], globals: Mapping[str, Au3Value], ) -> Au3Value | None: try: return self._call(function, list(arguments), globals, _Budget()) except _Refusal: return None def probe( self, function: Au3FunctionDeclaration, arguments: Sequence[Au3Value], ) -> Au3Probe: """ One ungated run of *function*'s body over an empty world: no summary gate, no globals, and a nested script-function call refusing. The run computes a value — it replaces nothing — so it needs no effect soundness: a read of a name the run never wrote refuses, and a read after a write sees the value the run itself stored, whatever storage an implicit write targets in the script. """ budget = _Budget() try: value = self._call(function, list(arguments), {}, budget, probing=True) except _Refusal: value = None return Au3Probe(value, budget.steps) def _call( self, function: Au3FunctionDeclaration, arguments: list[Au3Value], globals: Mapping[str, Au3Value], budget: _Budget, probing: bool = False, ) -> Au3Value: name = function.name.name if function.name is not None else None if name is None or ( not probing and self.summaries.get(name) != frozenset() ): raise _Refusal if len(arguments) > len(function.params): raise _Refusal budget.spend() budget.depth += 1 frame = _Frame(globals, budget, probing) frame.policy = self._policy(frame) try: if budget.depth > _MAX_INTERPRETER_DEPTH: raise _Refusal for index, parameter in enumerate(function.params): variable = parameter.variable if variable is None or parameter.by_ref: raise _Refusal if index < len(arguments): value = arguments[index] else: if parameter.default is None: raise _Refusal value = literal_value(parameter.default, self.function_names) if value is None: raise _Refusal frame.write(variable.name, value) self._run_body(function.body, frame) return of_int(0) except _Return as returned: return returned.value except (_ExitLoop, _ContinueLoop, _ContinueCase): raise _Refusal finally: budget.depth -= 1 def _run_body(self, body: Sequence[Statement], frame: _Frame) -> None: for statement in body: self._run_statement(statement, frame) def _run_statement(self, statement: Statement, frame: _Frame) -> None: if isinstance(statement, Au3VariableDeclaration): self._run_declaration(statement, frame) return if isinstance(statement, Au3EnumDeclaration): self._run_enum(statement, frame) return if isinstance(statement, Au3ReDimStatement): self._run_redim(statement, frame) return if isinstance(statement, Au3IfStatement): self._run_if(statement, frame) return if isinstance(statement, Au3WhileStatement): self._run_while(statement, frame) return if isinstance(statement, Au3DoUntilStatement): self._run_do_until(statement, frame) return if isinstance(statement, Au3ForStatement): self._run_for(statement, frame) return if isinstance(statement, Au3ForInStatement): self._run_for_in(statement, frame) return if isinstance(statement, Au3SwitchStatement): self._run_switch(statement, frame) return if isinstance(statement, Au3SelectStatement): self._run_select(statement, frame) return if isinstance(statement, Au3ReturnStatement): raise _Return( of_int(0) if statement.value is None else self._eval(statement.value, frame)) if isinstance(statement, Au3ExitLoopStatement): raise _ExitLoop(self._loop_level(statement.level, frame)) if isinstance(statement, Au3ContinueLoopStatement): raise _ContinueLoop(self._loop_level(statement.level, frame)) if isinstance(statement, Au3ContinueCaseStatement): raise _ContinueCase if isinstance( statement, ( Au3ExitStatement, Au3WithStatement, Au3ErrorNode, ), ): raise _Refusal for child in statement.children(): if isinstance(child, Expression): self._eval(child, frame) def _run_declaration(self, statement: Au3VariableDeclaration, frame: _Frame) -> None: if not _declares_into_local(statement.is_static, statement.scope, statement.is_const): raise _Refusal for declarator in statement.declarations: self._run_declarator(declarator, frame) def _run_declarator(self, declarator: Au3VariableDeclarator, frame: _Frame) -> None: variable = declarator.variable if variable is None: raise _Refusal if not declarator.dimensions: value = of_string('') if declarator.initializer is not None: value = self._eval(declarator.initializer, frame) frame.write(variable.name, value) return if len(declarator.dimensions) != 1: raise _Refusal dimension = declarator.dimensions[0] if dimension is None: initializer = self._eval(declarator.initializer, frame) if initializer.kind is not Kind.ARRAY: raise _Refusal frame.write(variable.name, initializer) return size = self._subscript(self._eval(dimension, frame)) if not 1 <= size <= _MAX_INTERPRETER_ARRAY_LEN: raise _Refusal elements = [of_string('') for _ in range(size)] if declarator.initializer is not None: initializer = self._eval(declarator.initializer, frame) if initializer.kind is not Kind.ARRAY: raise _Refusal for index, element in enumerate(cast(tuple[Au3Value, ...], initializer.payload)): if index >= size: break elements[index] = element frame.write(variable.name, of_array(tuple(elements))) def _run_enum(self, statement: Au3EnumDeclaration, frame: _Frame) -> None: if statement.scope is Au3ScopeKind.GLOBAL: raise _Refusal counter: Au3Value | None = of_int(0) step = of_int(statement.step_value) for declarator in statement.declarations: variable = declarator.variable if variable is None: raise _Refusal if declarator.initializer is not None: value = self._eval(declarator.initializer, frame) frame.write(variable.name, value) counter = apply_binary(statement.step_operator, value, step) continue if counter is None: raise _Refusal frame.write(variable.name, counter) counter = apply_binary(statement.step_operator, counter, step) def _run_redim(self, statement: Au3ReDimStatement, frame: _Frame) -> None: for target in statement.targets: variable = target.variable if variable is None or len(target.dimensions) != 1: raise _Refusal array = frame.array(variable.name) size = self._subscript(self._eval(target.dimensions[0], frame)) if not 1 <= size <= _MAX_INTERPRETER_ARRAY_LEN: raise _Refusal while len(array.elements) < size: array.elements.append(of_string('')) del array.elements[size:] def _run_if(self, statement: Au3IfStatement, frame: _Frame) -> None: if self._decides(statement.test, frame): self._run_body(statement.body, frame) return for elseif in statement.elseifs: if self._decides(elseif.test, frame): self._run_body(elseif.body, frame) return if statement.else_body is not None: self._run_body(statement.else_body, frame) def _run_while(self, statement: Au3WhileStatement, frame: _Frame) -> None: while self._decides(statement.test, frame): frame.budget.spend() if not self._run_loop_body(statement.body, frame): return def _run_do_until(self, statement: Au3DoUntilStatement, frame: _Frame) -> None: while True: frame.budget.spend() if not self._run_loop_body(statement.body, frame): return if self._decides(statement.test, frame): return def _run_for(self, statement: Au3ForStatement, frame: _Frame) -> None: variable = statement.variable if variable is None: raise _Refusal start = self._number(statement.start, frame) limit = self._number(statement.limit, frame) step = ( of_int(1) if statement.step is None else of_number(self._number(statement.step, frame)) ) number = to_number(step) if number is None or number == 0: raise _Refusal frame.write(variable.name, of_number(start)) while True: relation = '<=' if number > 0 else '>=' within = compare(relation, frame.read(variable.name), of_number(limit)) if within is None or not truthy(within): return frame.budget.spend() if not self._run_loop_body(statement.body, frame): return advanced = apply_binary('+', frame.read(variable.name), step) if advanced is None: raise _Refusal frame.write(variable.name, advanced) def _run_for_in(self, statement: Au3ForInStatement, frame: _Frame) -> None: variable = statement.variable if variable is None: raise _Refusal iterable = self._eval(statement.iterable, frame) if iterable.kind is not Kind.ARRAY: raise _Refusal for element in cast(tuple[Au3Value, ...], iterable.payload): frame.budget.spend() frame.write(variable.name, element) if not self._run_loop_body(statement.body, frame): return def _run_switch(self, statement: Au3SwitchStatement, frame: _Frame) -> None: discriminant = self._eval(statement.discriminant, frame) index = self._matching_case(statement.cases, discriminant, frame) if index is None: return while index < len(statement.cases): try: self._run_body(statement.cases[index].body, frame) return except _ContinueCase: index += 1 if index >= len(statement.cases): raise _Refusal def _run_select(self, statement: Au3SelectStatement, frame: _Frame) -> None: for case in statement.cases: if case.test is None or self._decides(case.test, frame): self._run_body(case.body, frame) return def _matching_case( self, cases: Sequence[Au3SwitchCase], discriminant: Au3Value, frame: _Frame, ) -> int | None: for index, case in enumerate(cases): if case.values is None: return index for value in case.values: if self._matches(discriminant, value, frame): return index return None def _matches(self, discriminant: Au3Value, value: Node, frame: _Frame) -> bool: if isinstance(value, Au3CaseRange): low = self._eval(value.start, frame) high = self._eval(value.end, frame) within = compare('<=', low, discriminant) ending = compare('<=', discriminant, high) if within is None or ending is None: raise _Refusal return truthy(within) is True and truthy(ending) is True equal = compare('=', discriminant, self._eval(cast(Expression, value), frame)) if equal is None: raise _Refusal return truthy(equal) is True def _run_loop_body(self, body: Sequence[Statement], frame: _Frame) -> bool: """ One iteration of a loop body: `False` when an `ExitLoop` ended the loop, `True` when the loop continues — a `ContinueLoop` of this level among them. A signal of a deeper level travels on, one level cheaper. """ try: self._run_body(body, frame) except _ExitLoop as signal: if signal.level > 1: raise _ExitLoop(signal.level - 1) return False except _ContinueLoop as signal: if signal.level > 1: raise _ContinueLoop(signal.level - 1) return True def _policy(self, frame: _Frame) -> Policy: """ The evaluation over one frame: its lookup reads the frame, its call hook runs the script functions the callee names, and its fallback answers the shapes only the run holds. """ return Policy( self.function_names, lookup=lambda name: self._frame_value(name, frame), limit=_MAX_INTERPRETER_PAYLOAD_LEN, call=lambda name, arguments: self._run_call(name, arguments, frame), fallback=lambda expression: self._eval_special(expression, frame), ) def _eval(self, node: Expression | None, frame: _Frame) -> Au3Value: value = self._eval_expression(node, frame) if not within_limit(value, _MAX_INTERPRETER_PAYLOAD_LEN) or _nested_array(value): raise _Refusal return value def _eval_expression(self, node: Expression | None, frame: _Frame) -> Au3Value: if node is None: raise _Refusal if isinstance(node, (Au3Assignment, Au3ArrayAccess, Au3ArrayLiteral)): special = self._eval_special(node, frame) if special is None: raise _Refusal return special value = constant_value(node, frame.policy) if value is None: raise _Refusal return value def _eval_special(self, node: Expression, frame: _Frame) -> Au3Value | None: """ The value of an expression only the run can hold — an assignment, an array access, an array literal — asked where it stands inside an expression the constant evaluator walks, or `None` where the run refuses it. """ try: if isinstance(node, Au3Assignment): return self._eval_assignment(node, frame) if isinstance(node, Au3ArrayAccess): return self._element(self._eval(node.array, frame), self._eval(node.index, frame)) if isinstance(node, Au3ArrayLiteral): elements = [self._eval(element, frame) for element in node.elements] if any(element.kind is Kind.ARRAY for element in elements): return None return of_array(tuple(elements)) except _Refusal: return None return None def _frame_value(self, name: str, frame: _Frame) -> Au3Value | None: """ The value a variable read holds in *frame*, or `None` where the run stored none. """ try: return frame.read(name) except _Refusal: return None def _run_call( self, name: str, arguments: tuple[Au3Value, ...], frame: _Frame, ) -> Au3Value | None: """ The value of one call inside a run: a script function the interpreter runs — the precedence a call site has over the builtin of the same name — then the builtin table. Every argument answers the bounds first: the payload bound the run holds over every value it evaluates, and the refusal of an array holding an array. """ if any( not within_limit(argument, _MAX_INTERPRETER_PAYLOAD_LEN) or _nested_array(argument) for argument in arguments ): return None function = self.functions.get(name) if function is not None: if frame.probing: return None try: return self._call(function, list(arguments), frame.globals, frame.budget) except _Refusal: return None return evaluate_builtin(name, tuple(arguments)) def _eval_assignment(self, node: Au3Assignment, frame: _Frame) -> Au3Value: variable = ast.store_target_variable(node.target) if variable is None: raise _Refusal target = ast.unwrap_parens(node.target) if isinstance(target, Au3ArrayAccess): return self._eval_element_assignment(node, target, variable, frame) if not isinstance(target, Au3Variable): raise _Refusal if node.operator == '=': value = self._eval(node.value, frame) else: current = frame.read(variable.name) operand = self._eval(node.value, frame) value = apply_binary(node.operator.rstrip('='), current, operand) if value is None: raise _Refusal frame.write(variable.name, value) return value def _eval_element_assignment( self, node: Au3Assignment, target: Au3ArrayAccess, variable: Au3Variable, frame: _Frame, ) -> Au3Value: if not isinstance(ast.unwrap_parens(target.array), Au3Variable): raise _Refusal array = frame.array(variable.name) subscript = self._subscript(self._eval(target.index, frame)) if not 0 <= subscript < len(array.elements): raise _Refusal if node.operator == '=': value = self._eval(node.value, frame) else: operand = self._eval(node.value, frame) value = apply_binary( node.operator.rstrip('='), array.elements[subscript], operand) if value is None: raise _Refusal if value.kind is Kind.ARRAY: raise _Refusal array.elements[subscript] = value return value def _element(self, array: Au3Value, index: Au3Value) -> Au3Value: if array.kind is not Kind.ARRAY: raise _Refusal elements = cast(tuple[Au3Value, ...], array.payload) subscript = self._subscript(index) if not 0 <= subscript < len(elements): raise _Refusal return elements[subscript] def _subscript(self, index: Au3Value) -> int: number = to_number(index) if number is None: raise _Refusal return int(number) def _number(self, node: Expression | None, frame: _Frame) -> int | float: number = to_number(self._eval(node, frame)) if number is None: raise _Refusal return number def _decides(self, node: Expression | None, frame: _Frame) -> bool: verdict = truthy(self._eval(node, frame)) if verdict is None: raise _Refusal return verdict def _loop_level(self, node: Expression | None, frame: _Frame) -> int: if node is None: return 1 number = to_number(self._eval(node, frame)) if number is None or not isinstance(number, int): raise _Refusal return max(1, number)Methods
def call(self, function, arguments, globals)-
Expand source code Browse git
def call( self, function: Au3FunctionDeclaration, arguments: Sequence[Au3Value], globals: Mapping[str, Au3Value], ) -> Au3Value | None: try: return self._call(function, list(arguments), globals, _Budget()) except _Refusal: return None def probe(self, function, arguments)-
One ungated run of function's body over an empty world: no summary gate, no globals, and a nested script-function call refusing. The run computes a value — it replaces nothing — so it needs no effect soundness: a read of a name the run never wrote refuses, and a read after a write sees the value the run itself stored, whatever storage an implicit write targets in the script.
Expand source code Browse git
def probe( self, function: Au3FunctionDeclaration, arguments: Sequence[Au3Value], ) -> Au3Probe: """ One ungated run of *function*'s body over an empty world: no summary gate, no globals, and a nested script-function call refusing. The run computes a value — it replaces nothing — so it needs no effect soundness: a read of a name the run never wrote refuses, and a read after a write sees the value the run itself stored, whatever storage an implicit write targets in the script. """ budget = _Budget() try: value = self._call(function, list(arguments), {}, budget, probing=True) except _Refusal: value = None return Au3Probe(value, budget.steps)