Module refinery.lib.scripts.js.analysis.tampering
The anchored tampering oracle: whether the built-ins a fold or an interpreted execution is about to trust still mean what the language says at one point in the program — the anchor.
The trust questions the effect model answers (EffectModel.trusted_intrinsic,
EffectModel.trusted_prototype, EffectModel.read_chain_intact) are asked without a point: a
reflective surface anywhere withdraws trust everywhere, because code could have replaced a built-in
before any use. That refusal is sound and costs recall in proportion to the program — the obfuscated
files this pipeline reads carry their decoder in an eval or a Function construction, so every
later fold is refused even where the construction provably runs after the fold it blocks.
TamperingModel.builtins_intact_at() asks the same question with the point supplied, as the
conjunction of:
- program-wide refusals: no static import, no
with, no span of source the model never read — facts that cannot be ordered against an anchor at all; - a site enumeration by forward value-flow — every store through a base that may be the global
object under a key only the runtime resolves, every read of the global object in an argument
position, every code-execution surface, every load of code the text does not hold
(
SemanticModel.code_load_sites), and every recognized construction, followed to the positions that obtain its value: a callee position is an invocation of it, a binding initializer flows onward through that binding's reads, and any other position is an unreadable escape; - multiplicity: the anchor executes at most once, counted by invocation capability — the
activation chain up to the script root holds exactly one alias-resolved invocation each, off
cycle, with no
callee/callerre-entry spelling and no activation the host may invoke freely; - ordering: every site is guaranteed to run after the anchor
(
DominanceModel.runs_before, the interprocedural first-execution guarantee).
With no sites there is nothing a re-execution could cross, so the answer is True without the
multiplicity walk — which is what keeps an ordinary clean program foldable at every anchor in it.
The per-name arms (EffectModel.trusted_intrinsic's written-name check,
EffectModel.trusted_prototype's owner check, EffectModel.global_key_written) stay independent of
the oracle: they answer attributed writes, the oracle answers unattributable execution, and a
consumer composes the two — the oracle first, then the per-name arm. A consumer without an anchor
keeps the program-wide question; see the consumers of ModelCache.builtins_intact_at for what
asking the oracle presupposes.
The enumeration is model-backed — computed once per model lifetime and memoized, never re-derived — because it runs inside the pinned-model window of the reflection pass, where splices change the tree underneath the held models. Its soundness there rests on two legs. A splice that removes a site leaves the stale site in a memoized list, so the answer stays refused. And every predicate the enumeration asks fails closed on nodes the model cannot place: a member-write base, an argument-position read, or a callee the model never saw is treated as may-be-global — a site — so code spliced in after the model was built refuses rather than clears.
Expand source code Browse git
"""
The anchored tampering oracle: whether the built-ins a fold or an interpreted execution is about to
trust still mean what the language says at one point in the program — the anchor.
The trust questions the effect model answers (`EffectModel.trusted_intrinsic`,
`EffectModel.trusted_prototype`, `EffectModel.read_chain_intact`) are asked without a point: a
reflective surface anywhere withdraws trust everywhere, because code could have replaced a built-in
before any use. That refusal is sound and costs recall in proportion to the program — the obfuscated
files this pipeline reads carry their decoder in an `eval` or a `Function` construction, so every
later fold is refused even where the construction provably runs after the fold it blocks.
`TamperingModel.builtins_intact_at` asks the same question with the point supplied, as the
conjunction of:
- program-wide refusals: no static import, no `with`, no span of source the model never read —
facts that cannot be ordered against an anchor at all;
- a site enumeration by forward value-flow — every store through a base that may be the global
object under a key only the runtime resolves, every read of the global object in an argument
position, every code-execution surface, every load of code the text does not hold
(`SemanticModel.code_load_sites`), and every recognized construction, followed to the
positions that obtain its value: a callee position is an invocation of it, a binding initializer
flows onward through that binding's reads, and any other position is an unreadable escape;
- multiplicity: the anchor executes at most once, counted by invocation *capability* — the
activation chain up to the script root holds exactly one alias-resolved invocation each, off
cycle, with no `callee`/`caller` re-entry spelling and no activation the host may invoke freely;
- ordering: every site is guaranteed to run after the anchor
(`DominanceModel.runs_before`, the interprocedural first-execution guarantee).
With no sites there is nothing a re-execution could cross, so the answer is `True` without the
multiplicity walk — which is what keeps an ordinary clean program foldable at every anchor in it.
The per-name arms (`EffectModel.trusted_intrinsic`'s written-name check,
`EffectModel.trusted_prototype`'s owner check, `EffectModel.global_key_written`) stay independent of
the oracle: they answer attributed writes, the oracle answers unattributable execution, and a
consumer composes the two — the oracle first, then the per-name arm. A consumer without an anchor
keeps the program-wide question; see the consumers of `ModelCache.builtins_intact_at` for what
asking the oracle presupposes.
The enumeration is model-backed — computed once per model lifetime and memoized, never re-derived —
because it runs inside the pinned-model window of the reflection pass, where splices change the tree
underneath the held models. Its soundness there rests on two legs. A splice that removes a site
leaves the stale site in a memoized list, so the answer stays refused. And every predicate the
enumeration asks fails closed on nodes the model cannot place: a member-write base, an
argument-position read, or a callee the model never saw is treated as may-be-global — a site — so
code spliced in after the model was built refuses rather than clears.
"""
from __future__ import annotations
from typing import Callable, Collection, NamedTuple
from refinery.lib.scripts import Node
from refinery.lib.scripts.analysis.cycles import CycleModel
from refinery.lib.scripts.js.analysis.dominance import DominanceModel
from refinery.lib.scripts.js.analysis.effects import AnchoredTrust, EffectModel, side_effect_free
from refinery.lib.scripts.js.analysis.evaluation import enclosing_run
from refinery.lib.scripts.js.analysis.model import (
Binding,
ScopeKind,
SemanticModel,
_pattern_reflective_exposure,
is_unread_source,
opaque_global_write,
)
from refinery.lib.scripts.js.keys import (
NO_IS_STRICTER,
KeyAssumptions,
key_is,
key_spells,
key_text,
)
from refinery.lib.scripts.js.model import (
FUNCTION_NODES,
JsAssignmentExpression,
JsCallExpression,
JsExpressionStatement,
JsIdentifier,
JsImportDeclaration,
JsImportExpression,
JsMemberExpression,
JsNewExpression,
JsObjectPattern,
JsParenthesizedExpression,
JsProperty,
JsVariableDeclarator,
JsWithStatement,
file_ended_inside,
strip_parens,
wraps_return,
)
#: How many name-to-value hops the recognizer follows in search of the `Function` intrinsic. A
#: chain longer than any real spelling of it is a cycle, and the limit answers undecided
#: before walking it.
_CONSTRUCTOR_ALIAS_LIMIT = 8
_EVAL = frozenset({'eval'})
def _key_is_known(member: JsMemberExpression) -> bool:
"""
Whether the key of *member* is statically known (`refinery.lib.scripts.js.keys.key_text`) and no
literal the file ended inside.
"""
key = member.property
return key is not None and not file_ended_inside(key) and key_text(member) is not None
def _spells_constructor(member: JsMemberExpression) -> bool:
"""
Whether *member* certainly reads `constructor` (`refinery.lib.scripts.js.keys.key_spells`) under a
key that is no literal the file ended inside.
"""
return _key_is_known(member) and key_spells(member, 'constructor')
def denotes_function_intrinsic(
callee: Node | None,
model: SemanticModel,
effects: EffectModel,
dominance: DominanceModel,
*,
eval_string: Callable[[Node | None], str | None] | None = None,
read_effect: Callable[[Node], bool] | None = None,
positioned_value: Callable[[Binding, Node], Node | None] | None = None,
spliced_names: Collection[str] = (),
depth: int = 0,
builtin_function: Callable[[Node], bool] | None = None,
) -> bool | None:
"""
Whether *callee* denotes the `Function` intrinsic — the callee every spelling of a code
construction calls — or `None` when that cannot be decided. `True` covers the spellings the
text can pin: the bare free global, a `.constructor` navigation off a literal or off a name the
model pins to one plain function, and a name holding one of those, transitively through
`SemanticModel.singular_value` up to the alias-hop limit. `None` marks the shapes no reading of
the text decides — a node the model cannot place, a computed key no string resolution reads, an
alias chain past the limit — and `False` everything else, including every callee that provably
denotes some other value.
The one gate every navigation spelling passes first: a program that writes the `constructor`
key on a chain rooted at `Function` has replaced the intrinsic every navigation hands out, so
all of them are refused at once. A callee the recognizer declines leaves its construction
unrecognized, which is sound for the callers this package serves — the value a construction
produces can only reach a callee position through a position the tampering enumeration reads,
so an unrecognized spelling costs recall there and never a site.
Every name-to-value hop of an alias chain is ordered against the read that resolves it: the
name's single value must be established before the hop's identifier is read, or the name
spelled `undefined` there and the construction threw. A chain is a value *at the moment each
hop reads it*, which is what the recursion's node positions carry and why the check sits on
the hop rather than on the invocation that started the walk.
*eval_string* resolves a computed key the model cannot read by folding; a caller with no folder
passes `None` and every unresolvable key answers `None` — undecided. *read_effect* rejects a
navigation base whose evaluation fires an effect; without it more bases read as side-effect
free, which recognizes more constructions and never fewer. *positioned_value* resolves the two
alias hops with the read's position supplied — the value the name holds at the moment it is
read (`TamperingModel.singular_value_at`), so a binding the program-wide volatility question
refuses can still answer where ordering proves the read safe. It is asked only where
`SemanticModel.singular_value` has no value, so every verdict the stock sequence already gives
is kept and the repair recognizes only what stock declines; on that path the member hop's
establishment question is answered by the query's own leg — the same
`SemanticModel.binding_establishment_sites` rules — and the stock
`DominanceModel.binding_established_before` call runs only beside the stock value.
*builtin_function* says whether a node denotes a function the language installs, read without
an effect, whose `constructor` read still reaches `Function`: `[].at.constructor` is `Function`
as `f.constructor` is for a plain function `f`. Only a caller that evaluates such reads can say
so; without it no such base is recognized.
"""
return _denotes_function_intrinsic(
callee, model, effects, dominance,
eval_string=eval_string, read_effect=read_effect,
positioned_value=positioned_value, spliced_names=spliced_names,
depth=depth, aliases=None, builtin_function=builtin_function,
)
def function_intrinsic_aliases(
callee: Node | None,
model: SemanticModel,
effects: EffectModel,
dominance: DominanceModel,
*,
eval_string: Callable[[Node | None], str | None] | None = None,
read_effect: Callable[[Node], bool] | None = None,
positioned_value: Callable[[Binding, Node], Node | None] | None = None,
spliced_names: Collection[str] = (),
builtin_function: Callable[[Node], bool] | None = None,
) -> list[Binding] | None:
"""
The name-to-value bindings *callee* resolves through to the `Function` intrinsic — one entry
per identifier hop of the spelling, innermost first — or `None` where it does not provably
denote it. The verdict is `denotes_function_intrinsic`'s, read off the same walk, so the two
can never disagree about a callee; the chain is what a fold that consumes the construction
together with the names it was spelled through has to delete, and the hop ordering the walk
already proves is what makes each of those declarators hold the value the construction read.
"""
aliases: list[Binding] = []
verdict = _denotes_function_intrinsic(
callee, model, effects, dominance,
eval_string=eval_string, read_effect=read_effect,
positioned_value=positioned_value, spliced_names=spliced_names,
depth=0, aliases=aliases, builtin_function=builtin_function,
)
if verdict is True:
return aliases
return None
def _denotes_function_intrinsic(
callee: Node | None,
model: SemanticModel,
effects: EffectModel,
dominance: DominanceModel,
*,
eval_string: Callable[[Node | None], str | None] | None,
read_effect: Callable[[Node], bool] | None,
positioned_value: Callable[[Binding, Node], Node | None] | None,
spliced_names: Collection[str],
depth: int,
aliases: list[Binding] | None,
builtin_function: Callable[[Node], bool] | None,
) -> bool | None:
def resolved_member(member: JsMemberExpression) -> bool | None:
if model.scope_of(member) is None:
return None
if not _key_is_known(member):
if member.computed:
key = eval_string(member.property) if eval_string is not None else None
if key != 'constructor':
return None
else:
return False
base = strip_parens(member.object)
if not isinstance(base, JsIdentifier) or base.name in spliced_names:
return False
if model.scope_of(base) is None:
return None
binding = model.resolve(base)
if binding is None:
return False
value = model.singular_value(binding)
if value is None and positioned_value is not None:
value = positioned_value(binding, base)
if value is not None:
return isinstance(value, FUNCTION_NODES) and not wraps_return(value)
if not isinstance(value, FUNCTION_NODES) or wraps_return(value):
return False
return dominance.binding_established_before(binding, member)
expr = strip_parens(callee)
if expr is None or depth > _CONSTRUCTOR_ALIAS_LIMIT:
return None
if isinstance(expr, JsIdentifier):
if expr.name in spliced_names:
return False
if model.scope_of(expr) is None:
return None
binding = model.resolve(expr)
if binding is None:
return expr.name == 'Function' and not model.read_has_dynamic_effect(expr)
if effects.global_key_written('Function', 'constructor'):
return False
if aliases is not None:
aliases.append(binding)
value = model.singular_value(binding)
if value is not None:
if not dominance.binding_established_before(binding, expr):
return False
elif positioned_value is not None:
value = positioned_value(binding, expr)
if value is None:
return False
return _denotes_function_intrinsic(
value, model, effects, dominance,
eval_string=eval_string, read_effect=read_effect,
positioned_value=positioned_value,
spliced_names=spliced_names, depth=depth + 1, aliases=aliases,
builtin_function=builtin_function,
)
if effects.global_key_written('Function', 'constructor'):
return False
if isinstance(expr, JsMemberExpression):
return _denotes_function_constructor(
expr,
read_effect,
resolved_member,
builtin_function,
assumptions=model.assumed_keys,
)
return False
def _denotes_function_constructor(
expr: JsMemberExpression,
read_effect: Callable[[Node], bool] | None = None,
resolved_member: Callable[[JsMemberExpression], bool | None] | None = None,
builtin_function: Callable[[Node], bool] | None = None,
*,
assumptions: KeyAssumptions,
) -> bool | None:
"""
Whether *expr* evaluates to the `Function` intrinsic, reached by `.constructor` navigation from
a side-effect-free base. `Function` is what the reflective `Function("code")` idiom calls, so a
callee that denotes it under another spelling constructs a function from the same code. Two
spellings reach it without the model:
<function literal>.constructor (a plain function or arrow literal)
<literal>.constructor.constructor (any side-effect-free base)
A plain function or arrow literal's own `.constructor` is `Function`, since every ordinary
function is an instance of `Function`; an `async` or generator literal is refused, its
`.constructor` being `AsyncFunction` or `GeneratorFunction`, which build a coroutine or
generator body rather than the plain function `Function` builds. Any value's
`.constructor.constructor` is `Function`, because the first hop yields that value's constructor
— itself a function — whose own `.constructor` is `Function`. Inlining discards the evaluation
of the base, so it must be side-effect free; a function literal always is, and for the double
hop *read_effect* rejects a bare-identifier base that resolves through a `with` body's dynamic
scope (firing a getter or throwing), which the model-free check cannot see.
The one spelling left is a `.constructor` read whose base is a *name* rather than a literal —
`f.constructor`, `f[key]` — where only the model can pin the base to one function value and
only the caller can read the key. That arm is *resolved_member*, a resolver the caller injects
for exactly those questions, keeping this predicate model-free. A base that is a function the
language installs, `[].at`, is one only *builtin_function* can recognize, for the same reason.
"""
if not _spells_constructor(expr):
return resolved_member is not None and resolved_member(expr)
base = strip_parens(expr.object)
if base is None:
return False
if isinstance(base, FUNCTION_NODES):
return not wraps_return(base)
if isinstance(base, JsMemberExpression) and _spells_constructor(base):
inner = base.object
return inner is not None and side_effect_free(inner, read_effect=read_effect, assumptions=assumptions)
if builtin_function is not None and builtin_function(base):
return True
return resolved_member is not None and resolved_member(expr)
class _Flow(NamedTuple):
"""
The positions a value reaches, as one forward value-flow walk classifies them: the calls and
constructions that invoke it, and the positions that obtain it where the text alone does not
say what runs next — an argument, a member store, an array element, an object-literal value, a
return.
"""
invocations: list[Node]
escapes: list[Node]
class TamperingModel:
"""
The anchored trust oracle for one script, built over a
`refinery.lib.scripts.js.analysis.model.SemanticModel`, an
`refinery.lib.scripts.js.analysis.effects.EffectModel`, a
`refinery.lib.scripts.js.analysis.dominance.DominanceModel`, and the
`refinery.lib.scripts.analysis.cycles.CycleModel` the cycle questions go through. Ask whether
the built-ins are intact at one point with `builtins_intact_at`, and the trust questions of a
fold or an execution there with `trust_at`;
ask which single value a binding holds at one point with
`singular_value_at` — the positioned value question the same models answer; ask whether every
invocation of a function discards its completion value with
`every_invocation_discards_the_value`. Build through `build_tampering`.
"""
def __init__(
self,
model: SemanticModel,
effects: EffectModel,
dominance: DominanceModel,
cycles: CycleModel,
):
self.model = model
self.effects = effects
self.dominance = dominance
self._cycles = cycles
self._site_list: list[Node] | None = None
self._sites_unenumerable = False
self._program_wide: bool | None = None
self._anchor_cache: dict[int, bool] = {}
self._invocation_cache: dict[int, _Flow] = {}
self._positioned_cache: dict[tuple[int, int], Node | None] = {}
def builtins_intact_at(self, anchor: Node) -> bool:
"""
Whether every built-in is still what the language says at the moment *anchor* is evaluated,
so a fold or an interpreted execution at that point may trust a name, a prototype, or a
chain the program-wide questions refuse. The answer is the conjunction of no program-wide
refusal, every tampering site being guaranteed to run after the anchor, and the anchor
executing at most once — the conjunct the name does not suggest: a site the anchor's
re-execution could cross on a second run is a site before it, so a call inside a loop or a
twice-called function is refused wherever a site exists at all. With no sites there is
nothing to cross and the answer is `True` without the multiplicity walk.
"""
cached = self._anchor_cache.get(id(anchor))
if cached is None:
cached = self._compute_intact(anchor)
self._anchor_cache[id(anchor)] = cached
return cached
def trust_at(self, anchor: Node) -> AnchoredTrust:
"""
The questions whether the built-ins are still the language's when *anchor* is evaluated:
`builtins_intact_at` composed with the effect model's per-name arms (`AnchoredTrust`).
"""
return AnchoredTrust(
self.effects,
lambda: self.builtins_intact_at(anchor),
lambda: self.code_load_may_precede(anchor),
)
def code_load_may_precede(self, anchor: Node) -> bool:
"""
Whether code the text does not hold may have been loaded before *anchor* runs: the file
imports such code, which the host runs before its body
(`refinery.lib.scripts.js.analysis.model.SemanticModel.imports_code`), or a site of
`refinery.lib.scripts.js.analysis.model.SemanticModel.code_load_sites` may come first. A
site comes first unless the anchor executes at most once (`at_most_once`) and
`refinery.lib.scripts.js.analysis.dominance.DominanceModel.cannot_precede` places the site
after it: a second run of the anchor follows a load the first run preceded.
"""
if self.model.imports_code():
return True
sites = self.model.code_load_sites()
if not sites:
return False
if not self.at_most_once(anchor):
return True
return not all(self.dominance.cannot_precede(site, anchor) for site in sites)
def _compute_intact(self, anchor: Node) -> bool:
if not self._program_is_clear():
return False
sites = self._sites()
if sites is None:
return False
sites = [site for site in sites if site is not anchor]
if not sites:
return True
if not self.at_most_once(anchor):
return False
return all(self.dominance.cannot_precede(site, anchor) for site in sites)
def singular_value_at(self, binding: Binding | None, at: Node) -> Node | None:
"""
The single value *binding* provably holds at the moment *at* is evaluated —
`SemanticModel.singular_value` with the position supplied, so a binding the program-wide
volatility question refuses can still answer where ordering proves the read safe. The value
is the one channel the text spells, judged by three legs, each asked of every binding the
query answers:
1. establishment: the channel's establishing write has run before the read, judged by the
`SemanticModel.binding_establishment_sites` rules, so a hoisted function declaration
needs no ordering;
2. hazards: every dynamic rebind the located question
(`SemanticModel.binding_dynamic_rebind_sites`) finds first-executes after the read;
3. multiplicity: the read executes at most once (`at_most_once`), since ordering first
executions does not bound what a read on a cycle sees on its second one.
The channels and their completeness are read on the ignore view
(`binding_values` with *ignore_dynamic_rebinds*), the one where a rebind is a located
hazard rather than a program-wide refusal. A script-scope binding fails closed as leg 2's
own case: its eval-surface hazards are the whole-program questions the volatility boolean
already refuses to freeze it on. `None` whenever any leg fails, the binding holds no
single spelled value, or its only incompleteness is the nodeless entry write. Memoized per
(binding, read) for the model's lifetime, exactly as the site enumeration is — the
recognizer asks the same hops from the site enumeration and the flow walk.
"""
if binding is None:
return None
key = (id(binding), id(at))
if key not in self._positioned_cache:
self._positioned_cache[key] = self._compute_singular_value_at(binding, at)
return self._positioned_cache[key]
def _compute_singular_value_at(self, binding: Binding, at: Node) -> Node | None:
owner = binding.scope.var_scope
if owner is None or owner.kind is ScopeKind.SCRIPT:
return None
values, complete = self.model.binding_values(
binding, ignore_dynamic_rebinds=True)
establishment = self.model.binding_establishment_sites(
binding, ignore_dynamic_rebinds=True)
if not complete or establishment is None or len(values) != 1:
return None
hazards = self.model.binding_dynamic_rebind_sites(binding)
if hazards is None or not self.at_most_once(at):
return None
if not all(self.dominance.runs_before(site, at) for site in establishment):
return None
if not all(self.dominance.cannot_precede(hazard, at) for hazard in hazards):
return None
return values[0]
def _program_is_clear(self) -> bool:
"""
Whether the program holds nothing that cannot be ordered against an anchor: no static
import declaration, no `with` statement, and no span of source the model never read. Each
of these may rebind or invoke anything at a point no dominance question places, so their
presence refuses every anchor rather than degrading to an ordering that cannot hold.
"""
if self._program_wide is None:
self._program_wide = not any(
isinstance(node, (JsImportDeclaration, JsWithStatement)) or is_unread_source(node)
for node in self.model.root.walk()
)
return self._program_wide
def at_most_once(self, anchor: Node) -> bool:
"""
Whether *anchor* executes at most once: its own control-flow node is off a cycle, and
every activation up to the script root is invoked at most once — exactly one
alias-resolved invocation site, off a cycle, not lexically inside the function it invokes.
An activation that may be called without its name, through the `caller` of another
function, its own `arguments` or `new.target`
(`refinery.lib.scripts.js.analysis.model.SemanticModel.reached_without_its_name`), breaks
it: it runs again where no textual count sees it. So does an activation the host may invoke
freely or that reflective code in its own scope could re-invoke by name, and the initializer
of a field, which runs at each construction of its class.
"""
located = self.dominance.locate(anchor)
if located is None:
return False
if self._cycles.on_a_cycle(located[0], located[1]):
return False
function = enclosing_run(anchor)
seen: set[int] = set()
while function is not None:
if not isinstance(function, FUNCTION_NODES) or id(function) in seen:
return False
seen.add(id(function))
if self.model.reached_without_its_name(function):
return False
flow = self._invocations_of(function)
if flow.escapes or len(flow.invocations) != 1:
return False
point = flow.invocations[0]
if enclosing_run(point) is function:
return False
if not self._invocation_is_singly_executed(function, point):
return False
function = enclosing_run(point)
return True
def _invocation_is_singly_executed(self, function: Node, point: Node) -> bool:
"""
Whether the one invocation *point* of *function* runs its callee at most once: the point's
own node is off a cycle, and the activation is not one the host or reflective code may
invoke freely.
"""
if not self._invocation_enumeration_is_complete(function):
return False
located = self.dominance.locate(point)
return located is not None and not self._cycles.on_a_cycle(located[0], located[1])
def _invocation_enumeration_is_complete(self, function: Node) -> bool:
"""
Whether nothing can run *function* through a channel the invocation enumeration never saw:
code outside the file
(`refinery.lib.scripts.js.analysis.model.SemanticModel.callable_from_outside`), a
script-scope name under any opaque surface (executable text in the global scope can
re-invoke it by name), or a function-local name reflection can reach in its own scope. A
function whose enumeration is complete is one whose listed invocations are every way its
body runs.
"""
if self.model.callable_from_outside(function):
return False
names = self.model.function_names(function)
for binding in () if names is None else names.bindings:
owner = binding.scope.var_scope
if owner is None or owner.kind is ScopeKind.SCRIPT:
if self.model.has_reflection_surface():
return False
elif self.model.reflection_can_reach(binding):
return False
return True
def every_invocation_discards_the_value(
self, function: Node, value_discarded: Callable[[Node], bool],
) -> bool:
"""
Whether every invocation of *function* throws its completion value away, judged by the
forward value-flow enumeration `_invocations_of` reads: a position the value reaches without
being invoked — an argument, a store, a return — could hand it to a reader, so one such
escape refuses the answer, as does a function the text never invokes at all (an entrypoint
or dead code, neither of which this model can pin). *value_discarded* classifies each
invocation position, the caller's own reading of the positions it knows; no invocation, or
one whose value a caller of this method would keep, answers `False`.
The enumeration must also be complete — nothing may run *function* outside the invocations
it listed, since a host or reflective caller could read what every listed one discards.
Multiplicity is deliberately absent: a value discarded at every invocation is discarded at
each of them however often they run.
"""
flow = self._invocations_of(function)
if flow.escapes or not flow.invocations:
return False
if not self._invocation_enumeration_is_complete(function):
return False
return all(value_discarded(point) for point in flow.invocations)
def _invocations_of(self, function: Node) -> _Flow:
"""
The positions *function*'s value reaches: the reads of the name it is invoked through, or
the function node itself when no name pins it — the immediate-invocation form. Memoized per
function; the reads are model facts, fixed for the model's lifetime.
"""
cached = self._invocation_cache.get(id(function))
if cached is None:
names = self.model.function_names(function)
frontier = list(names.reads) if names is not None else [function]
cached = self._flow(frontier, through_constructions=False)
self._invocation_cache[id(function)] = cached
return cached
def _flow(self, frontier: list[Node], *, through_constructions: bool) -> _Flow:
"""
The one forward value-flow walk: every position that obtains a value the walk follows. The
positions are read the same way whichever value is flowing — the callee of a call or `new`
invokes it, the initializer of a declarator or `=` assignment stores it in a name whose
reads the walk follows, an expression statement drops it, and any other position escapes —
with one variation: a value that flows into the callee of a recognized *construction* is
built with rather than invoked, so the walk continues from that call's result instead of
recording an invocation.
*frontier* holds the nodes whose positions the walk starts from — the reads of a binding,
or a value expression. A binding the walk follows must have its reads enumerable: dynamic
references a `with` body records, or a write nothing attributes, make the flow escape
rather than guess at what runs.
"""
invocations: list[Node] = []
escapes: list[Node] = []
pending = list(frontier)
visited: set[int] = set()
while pending:
node = pending.pop()
if id(node) in visited:
continue
visited.add(id(node))
cursor = node
parent = cursor.parent
while isinstance(parent, JsParenthesizedExpression):
cursor = parent
parent = cursor.parent
if parent is None:
escapes.append(node)
continue
if (
isinstance(parent, (JsCallExpression, JsNewExpression))
and parent.callee is cursor
):
if (
through_constructions
and denotes_function_intrinsic(
parent.callee, self.model, self.effects, self.dominance,
positioned_value=self.singular_value_at,
) is True
):
pending.append(parent)
continue
invocations.append(parent)
continue
reads = self._stored_reads(parent, cursor)
if reads is None:
escapes.append(node)
else:
pending.extend(reads)
return _Flow(invocations, escapes)
def _stored_reads(
self, parent: Node, node: Node,
) -> list[Node] | None:
"""
The reads of the binding *node* is stored into by *parent* — a declarator initializer or
the right side of an `=` assignment — or `None` when *node*'s position neither stores it in
a name nor drops it as an expression statement, so the value escapes the walk's reading. A
binding with dynamic references or an unattributed write also escapes: its reads do not
enumerate what could invoke the value.
"""
target: JsIdentifier | None = None
if isinstance(parent, JsVariableDeclarator) and parent.init is node:
if isinstance(parent.id, JsIdentifier):
target = parent.id
elif (
isinstance(parent, JsAssignmentExpression)
and parent.operator == '='
and parent.right is node
):
stripped = strip_parens(parent.left)
if isinstance(stripped, JsIdentifier):
target = stripped
elif isinstance(parent, JsExpressionStatement):
return []
else:
return None
if target is None:
return None
binding = self.model.lookup(target.name, self.model.scope_of(target))
if binding is None or binding.dynamic_refs or binding.has_indefinite_write:
return None
return list(binding.reads)
def _may_be_global_object(self, node: Node | None) -> bool:
"""
Whether *node* may be the global object once the program runs —
`SemanticModel.may_be_the_global_object` — widened by the one case the model cannot answer:
a node it never placed, spliced into the tree after the model was built. Such a node is
treated as may-be-global, the fail-closed direction that keeps a spliced base a site.
"""
if node is None:
return False
if self.model.scope_of(node) is None:
return True
return self.model.may_be_the_global_object(node)
def _sites(self) -> list[Node] | None:
"""
Every point in the program at which a built-in may be replaced or executed code may run,
or `None` when that cannot be enumerated — the hand-over of the global object to a callee
that may write it, a fact with no site to order. Computed once over the model's facts and
memoized; the anchor's own call node is excluded by the caller, its rebind routes owned by
the gates of the execution that asked.
"""
if self._sites_unenumerable:
return None
if self._site_list is not None:
return self._site_list
if self.model.has_opaque_global_write() and self.model.opaque_global_write_sites() is None:
self._sites_unenumerable = True
return None
sites: list[Node] = []
root = self.model.root
for member in root.walk():
if isinstance(member, JsMemberExpression) and opaque_global_write(member, self._may_be_global_object):
sites.append(member)
for node in root.walk():
if not isinstance(node, (JsCallExpression, JsNewExpression)):
continue
if any(
self._may_be_global_object(argument)
for argument in node.arguments
):
sites.append(node)
for site in self.model.opaque_reflection_sites():
sites.extend(self._surface_sites(site))
for site in self.model.code_load_sites():
sites.extend(self._surface_sites(site))
for node in root.walk():
if not isinstance(node, (JsCallExpression, JsNewExpression)):
continue
verdict = denotes_function_intrinsic(
node.callee, self.model, self.effects, self.dominance,
positioned_value=self.singular_value_at,
)
if verdict is True:
flow = self._flow([node], through_constructions=True)
sites.extend(flow.invocations)
sites.extend(flow.escapes)
elif verdict is None:
sites.append(node)
self._site_list = sites
return sites
def _surface_sites(self, site: Node) -> list[Node]:
"""
The sites an opaque reflective surface or a load of code implies. A dynamic `import()`, a
string timer and a `require` that loads code run code at their own node; every other surface
is a value-read of a reflective intrinsic, so the sites are the positions that value
reaches — its invocations and escapes, by the one forward walk. A read of `eval` invokes the
code it is handed, while a read of `Function` builds with it and the walk continues through
the construction; a computed read on the global object names an unknown global and is
followed without the construction continuation, so a call it flows into is a site at that
call.
"""
if isinstance(site, (JsImportExpression, JsCallExpression)):
return [site]
through_constructions = True
if isinstance(site, JsMemberExpression):
through_constructions = not key_is(site, _EVAL, NO_IS_STRICTER)
elif isinstance(site, JsIdentifier):
through_constructions = site.name != 'eval'
elif isinstance(site, (JsVariableDeclarator, JsAssignmentExpression)):
if self._destructuring_reads_a_reflective_intrinsic_imprecisely(site):
return [site]
flows: list[_Flow] = []
for name, follows_construction in self._destructured_reflective_names(site):
binding = self._destructured_binding(site, name)
if binding is None:
return [site]
flows.append(self._flow(list(binding.reads), through_constructions=follows_construction))
return [
position
for flow in flows
for position in (*flow.invocations, *flow.escapes)
]
else:
return [site]
flow = self._flow([site], through_constructions=through_constructions)
return [*flow.invocations, *flow.escapes]
def _destructured_reflective_names(
self, site: JsVariableDeclarator | JsAssignmentExpression,
) -> list[tuple[str, bool]]:
"""
The reflective-intrinsic names an object pattern binds out of a source that may be the
global object, each paired with whether its read builds rather than invokes — `Function`
builds, `eval` runs. `const {eval} = globalThis` is the same value-read of the intrinsic
the bare name spells, so it starts the same walk.
"""
if isinstance(site, JsVariableDeclarator):
pattern, source = site.id, site.init
else:
pattern, source = site.left, site.right
if not isinstance(pattern, JsObjectPattern):
return []
if not self._may_be_global_object(source):
return []
exposure = _pattern_reflective_exposure(pattern)
return [(name, name != 'eval') for name in exposure.named]
def _destructuring_reads_a_reflective_intrinsic_imprecisely(
self, site: JsVariableDeclarator | JsAssignmentExpression,
) -> bool:
"""
Whether the object pattern at *site*, off a source that may be the global object, reads a
reflective intrinsic under a spelling this model cannot bind to a forward flow — a computed
key the runtime resolves (`{[k]: e}`) or a rest element (`{...r}`) that captures whatever the
object holds, `eval`/`Function` among it. Such a read reaches no invocation the walk can
find, so the surface it forms must fall back to the whole node as its tampering site;
otherwise `builtins_intact_at` would clear on an empty site list. A named key with a
non-identifier value already falls back through `_destructured_binding`.
"""
if isinstance(site, JsVariableDeclarator):
pattern, source = site.id, site.init
else:
pattern, source = site.left, site.right
if not isinstance(pattern, JsObjectPattern) or not self._may_be_global_object(source):
return False
exposure = _pattern_reflective_exposure(pattern)
return exposure.dynamic_key or exposure.rest
def _destructured_binding(
self, site: JsVariableDeclarator | JsAssignmentExpression, name: str,
) -> Binding | None:
"""
The binding the name *name* of a destructuring at *site* reads into: the binding its
pattern position declares for a declarator, or resolves for an assignment pattern. `None`
when the model places no binding for it.
"""
if isinstance(site, JsVariableDeclarator):
for prop in _pattern_properties(site.id):
value = prop.value
if isinstance(value, JsIdentifier) and value.name == name:
return self.model.binding_of(value)
return None
for prop in _pattern_properties(site.left):
value = prop.value
if isinstance(value, JsIdentifier) and value.name == name:
return self.model.resolve(value)
return None
def _pattern_properties(pattern: Node) -> list[JsProperty]:
if not isinstance(pattern, JsObjectPattern):
return []
return [prop for prop in pattern.properties if isinstance(prop, JsProperty)]
def build_tampering(
model: SemanticModel,
effects: EffectModel,
dominance: DominanceModel,
cycles: CycleModel,
) -> TamperingModel:
"""
Build the `TamperingModel` for a script's analysis models, reusing *dominance* and *cycles*
the caller already holds. The host entry points the multiplicity guard refuses
are the ones *model* was built under.
"""
return TamperingModel(model, effects, dominance, cycles)
Functions
def denotes_function_intrinsic(callee, model, effects, dominance, *, eval_string=None, read_effect=None, positioned_value=None, spliced_names=(), depth=0, builtin_function=None)-
Whether callee denotes the
Functionintrinsic — the callee every spelling of a code construction calls — orNonewhen that cannot be decided.Truecovers the spellings the text can pin: the bare free global, a.constructornavigation off a literal or off a name the model pins to one plain function, and a name holding one of those, transitively throughSemanticModel.singular_valueup to the alias-hop limit.Nonemarks the shapes no reading of the text decides — a node the model cannot place, a computed key no string resolution reads, an alias chain past the limit — andFalseeverything else, including every callee that provably denotes some other value.The one gate every navigation spelling passes first: a program that writes the
constructorkey on a chain rooted atFunctionhas replaced the intrinsic every navigation hands out, so all of them are refused at once. A callee the recognizer declines leaves its construction unrecognized, which is sound for the callers this package serves — the value a construction produces can only reach a callee position through a position the tampering enumeration reads, so an unrecognized spelling costs recall there and never a site.Every name-to-value hop of an alias chain is ordered against the read that resolves it: the name's single value must be established before the hop's identifier is read, or the name spelled
undefinedthere and the construction threw. A chain is a value at the moment each hop reads it, which is what the recursion's node positions carry and why the check sits on the hop rather than on the invocation that started the walk.eval_string resolves a computed key the model cannot read by folding; a caller with no folder passes
Noneand every unresolvable key answersNone— undecided. read_effect rejects a navigation base whose evaluation fires an effect; without it more bases read as side-effect free, which recognizes more constructions and never fewer. positioned_value resolves the two alias hops with the read's position supplied — the value the name holds at the moment it is read (TamperingModel.singular_value_at()), so a binding the program-wide volatility question refuses can still answer where ordering proves the read safe. It is asked only whereSemanticModel.singular_valuehas no value, so every verdict the stock sequence already gives is kept and the repair recognizes only what stock declines; on that path the member hop's establishment question is answered by the query's own leg — the sameSemanticModel.binding_establishment_sitesrules — and the stockDominanceModel.binding_established_beforecall runs only beside the stock value.builtin_function says whether a node denotes a function the language installs, read without an effect, whose
constructorread still reachesFunction:[].at.constructorisFunctionasf.constructoris for a plain functionf. Only a caller that evaluates such reads can say so; without it no such base is recognized.Expand source code Browse git
def denotes_function_intrinsic( callee: Node | None, model: SemanticModel, effects: EffectModel, dominance: DominanceModel, *, eval_string: Callable[[Node | None], str | None] | None = None, read_effect: Callable[[Node], bool] | None = None, positioned_value: Callable[[Binding, Node], Node | None] | None = None, spliced_names: Collection[str] = (), depth: int = 0, builtin_function: Callable[[Node], bool] | None = None, ) -> bool | None: """ Whether *callee* denotes the `Function` intrinsic — the callee every spelling of a code construction calls — or `None` when that cannot be decided. `True` covers the spellings the text can pin: the bare free global, a `.constructor` navigation off a literal or off a name the model pins to one plain function, and a name holding one of those, transitively through `SemanticModel.singular_value` up to the alias-hop limit. `None` marks the shapes no reading of the text decides — a node the model cannot place, a computed key no string resolution reads, an alias chain past the limit — and `False` everything else, including every callee that provably denotes some other value. The one gate every navigation spelling passes first: a program that writes the `constructor` key on a chain rooted at `Function` has replaced the intrinsic every navigation hands out, so all of them are refused at once. A callee the recognizer declines leaves its construction unrecognized, which is sound for the callers this package serves — the value a construction produces can only reach a callee position through a position the tampering enumeration reads, so an unrecognized spelling costs recall there and never a site. Every name-to-value hop of an alias chain is ordered against the read that resolves it: the name's single value must be established before the hop's identifier is read, or the name spelled `undefined` there and the construction threw. A chain is a value *at the moment each hop reads it*, which is what the recursion's node positions carry and why the check sits on the hop rather than on the invocation that started the walk. *eval_string* resolves a computed key the model cannot read by folding; a caller with no folder passes `None` and every unresolvable key answers `None` — undecided. *read_effect* rejects a navigation base whose evaluation fires an effect; without it more bases read as side-effect free, which recognizes more constructions and never fewer. *positioned_value* resolves the two alias hops with the read's position supplied — the value the name holds at the moment it is read (`TamperingModel.singular_value_at`), so a binding the program-wide volatility question refuses can still answer where ordering proves the read safe. It is asked only where `SemanticModel.singular_value` has no value, so every verdict the stock sequence already gives is kept and the repair recognizes only what stock declines; on that path the member hop's establishment question is answered by the query's own leg — the same `SemanticModel.binding_establishment_sites` rules — and the stock `DominanceModel.binding_established_before` call runs only beside the stock value. *builtin_function* says whether a node denotes a function the language installs, read without an effect, whose `constructor` read still reaches `Function`: `[].at.constructor` is `Function` as `f.constructor` is for a plain function `f`. Only a caller that evaluates such reads can say so; without it no such base is recognized. """ return _denotes_function_intrinsic( callee, model, effects, dominance, eval_string=eval_string, read_effect=read_effect, positioned_value=positioned_value, spliced_names=spliced_names, depth=depth, aliases=None, builtin_function=builtin_function, ) def function_intrinsic_aliases(callee, model, effects, dominance, *, eval_string=None, read_effect=None, positioned_value=None, spliced_names=(), builtin_function=None)-
The name-to-value bindings callee resolves through to the
Functionintrinsic — one entry per identifier hop of the spelling, innermost first — orNonewhere it does not provably denote it. The verdict isdenotes_function_intrinsic()'s, read off the same walk, so the two can never disagree about a callee; the chain is what a fold that consumes the construction together with the names it was spelled through has to delete, and the hop ordering the walk already proves is what makes each of those declarators hold the value the construction read.Expand source code Browse git
def function_intrinsic_aliases( callee: Node | None, model: SemanticModel, effects: EffectModel, dominance: DominanceModel, *, eval_string: Callable[[Node | None], str | None] | None = None, read_effect: Callable[[Node], bool] | None = None, positioned_value: Callable[[Binding, Node], Node | None] | None = None, spliced_names: Collection[str] = (), builtin_function: Callable[[Node], bool] | None = None, ) -> list[Binding] | None: """ The name-to-value bindings *callee* resolves through to the `Function` intrinsic — one entry per identifier hop of the spelling, innermost first — or `None` where it does not provably denote it. The verdict is `denotes_function_intrinsic`'s, read off the same walk, so the two can never disagree about a callee; the chain is what a fold that consumes the construction together with the names it was spelled through has to delete, and the hop ordering the walk already proves is what makes each of those declarators hold the value the construction read. """ aliases: list[Binding] = [] verdict = _denotes_function_intrinsic( callee, model, effects, dominance, eval_string=eval_string, read_effect=read_effect, positioned_value=positioned_value, spliced_names=spliced_names, depth=0, aliases=aliases, builtin_function=builtin_function, ) if verdict is True: return aliases return None def build_tampering(model, effects, dominance, cycles)-
Build the
TamperingModelfor a script's analysis models, reusing dominance and cycles the caller already holds. The host entry points the multiplicity guard refuses are the ones model was built under.Expand source code Browse git
def build_tampering( model: SemanticModel, effects: EffectModel, dominance: DominanceModel, cycles: CycleModel, ) -> TamperingModel: """ Build the `TamperingModel` for a script's analysis models, reusing *dominance* and *cycles* the caller already holds. The host entry points the multiplicity guard refuses are the ones *model* was built under. """ return TamperingModel(model, effects, dominance, cycles)
Classes
class TamperingModel (model, effects, dominance, cycles)-
The anchored trust oracle for one script, built over a
SemanticModel, anEffectModel, aDominanceModel, and theCycleModelthe cycle questions go through. Ask whether the built-ins are intact at one point withbuiltins_intact_at, and the trust questions of a fold or an execution there withtrust_at; ask which single value a binding holds at one point withsingular_value_at— the positioned value question the same models answer; ask whether every invocation of a function discards its completion value withevery_invocation_discards_the_value. Build throughbuild_tampering().Expand source code Browse git
class TamperingModel: """ The anchored trust oracle for one script, built over a `refinery.lib.scripts.js.analysis.model.SemanticModel`, an `refinery.lib.scripts.js.analysis.effects.EffectModel`, a `refinery.lib.scripts.js.analysis.dominance.DominanceModel`, and the `refinery.lib.scripts.analysis.cycles.CycleModel` the cycle questions go through. Ask whether the built-ins are intact at one point with `builtins_intact_at`, and the trust questions of a fold or an execution there with `trust_at`; ask which single value a binding holds at one point with `singular_value_at` — the positioned value question the same models answer; ask whether every invocation of a function discards its completion value with `every_invocation_discards_the_value`. Build through `build_tampering`. """ def __init__( self, model: SemanticModel, effects: EffectModel, dominance: DominanceModel, cycles: CycleModel, ): self.model = model self.effects = effects self.dominance = dominance self._cycles = cycles self._site_list: list[Node] | None = None self._sites_unenumerable = False self._program_wide: bool | None = None self._anchor_cache: dict[int, bool] = {} self._invocation_cache: dict[int, _Flow] = {} self._positioned_cache: dict[tuple[int, int], Node | None] = {} def builtins_intact_at(self, anchor: Node) -> bool: """ Whether every built-in is still what the language says at the moment *anchor* is evaluated, so a fold or an interpreted execution at that point may trust a name, a prototype, or a chain the program-wide questions refuse. The answer is the conjunction of no program-wide refusal, every tampering site being guaranteed to run after the anchor, and the anchor executing at most once — the conjunct the name does not suggest: a site the anchor's re-execution could cross on a second run is a site before it, so a call inside a loop or a twice-called function is refused wherever a site exists at all. With no sites there is nothing to cross and the answer is `True` without the multiplicity walk. """ cached = self._anchor_cache.get(id(anchor)) if cached is None: cached = self._compute_intact(anchor) self._anchor_cache[id(anchor)] = cached return cached def trust_at(self, anchor: Node) -> AnchoredTrust: """ The questions whether the built-ins are still the language's when *anchor* is evaluated: `builtins_intact_at` composed with the effect model's per-name arms (`AnchoredTrust`). """ return AnchoredTrust( self.effects, lambda: self.builtins_intact_at(anchor), lambda: self.code_load_may_precede(anchor), ) def code_load_may_precede(self, anchor: Node) -> bool: """ Whether code the text does not hold may have been loaded before *anchor* runs: the file imports such code, which the host runs before its body (`refinery.lib.scripts.js.analysis.model.SemanticModel.imports_code`), or a site of `refinery.lib.scripts.js.analysis.model.SemanticModel.code_load_sites` may come first. A site comes first unless the anchor executes at most once (`at_most_once`) and `refinery.lib.scripts.js.analysis.dominance.DominanceModel.cannot_precede` places the site after it: a second run of the anchor follows a load the first run preceded. """ if self.model.imports_code(): return True sites = self.model.code_load_sites() if not sites: return False if not self.at_most_once(anchor): return True return not all(self.dominance.cannot_precede(site, anchor) for site in sites) def _compute_intact(self, anchor: Node) -> bool: if not self._program_is_clear(): return False sites = self._sites() if sites is None: return False sites = [site for site in sites if site is not anchor] if not sites: return True if not self.at_most_once(anchor): return False return all(self.dominance.cannot_precede(site, anchor) for site in sites) def singular_value_at(self, binding: Binding | None, at: Node) -> Node | None: """ The single value *binding* provably holds at the moment *at* is evaluated — `SemanticModel.singular_value` with the position supplied, so a binding the program-wide volatility question refuses can still answer where ordering proves the read safe. The value is the one channel the text spells, judged by three legs, each asked of every binding the query answers: 1. establishment: the channel's establishing write has run before the read, judged by the `SemanticModel.binding_establishment_sites` rules, so a hoisted function declaration needs no ordering; 2. hazards: every dynamic rebind the located question (`SemanticModel.binding_dynamic_rebind_sites`) finds first-executes after the read; 3. multiplicity: the read executes at most once (`at_most_once`), since ordering first executions does not bound what a read on a cycle sees on its second one. The channels and their completeness are read on the ignore view (`binding_values` with *ignore_dynamic_rebinds*), the one where a rebind is a located hazard rather than a program-wide refusal. A script-scope binding fails closed as leg 2's own case: its eval-surface hazards are the whole-program questions the volatility boolean already refuses to freeze it on. `None` whenever any leg fails, the binding holds no single spelled value, or its only incompleteness is the nodeless entry write. Memoized per (binding, read) for the model's lifetime, exactly as the site enumeration is — the recognizer asks the same hops from the site enumeration and the flow walk. """ if binding is None: return None key = (id(binding), id(at)) if key not in self._positioned_cache: self._positioned_cache[key] = self._compute_singular_value_at(binding, at) return self._positioned_cache[key] def _compute_singular_value_at(self, binding: Binding, at: Node) -> Node | None: owner = binding.scope.var_scope if owner is None or owner.kind is ScopeKind.SCRIPT: return None values, complete = self.model.binding_values( binding, ignore_dynamic_rebinds=True) establishment = self.model.binding_establishment_sites( binding, ignore_dynamic_rebinds=True) if not complete or establishment is None or len(values) != 1: return None hazards = self.model.binding_dynamic_rebind_sites(binding) if hazards is None or not self.at_most_once(at): return None if not all(self.dominance.runs_before(site, at) for site in establishment): return None if not all(self.dominance.cannot_precede(hazard, at) for hazard in hazards): return None return values[0] def _program_is_clear(self) -> bool: """ Whether the program holds nothing that cannot be ordered against an anchor: no static import declaration, no `with` statement, and no span of source the model never read. Each of these may rebind or invoke anything at a point no dominance question places, so their presence refuses every anchor rather than degrading to an ordering that cannot hold. """ if self._program_wide is None: self._program_wide = not any( isinstance(node, (JsImportDeclaration, JsWithStatement)) or is_unread_source(node) for node in self.model.root.walk() ) return self._program_wide def at_most_once(self, anchor: Node) -> bool: """ Whether *anchor* executes at most once: its own control-flow node is off a cycle, and every activation up to the script root is invoked at most once — exactly one alias-resolved invocation site, off a cycle, not lexically inside the function it invokes. An activation that may be called without its name, through the `caller` of another function, its own `arguments` or `new.target` (`refinery.lib.scripts.js.analysis.model.SemanticModel.reached_without_its_name`), breaks it: it runs again where no textual count sees it. So does an activation the host may invoke freely or that reflective code in its own scope could re-invoke by name, and the initializer of a field, which runs at each construction of its class. """ located = self.dominance.locate(anchor) if located is None: return False if self._cycles.on_a_cycle(located[0], located[1]): return False function = enclosing_run(anchor) seen: set[int] = set() while function is not None: if not isinstance(function, FUNCTION_NODES) or id(function) in seen: return False seen.add(id(function)) if self.model.reached_without_its_name(function): return False flow = self._invocations_of(function) if flow.escapes or len(flow.invocations) != 1: return False point = flow.invocations[0] if enclosing_run(point) is function: return False if not self._invocation_is_singly_executed(function, point): return False function = enclosing_run(point) return True def _invocation_is_singly_executed(self, function: Node, point: Node) -> bool: """ Whether the one invocation *point* of *function* runs its callee at most once: the point's own node is off a cycle, and the activation is not one the host or reflective code may invoke freely. """ if not self._invocation_enumeration_is_complete(function): return False located = self.dominance.locate(point) return located is not None and not self._cycles.on_a_cycle(located[0], located[1]) def _invocation_enumeration_is_complete(self, function: Node) -> bool: """ Whether nothing can run *function* through a channel the invocation enumeration never saw: code outside the file (`refinery.lib.scripts.js.analysis.model.SemanticModel.callable_from_outside`), a script-scope name under any opaque surface (executable text in the global scope can re-invoke it by name), or a function-local name reflection can reach in its own scope. A function whose enumeration is complete is one whose listed invocations are every way its body runs. """ if self.model.callable_from_outside(function): return False names = self.model.function_names(function) for binding in () if names is None else names.bindings: owner = binding.scope.var_scope if owner is None or owner.kind is ScopeKind.SCRIPT: if self.model.has_reflection_surface(): return False elif self.model.reflection_can_reach(binding): return False return True def every_invocation_discards_the_value( self, function: Node, value_discarded: Callable[[Node], bool], ) -> bool: """ Whether every invocation of *function* throws its completion value away, judged by the forward value-flow enumeration `_invocations_of` reads: a position the value reaches without being invoked — an argument, a store, a return — could hand it to a reader, so one such escape refuses the answer, as does a function the text never invokes at all (an entrypoint or dead code, neither of which this model can pin). *value_discarded* classifies each invocation position, the caller's own reading of the positions it knows; no invocation, or one whose value a caller of this method would keep, answers `False`. The enumeration must also be complete — nothing may run *function* outside the invocations it listed, since a host or reflective caller could read what every listed one discards. Multiplicity is deliberately absent: a value discarded at every invocation is discarded at each of them however often they run. """ flow = self._invocations_of(function) if flow.escapes or not flow.invocations: return False if not self._invocation_enumeration_is_complete(function): return False return all(value_discarded(point) for point in flow.invocations) def _invocations_of(self, function: Node) -> _Flow: """ The positions *function*'s value reaches: the reads of the name it is invoked through, or the function node itself when no name pins it — the immediate-invocation form. Memoized per function; the reads are model facts, fixed for the model's lifetime. """ cached = self._invocation_cache.get(id(function)) if cached is None: names = self.model.function_names(function) frontier = list(names.reads) if names is not None else [function] cached = self._flow(frontier, through_constructions=False) self._invocation_cache[id(function)] = cached return cached def _flow(self, frontier: list[Node], *, through_constructions: bool) -> _Flow: """ The one forward value-flow walk: every position that obtains a value the walk follows. The positions are read the same way whichever value is flowing — the callee of a call or `new` invokes it, the initializer of a declarator or `=` assignment stores it in a name whose reads the walk follows, an expression statement drops it, and any other position escapes — with one variation: a value that flows into the callee of a recognized *construction* is built with rather than invoked, so the walk continues from that call's result instead of recording an invocation. *frontier* holds the nodes whose positions the walk starts from — the reads of a binding, or a value expression. A binding the walk follows must have its reads enumerable: dynamic references a `with` body records, or a write nothing attributes, make the flow escape rather than guess at what runs. """ invocations: list[Node] = [] escapes: list[Node] = [] pending = list(frontier) visited: set[int] = set() while pending: node = pending.pop() if id(node) in visited: continue visited.add(id(node)) cursor = node parent = cursor.parent while isinstance(parent, JsParenthesizedExpression): cursor = parent parent = cursor.parent if parent is None: escapes.append(node) continue if ( isinstance(parent, (JsCallExpression, JsNewExpression)) and parent.callee is cursor ): if ( through_constructions and denotes_function_intrinsic( parent.callee, self.model, self.effects, self.dominance, positioned_value=self.singular_value_at, ) is True ): pending.append(parent) continue invocations.append(parent) continue reads = self._stored_reads(parent, cursor) if reads is None: escapes.append(node) else: pending.extend(reads) return _Flow(invocations, escapes) def _stored_reads( self, parent: Node, node: Node, ) -> list[Node] | None: """ The reads of the binding *node* is stored into by *parent* — a declarator initializer or the right side of an `=` assignment — or `None` when *node*'s position neither stores it in a name nor drops it as an expression statement, so the value escapes the walk's reading. A binding with dynamic references or an unattributed write also escapes: its reads do not enumerate what could invoke the value. """ target: JsIdentifier | None = None if isinstance(parent, JsVariableDeclarator) and parent.init is node: if isinstance(parent.id, JsIdentifier): target = parent.id elif ( isinstance(parent, JsAssignmentExpression) and parent.operator == '=' and parent.right is node ): stripped = strip_parens(parent.left) if isinstance(stripped, JsIdentifier): target = stripped elif isinstance(parent, JsExpressionStatement): return [] else: return None if target is None: return None binding = self.model.lookup(target.name, self.model.scope_of(target)) if binding is None or binding.dynamic_refs or binding.has_indefinite_write: return None return list(binding.reads) def _may_be_global_object(self, node: Node | None) -> bool: """ Whether *node* may be the global object once the program runs — `SemanticModel.may_be_the_global_object` — widened by the one case the model cannot answer: a node it never placed, spliced into the tree after the model was built. Such a node is treated as may-be-global, the fail-closed direction that keeps a spliced base a site. """ if node is None: return False if self.model.scope_of(node) is None: return True return self.model.may_be_the_global_object(node) def _sites(self) -> list[Node] | None: """ Every point in the program at which a built-in may be replaced or executed code may run, or `None` when that cannot be enumerated — the hand-over of the global object to a callee that may write it, a fact with no site to order. Computed once over the model's facts and memoized; the anchor's own call node is excluded by the caller, its rebind routes owned by the gates of the execution that asked. """ if self._sites_unenumerable: return None if self._site_list is not None: return self._site_list if self.model.has_opaque_global_write() and self.model.opaque_global_write_sites() is None: self._sites_unenumerable = True return None sites: list[Node] = [] root = self.model.root for member in root.walk(): if isinstance(member, JsMemberExpression) and opaque_global_write(member, self._may_be_global_object): sites.append(member) for node in root.walk(): if not isinstance(node, (JsCallExpression, JsNewExpression)): continue if any( self._may_be_global_object(argument) for argument in node.arguments ): sites.append(node) for site in self.model.opaque_reflection_sites(): sites.extend(self._surface_sites(site)) for site in self.model.code_load_sites(): sites.extend(self._surface_sites(site)) for node in root.walk(): if not isinstance(node, (JsCallExpression, JsNewExpression)): continue verdict = denotes_function_intrinsic( node.callee, self.model, self.effects, self.dominance, positioned_value=self.singular_value_at, ) if verdict is True: flow = self._flow([node], through_constructions=True) sites.extend(flow.invocations) sites.extend(flow.escapes) elif verdict is None: sites.append(node) self._site_list = sites return sites def _surface_sites(self, site: Node) -> list[Node]: """ The sites an opaque reflective surface or a load of code implies. A dynamic `import()`, a string timer and a `require` that loads code run code at their own node; every other surface is a value-read of a reflective intrinsic, so the sites are the positions that value reaches — its invocations and escapes, by the one forward walk. A read of `eval` invokes the code it is handed, while a read of `Function` builds with it and the walk continues through the construction; a computed read on the global object names an unknown global and is followed without the construction continuation, so a call it flows into is a site at that call. """ if isinstance(site, (JsImportExpression, JsCallExpression)): return [site] through_constructions = True if isinstance(site, JsMemberExpression): through_constructions = not key_is(site, _EVAL, NO_IS_STRICTER) elif isinstance(site, JsIdentifier): through_constructions = site.name != 'eval' elif isinstance(site, (JsVariableDeclarator, JsAssignmentExpression)): if self._destructuring_reads_a_reflective_intrinsic_imprecisely(site): return [site] flows: list[_Flow] = [] for name, follows_construction in self._destructured_reflective_names(site): binding = self._destructured_binding(site, name) if binding is None: return [site] flows.append(self._flow(list(binding.reads), through_constructions=follows_construction)) return [ position for flow in flows for position in (*flow.invocations, *flow.escapes) ] else: return [site] flow = self._flow([site], through_constructions=through_constructions) return [*flow.invocations, *flow.escapes] def _destructured_reflective_names( self, site: JsVariableDeclarator | JsAssignmentExpression, ) -> list[tuple[str, bool]]: """ The reflective-intrinsic names an object pattern binds out of a source that may be the global object, each paired with whether its read builds rather than invokes — `Function` builds, `eval` runs. `const {eval} = globalThis` is the same value-read of the intrinsic the bare name spells, so it starts the same walk. """ if isinstance(site, JsVariableDeclarator): pattern, source = site.id, site.init else: pattern, source = site.left, site.right if not isinstance(pattern, JsObjectPattern): return [] if not self._may_be_global_object(source): return [] exposure = _pattern_reflective_exposure(pattern) return [(name, name != 'eval') for name in exposure.named] def _destructuring_reads_a_reflective_intrinsic_imprecisely( self, site: JsVariableDeclarator | JsAssignmentExpression, ) -> bool: """ Whether the object pattern at *site*, off a source that may be the global object, reads a reflective intrinsic under a spelling this model cannot bind to a forward flow — a computed key the runtime resolves (`{[k]: e}`) or a rest element (`{...r}`) that captures whatever the object holds, `eval`/`Function` among it. Such a read reaches no invocation the walk can find, so the surface it forms must fall back to the whole node as its tampering site; otherwise `builtins_intact_at` would clear on an empty site list. A named key with a non-identifier value already falls back through `_destructured_binding`. """ if isinstance(site, JsVariableDeclarator): pattern, source = site.id, site.init else: pattern, source = site.left, site.right if not isinstance(pattern, JsObjectPattern) or not self._may_be_global_object(source): return False exposure = _pattern_reflective_exposure(pattern) return exposure.dynamic_key or exposure.rest def _destructured_binding( self, site: JsVariableDeclarator | JsAssignmentExpression, name: str, ) -> Binding | None: """ The binding the name *name* of a destructuring at *site* reads into: the binding its pattern position declares for a declarator, or resolves for an assignment pattern. `None` when the model places no binding for it. """ if isinstance(site, JsVariableDeclarator): for prop in _pattern_properties(site.id): value = prop.value if isinstance(value, JsIdentifier) and value.name == name: return self.model.binding_of(value) return None for prop in _pattern_properties(site.left): value = prop.value if isinstance(value, JsIdentifier) and value.name == name: return self.model.resolve(value) return NoneMethods
def builtins_intact_at(self, anchor)-
Whether every built-in is still what the language says at the moment anchor is evaluated, so a fold or an interpreted execution at that point may trust a name, a prototype, or a chain the program-wide questions refuse. The answer is the conjunction of no program-wide refusal, every tampering site being guaranteed to run after the anchor, and the anchor executing at most once — the conjunct the name does not suggest: a site the anchor's re-execution could cross on a second run is a site before it, so a call inside a loop or a twice-called function is refused wherever a site exists at all. With no sites there is nothing to cross and the answer is
Truewithout the multiplicity walk.Expand source code Browse git
def builtins_intact_at(self, anchor: Node) -> bool: """ Whether every built-in is still what the language says at the moment *anchor* is evaluated, so a fold or an interpreted execution at that point may trust a name, a prototype, or a chain the program-wide questions refuse. The answer is the conjunction of no program-wide refusal, every tampering site being guaranteed to run after the anchor, and the anchor executing at most once — the conjunct the name does not suggest: a site the anchor's re-execution could cross on a second run is a site before it, so a call inside a loop or a twice-called function is refused wherever a site exists at all. With no sites there is nothing to cross and the answer is `True` without the multiplicity walk. """ cached = self._anchor_cache.get(id(anchor)) if cached is None: cached = self._compute_intact(anchor) self._anchor_cache[id(anchor)] = cached return cached def trust_at(self, anchor)-
The questions whether the built-ins are still the language's when anchor is evaluated:
builtins_intact_atcomposed with the effect model's per-name arms (AnchoredTrust).Expand source code Browse git
def trust_at(self, anchor: Node) -> AnchoredTrust: """ The questions whether the built-ins are still the language's when *anchor* is evaluated: `builtins_intact_at` composed with the effect model's per-name arms (`AnchoredTrust`). """ return AnchoredTrust( self.effects, lambda: self.builtins_intact_at(anchor), lambda: self.code_load_may_precede(anchor), ) def code_load_may_precede(self, anchor)-
Whether code the text does not hold may have been loaded before anchor runs: the file imports such code, which the host runs before its body (
SemanticModel.imports_code()), or a site ofSemanticModel.code_load_sites()may come first. A site comes first unless the anchor executes at most once (at_most_once) andDominanceModel.cannot_precede()places the site after it: a second run of the anchor follows a load the first run preceded.Expand source code Browse git
def code_load_may_precede(self, anchor: Node) -> bool: """ Whether code the text does not hold may have been loaded before *anchor* runs: the file imports such code, which the host runs before its body (`refinery.lib.scripts.js.analysis.model.SemanticModel.imports_code`), or a site of `refinery.lib.scripts.js.analysis.model.SemanticModel.code_load_sites` may come first. A site comes first unless the anchor executes at most once (`at_most_once`) and `refinery.lib.scripts.js.analysis.dominance.DominanceModel.cannot_precede` places the site after it: a second run of the anchor follows a load the first run preceded. """ if self.model.imports_code(): return True sites = self.model.code_load_sites() if not sites: return False if not self.at_most_once(anchor): return True return not all(self.dominance.cannot_precede(site, anchor) for site in sites) def singular_value_at(self, binding, at)-
The single value binding provably holds at the moment at is evaluated —
SemanticModel.singular_valuewith the position supplied, so a binding the program-wide volatility question refuses can still answer where ordering proves the read safe. The value is the one channel the text spells, judged by three legs, each asked of every binding the query answers:- establishment: the channel's establishing write has run before the read, judged by the
SemanticModel.binding_establishment_sitesrules, so a hoisted function declaration needs no ordering; - hazards: every dynamic rebind the located question
(
SemanticModel.binding_dynamic_rebind_sites) finds first-executes after the read; - multiplicity: the read executes at most once (
at_most_once), since ordering first executions does not bound what a read on a cycle sees on its second one.
The channels and their completeness are read on the ignore view (
binding_valueswith ignore_dynamic_rebinds), the one where a rebind is a located hazard rather than a program-wide refusal. A script-scope binding fails closed as leg 2's own case: its eval-surface hazards are the whole-program questions the volatility boolean already refuses to freeze it on.Nonewhenever any leg fails, the binding holds no single spelled value, or its only incompleteness is the nodeless entry write. Memoized per (binding, read) for the model's lifetime, exactly as the site enumeration is — the recognizer asks the same hops from the site enumeration and the flow walk.Expand source code Browse git
def singular_value_at(self, binding: Binding | None, at: Node) -> Node | None: """ The single value *binding* provably holds at the moment *at* is evaluated — `SemanticModel.singular_value` with the position supplied, so a binding the program-wide volatility question refuses can still answer where ordering proves the read safe. The value is the one channel the text spells, judged by three legs, each asked of every binding the query answers: 1. establishment: the channel's establishing write has run before the read, judged by the `SemanticModel.binding_establishment_sites` rules, so a hoisted function declaration needs no ordering; 2. hazards: every dynamic rebind the located question (`SemanticModel.binding_dynamic_rebind_sites`) finds first-executes after the read; 3. multiplicity: the read executes at most once (`at_most_once`), since ordering first executions does not bound what a read on a cycle sees on its second one. The channels and their completeness are read on the ignore view (`binding_values` with *ignore_dynamic_rebinds*), the one where a rebind is a located hazard rather than a program-wide refusal. A script-scope binding fails closed as leg 2's own case: its eval-surface hazards are the whole-program questions the volatility boolean already refuses to freeze it on. `None` whenever any leg fails, the binding holds no single spelled value, or its only incompleteness is the nodeless entry write. Memoized per (binding, read) for the model's lifetime, exactly as the site enumeration is — the recognizer asks the same hops from the site enumeration and the flow walk. """ if binding is None: return None key = (id(binding), id(at)) if key not in self._positioned_cache: self._positioned_cache[key] = self._compute_singular_value_at(binding, at) return self._positioned_cache[key] - establishment: the channel's establishing write has run before the read, judged by the
def at_most_once(self, anchor)-
Whether anchor executes at most once: its own control-flow node is off a cycle, and every activation up to the script root is invoked at most once — exactly one alias-resolved invocation site, off a cycle, not lexically inside the function it invokes. An activation that may be called without its name, through the
callerof another function, its ownargumentsornew.target(SemanticModel.reached_without_its_name()), breaks it: it runs again where no textual count sees it. So does an activation the host may invoke freely or that reflective code in its own scope could re-invoke by name, and the initializer of a field, which runs at each construction of its class.Expand source code Browse git
def at_most_once(self, anchor: Node) -> bool: """ Whether *anchor* executes at most once: its own control-flow node is off a cycle, and every activation up to the script root is invoked at most once — exactly one alias-resolved invocation site, off a cycle, not lexically inside the function it invokes. An activation that may be called without its name, through the `caller` of another function, its own `arguments` or `new.target` (`refinery.lib.scripts.js.analysis.model.SemanticModel.reached_without_its_name`), breaks it: it runs again where no textual count sees it. So does an activation the host may invoke freely or that reflective code in its own scope could re-invoke by name, and the initializer of a field, which runs at each construction of its class. """ located = self.dominance.locate(anchor) if located is None: return False if self._cycles.on_a_cycle(located[0], located[1]): return False function = enclosing_run(anchor) seen: set[int] = set() while function is not None: if not isinstance(function, FUNCTION_NODES) or id(function) in seen: return False seen.add(id(function)) if self.model.reached_without_its_name(function): return False flow = self._invocations_of(function) if flow.escapes or len(flow.invocations) != 1: return False point = flow.invocations[0] if enclosing_run(point) is function: return False if not self._invocation_is_singly_executed(function, point): return False function = enclosing_run(point) return True def every_invocation_discards_the_value(self, function, value_discarded)-
Whether every invocation of function throws its completion value away, judged by the forward value-flow enumeration
_invocations_ofreads: a position the value reaches without being invoked — an argument, a store, a return — could hand it to a reader, so one such escape refuses the answer, as does a function the text never invokes at all (an entrypoint or dead code, neither of which this model can pin). value_discarded classifies each invocation position, the caller's own reading of the positions it knows; no invocation, or one whose value a caller of this method would keep, answersFalse.The enumeration must also be complete — nothing may run function outside the invocations it listed, since a host or reflective caller could read what every listed one discards. Multiplicity is deliberately absent: a value discarded at every invocation is discarded at each of them however often they run.
Expand source code Browse git
def every_invocation_discards_the_value( self, function: Node, value_discarded: Callable[[Node], bool], ) -> bool: """ Whether every invocation of *function* throws its completion value away, judged by the forward value-flow enumeration `_invocations_of` reads: a position the value reaches without being invoked — an argument, a store, a return — could hand it to a reader, so one such escape refuses the answer, as does a function the text never invokes at all (an entrypoint or dead code, neither of which this model can pin). *value_discarded* classifies each invocation position, the caller's own reading of the positions it knows; no invocation, or one whose value a caller of this method would keep, answers `False`. The enumeration must also be complete — nothing may run *function* outside the invocations it listed, since a host or reflective caller could read what every listed one discards. Multiplicity is deliberately absent: a value discarded at every invocation is discarded at each of them however often they run. """ flow = self._invocations_of(function) if flow.escapes or not flow.invocations: return False if not self._invocation_enumeration_is_complete(function): return False return all(value_discarded(point) for point in flow.invocations)