Module refinery.lib.scripts.js.analysis.intrinsics
What a JavaScript program writes on the intrinsics, computed once per
SemanticModel. build_intrinsic_writes() scans the program
for every global name it binds, assigns, writes a property on, installs a descriptor on, or hands to
code this analysis cannot read, and for the property keys it writes on the names whose keys are
watched; IntrinsicWrites holds the answer and asks the prototype-chain questions of it.
The scan reads only the semantic model, so it sits beside it rather than above any layer that orders
or summarizes code: the EffectModel and the
DominanceModel both read the one answer
ModelCache builds, and neither has to reach into the other
for it. The registry of intrinsics the scan watches lives here for the same reason.
Expand source code Browse git
"""
What a JavaScript program writes on the intrinsics, computed once per
`refinery.lib.scripts.js.analysis.model.SemanticModel`. `build_intrinsic_writes` scans the program
for every global name it binds, assigns, writes a property on, installs a descriptor on, or hands to
code this analysis cannot read, and for the property keys it writes on the names whose keys are
watched; `IntrinsicWrites` holds the answer and asks the prototype-chain questions of it.
The scan reads only the semantic model, so it sits beside it rather than above any layer that orders
or summarizes code: the `refinery.lib.scripts.js.analysis.effects.EffectModel` and the
`refinery.lib.scripts.js.analysis.dominance.DominanceModel` both read the one answer
`refinery.lib.scripts.js.analysis.cache.ModelCache` builds, and neither has to reach into the other
for it. The registry of intrinsics the scan watches lives here for the same reason.
"""
from __future__ import annotations
import enum
from typing import Iterator, NamedTuple, Sequence
from refinery.lib.scripts import Node
from refinery.lib.scripts.analysis.components import strongly_connected_components
from refinery.lib.scripts.js.analysis.model import (
_TESTED_FORMS,
FUNCTION_NODES,
Binding,
Role,
SemanticModel,
_governing_target,
enclosing_function,
is_invocation_target,
is_member_write_target,
reference_role,
)
from refinery.lib.scripts.js.builtins import (
BUILTIN_MEMBER_NAMES,
PROTOTYPE_CHAIN_PROPERTIES,
RECEIVER_WRITING_METHODS,
)
from refinery.lib.scripts.js.keys import accessor_installs, key_is, key_may_be, key_text
from refinery.lib.scripts.js.model import (
ACCESSOR_INSTALL_METHODS,
LOGICAL_ASSIGNMENT_OPERATORS,
JsArrowFunctionExpression,
JsAssignmentExpression,
JsBinaryExpression,
JsCallExpression,
JsConditionalExpression,
JsExpressionStatement,
JsForInStatement,
JsForOfStatement,
JsFunctionDeclaration,
JsFunctionExpression,
JsIdentifier,
JsLogicalExpression,
JsMemberExpression,
JsNewExpression,
JsParenthesizedExpression,
JsProperty,
JsReturnStatement,
JsSequenceExpression,
JsSpreadElement,
JsTaggedTemplateExpression,
JsTemplateLiteral,
JsUnaryExpression,
JsUpdateExpression,
JsVariableDeclarator,
strip_parens,
)
PURE_INTRINSIC_METHODS = frozenset({
'String.fromCharCode',
'Array.isArray',
'Math.abs',
'Math.ceil',
'Math.floor',
'Math.round',
'Math.trunc',
'Math.sign',
'Math.max',
'Math.min',
'Math.pow',
'Math.sqrt',
'Math.cbrt',
'Math.log',
'Math.log2',
'Math.log10',
'Math.exp',
'Number.isNaN',
'Number.isFinite',
'Number.isInteger',
'Number.isSafeInteger',
'Number.parseInt',
'Number.parseFloat',
})
PURE_GLOBAL_FUNCTIONS = frozenset({
'parseInt',
'parseFloat',
'isNaN',
'isFinite',
})
SPECIES_KEYS = PROTOTYPE_CHAIN_PROPERTIES
"""
The two property names that decide what an allocating `Array.prototype` method returns.
ArraySpeciesCreate reads `constructor[Symbol.species]` off the receiver, and `__proto__` replaces
the prototype that `constructor` lookup walks, so writing either can make the "newly created" array
be a shared object — or make the call throw, by leaving a primitive where a constructor is expected.
"""
_SURFACE_KEYS = SPECIES_KEYS | frozenset({'prototype'})
"""
The property names whose value shares the mutable surface of the intrinsic it was read from, so
handing that value to unanalysable code is as dangerous as handing over the intrinsic itself.
`SPECIES_KEYS` reach the prototype chain and `prototype` is the chain. Every other key yields
something whose properties nobody consults when the intrinsic is used: patching a property of the
number `Math.PI` or of the function `Math.floor` cannot change what `Math.floor(1.7)` returns, while
patching one of `Array.prototype` decides what `[1, 2].join()` means.
"""
PURE_INTRINSIC_ROOTS = (
frozenset(name.split('.', 1)[0] for name in PURE_INTRINSIC_METHODS) | PURE_GLOBAL_FUNCTIONS
)
class _GlobalObject:
"""
Sentinel denoting the global object as the value an expression evaluates to, distinct from a
named intrinsic root and from an unresolved value.
"""
GLOBAL_OBJECT = _GlobalObject()
_DENOTED_ROOT_DEPTH_LIMIT = 16
_CALLEE_DEPTH_LIMIT = 4
"""
How far `_callee_is_write_free` follows an intrinsic through nested calls before refusing. A chain
longer than this is not proven safe, merely unproven, so exhausting the limit records the write.
Four was enough for every shape measured; the limit exists because the recursion is over call
*edges*, which a mutually-recursive pair makes unbounded.
"""
_VALUE_FORWARDING_NODES = (
JsParenthesizedExpression,
JsLogicalExpression,
JsConditionalExpression,
JsSequenceExpression,
JsSpreadElement,
)
"""
The forms that hand an operand's value onward unchanged, so an intrinsic inside one escapes wherever
the form itself does. These are the outward counterpart of the arms `_denoted_roots` looks *into*,
and the pairing is not incidental: a fold that collapses `Math || 0` to `Math` must not change this
analysis's answer, which the pinning contract in `refinery.lib.scripts.js.analysis.cache.ModelCache`
requires.
"""
PROTOTYPE_OWNERS: dict[str, str] = {
'str': 'String',
'list': 'Array',
'JsBuffer': 'Buffer',
'dict': 'Object',
'bool': 'Boolean',
'int': 'Number',
'float': 'Number',
'JsFunctionDeclaration': 'Function',
'JsFunctionExpression': 'Function',
'JsArrowFunctionExpression': 'Function',
'RegExpObject': 'RegExp',
}
"""
The intrinsic whose prototype supplies the methods of each interpreter value type, keyed by type
name so this module needs no import from the interpreter that depends on it. A method call on a
literal receiver names no global at the call site, so trusting it means asking whether *this*
prototype is intact: `String.prototype.toUpperCase = f` is a write to `String`, and it changes what
`'ab'.toUpperCase()` means even though the expression mentions no identifier at all.
Every type in the interpreter's value domain is named here directly, including
`refinery.lib.scripts.js.values.JsBuffer`, whose methods come from `Buffer.prototype`
rather than the `Array.prototype` its `list` base would suggest. A function value is represented as
its own AST node, so the two function node names appear here as value types rather than as syntax;
both inherit from `Function.prototype`. Lookup is exact rather than a walk up the Python MRO, which
would silently answer `Array` for any future `list` subclass instead of refusing. A type with no
entry is never trusted through this route.
"""
INHERITED_CHAIN_ROOTS = frozenset({'Object'})
"""
The prototypes every value inherits from beyond the one that owns its own methods.
`Object.prototype` roots every chain, so a getter installed there is reached by a plain read on an
array literal and on `Math` alike — which is why reading a property is a strictly stronger
requirement than calling a method.
"""
KEYED_WRITE_ROOTS = (
PURE_INTRINSIC_ROOTS
| INHERITED_CHAIN_ROOTS
| frozenset(PROTOTYPE_OWNERS.values())
)
"""
The global names whose written property keys `IntrinsicWrites.key_written` bounds. A name outside
this set is one the scan records no key for, so answering `False` about it would read an absence of
evidence as evidence of absence; that predicate answers `True` for every key of such a name instead.
The set is the union of the names this module already watches for a reason: the intrinsics whose
methods are trusted, the roots a plain property read walks through, and the owners of the prototypes
that supply each value type's members. A caller asking about a name outside it is asking a question
this scan was not built to answer, and is told so.
"""
ESCAPE_RECORDED_ROOTS = PURE_INTRINSIC_ROOTS | frozenset({'RegExp', 'Date', 'Iterator'})
"""
The global names an escape records as written by name, not only by key: the names a fold trusts
to run code. A fold runs the methods of the pure roots, a fold that decides what a regular
expression answers trusts `RegExp`, so `p(RegExp)` for a `p` that replaces `RegExp.prototype.test`
must withdraw that trust, a fold that decides a comparison of the clock trusts `Date`, which
`Reflect.set(Date, 'now', f)` must withdraw, and a fold that converts an array iterator trusts the
prototype `Iterator` holds, which every such iterator inherits.
`Object` and `Function` are left out on purpose. Real samples hand `Object` to a call this scan
cannot resolve, and recording that escape refuses every fold that trusts `Object.prototype` in
them; the precise answer follows the escaped value to its writes, which this scan does not do.
"""
ESCAPE_WATCHED_ROOTS = KEYED_WRITE_ROOTS | ESCAPE_RECORDED_ROOTS
"""
The global names whose escape the scan looks for: every name it records keys for, and every name
`ESCAPE_RECORDED_ROOTS` records by name.
"""
_KEY_RECORDED_ROOTS = ESCAPE_WATCHED_ROOTS - ESCAPE_RECORDED_ROOTS
"""
The watched names whose escape the scan records by key alone: `IntrinsicWrites.key_written`
answers every key of one handed off, or of one read as a property (`g.Object`) and handed off or
written through, while `IntrinsicWrites.names` still lists it as unwritten.
"""
_WRITING_METHODS = RECEIVER_WRITING_METHODS | ACCESSOR_INSTALL_METHODS
"""
The methods that write the object they are read off: the methods of the built-in prototypes that
store into their receiver, and the accessor installers.
"""
_PROTOTYPE_READERS = frozenset({'getPrototypeOf'})
_PROTOTYPE_KEY = frozenset({'prototype'})
_CONSTRUCTOR_KEY = frozenset({'constructor'})
class _BuiltinReach:
"""
Which values of a program may be a built-in object the scan cannot attribute a write through to
the root owning it, and whether a write, a call writing its receiver, or a hand-off reaches one:
the question behind `IntrinsicWrites.followable`.
The scan attributes a write to the name at the foot of the chain it goes through and follows
that name through the bindings holding it (`_IntrinsicAliases`). A built-in object reached
without spelling its owner escapes both: `''.constructor.prototype.x = 1` writes
`String.prototype` through no name, and so does `var p = [].__proto__; p.x = 1`. A value is
*unattributed* where it may be such an object:
- a member read whose key is unknown, or one a built-in type's chain supplies
(`refinery.lib.scripts.js.builtins.BUILTIN_MEMBER_NAMES`): each may hand out a prototype or a
function the language installed;
- `prototype` read off an unattributed value;
- a call of a method whose key may be `getPrototypeOf` (`refinery.lib.scripts.js.keys.key_may_be`);
- a name of a binding that a declarator or an assignment stores an unattributed value into, or
that a destructuring takes under a key the first arm counts;
- an operand handed on unchanged by a logical operator, a conditional, a sequence or a plain
assignment, where the operand is unattributed.
A fold leaves the set of unattributed values as it was or smaller: a key it collapses to a
string the first arm does not count takes a value out, and a forwarding form it removes is one
the search looked through.
"""
def __init__(self, model: SemanticModel, aliases: _IntrinsicAliases):
self.model = model
self.aliases = aliases
self._answers: dict[Node | Binding, bool] = {}
def unattributed(self, node: Node) -> bool:
"""
Whether *node* may evaluate to an unattributed value. The values and bindings *node* may
take its value from are searched with a stack rather than by recursion, so a chain of names
of any length costs no depth. Every one searched without finding an unattributed value is
remembered as having none, and every one on the way from *node* to one found is remembered
as having one, so the many reads of one name cost one search between them.
"""
known = self._answers.get(node)
if known is not None:
return known
leaf, sources = self._sources(node)
if leaf or not sources:
self._answers[node] = leaf
return leaf
searched: list[Node | Binding] = [node]
seen: set[Node | Binding] = {node}
reached_from: dict[Node | Binding, Node | Binding] = {}
pending: list[tuple[Node | Binding, Node | Binding]] = [(source, node) for source in sources]
found: Node | Binding | None = None
while pending:
item, origin = pending.pop()
if item in seen:
continue
seen.add(item)
reached_from[item] = origin
known = self._answers.get(item)
if known is False:
continue
if known is True:
found = item
break
searched.append(item)
leaf, sources = self._sources(item)
if leaf:
found = item
break
pending.extend((source, item) for source in sources)
if found is None:
for item in searched:
self._answers[item] = False
return False
cursor = found
while cursor is not node:
self._answers[cursor] = True
cursor = reached_from[cursor]
self._answers[node] = True
return True
def _sources(self, item: Node | Binding) -> tuple[bool, list[Node | Binding]]:
"""
Whether *item* is unattributed by itself, and otherwise the values and bindings it may take
its value from.
"""
if isinstance(item, Binding):
return self._binding_sources(item)
node = strip_parens(item)
if isinstance(node, JsIdentifier):
binding = self.model.resolve(node)
return False, [] if binding is None else [binding]
if isinstance(node, JsMemberExpression):
if key_may_be(node, BUILTIN_MEMBER_NAMES):
return True, []
if key_may_be(node, _PROTOTYPE_KEY) and node.object is not None:
return False, [node.object]
return False, []
if isinstance(node, JsCallExpression):
callee = strip_parens(node.callee)
reads_a_prototype = isinstance(callee, JsMemberExpression) and key_may_be(callee, _PROTOTYPE_READERS)
return reads_a_prototype, []
sources: Sequence[Node | None] = ()
if isinstance(node, JsLogicalExpression):
sources = [node.left, node.right]
elif isinstance(node, JsConditionalExpression):
sources = [node.consequent, node.alternate]
elif isinstance(node, JsSequenceExpression):
sources = node.expressions[-1:]
elif isinstance(node, JsAssignmentExpression):
if node.operator == '=':
sources = [node.right]
elif node.operator in LOGICAL_ASSIGNMENT_OPERATORS:
sources = [node.left, node.right]
return False, [source for source in sources if source is not None]
def _binding_sources(self, binding: Binding) -> tuple[bool, list[Node | Binding]]:
"""
Whether *binding* takes an unattributed value from a destructuring by itself
(`_destructured_under_a_builtin_key`), and otherwise the values its declarators and the
assignments to it store. A declaration and an assignment destructure alike.
"""
sources: list[Node | Binding] = []
for declaration in binding.declarations:
parent = declaration.parent
if isinstance(parent, JsVariableDeclarator) and parent.id is declaration:
if parent.init is not None:
sources.append(parent.init)
elif _destructured_under_a_builtin_key(declaration):
return True, []
for reference in (*binding.writes, *binding.indefinite_writes):
parent = reference.parent
if isinstance(parent, JsAssignmentExpression) and parent.left is reference:
if parent.right is not None:
sources.append(parent.right)
elif _destructured_under_a_builtin_key(reference):
return True, []
return False, sources
def trips_at(self, node: Node) -> bool:
"""
Whether *node* writes a built-in object in a way the scan does not follow: it writes through
an unattributed value, calls a method that may write its receiver
(`refinery.lib.scripts.js.builtins.RECEIVER_WRITING_METHODS`, or a key not known) on a
value that may be a built-in object, or is an unattributed value and hands it off. The scan
reads no call as a write, so `Array.prototype.push(5)`, which stores an element every array
then inherits, counts here although it names its owner. A name an assignment stores into
hands no value anywhere; what a logical assignment evaluates to, which may be the name's old
value, is asked of the assignment.
"""
for member in _written_members(_write_target(node)):
if member.object is not None and self.unattributed(member.object):
return True
if isinstance(node, JsCallExpression):
callee = strip_parens(node.callee)
if isinstance(callee, JsMemberExpression) and callee.object is not None:
if key_may_be(callee, RECEIVER_WRITING_METHODS) and self._may_be_a_builtin(callee.object):
return True
if isinstance(node, JsIdentifier) and reference_role(node) is not Role.READ:
return False
return self.unattributed(node) and self._handed_off(node)
def _may_be_a_builtin(self, node: Node) -> bool:
"""
Whether *node* may evaluate to a built-in object: an unattributed value, or a value rooted
at a name the scan watches (`ESCAPE_WATCHED_ROOTS`), as `Array.prototype` is rooted at
`Array`.
"""
if self.unattributed(node):
return True
return any(
self.aliases.names_denoted_by(root) & ESCAPE_WATCHED_ROOTS
for root in _denoted_roots(node)
)
def _handed_off(self, node: Node) -> bool:
"""
Whether the unattributed value at *node*, carried outward through the forms that hand it on,
reaches a position `_hands_over` reads as handing it over, or is stored into anything but a
name, whose binding the search follows instead. A read off it hands over nothing: what the
read yields is a value of its own, which this asks about where it stands.
"""
cursor = node
while True:
parent = cursor.parent
if parent is None or isinstance(parent, JsExpressionStatement):
return False
if isinstance(parent, JsMemberExpression) and parent.object is cursor:
return False
if isinstance(parent, JsConditionalExpression) and parent.test is cursor:
return False
if not isinstance(parent, _VALUE_FORWARDING_NODES):
break
cursor = parent
target = _rebinding_target(cursor)
if target is not None:
return not isinstance(strip_parens(target), JsIdentifier)
return _hands_over(self.model, self.aliases, cursor, 0)
def _destructured_under_a_builtin_key(target: Node) -> bool:
"""
Whether the destructuring that stores into *target* reads, on its way there, a key a built-in
type's chain may supply (`refinery.lib.scripts.js.builtins.BUILTIN_MEMBER_NAMES`) or a key not
known. Each of `({constructor: S} = '')`, `var {constructor: S = 0} = ''` and
`var {constructor: {prototype: P}} = ''` stores a built-in object without spelling its owner.
The way from *target* to the pattern is the climb
`refinery.lib.scripts.js.analysis.model.is_member_write_target` takes, through defaults and
nested patterns, so every property it passes is one the destructuring reads.
"""
_, outermost = _governing_target(target)
cursor = target
while cursor is not outermost and cursor.parent is not None:
cursor = cursor.parent
if isinstance(cursor, JsProperty) and key_may_be(cursor, BUILTIN_MEMBER_NAMES):
return True
return False
def _constructs_through_an_unread_key(model: SemanticModel, node: Node) -> bool:
"""
Whether, under the model that suspects code it cannot read, *node* calls or constructs through a
`constructor` read or a key not known. The reflection check counts no such read that is invoked
in place (`test_unfixed_defects.TestCodeRunThroughAConstructorReadInPlaceIsAReflectionSurface`),
so a string of code `[].constructor.constructor(s)()` runs is one nothing else accounts for.
"""
if model.trust_eval or not isinstance(node, (JsCallExpression, JsNewExpression)):
return False
callee = strip_parens(node.callee)
return isinstance(callee, JsMemberExpression) and key_may_be(callee, _CONSTRUCTOR_KEY)
class IntrinsicWrites(NamedTuple):
"""
What one scan of a program says about the globals it writes: *names*, the set a caller asks
about a whole name with, and *keys*, the properties each of the watched names was written at —
or `None` for a name whose written keys this scan cannot bound. *aliases* is the relation the
scan resolved write targets through, kept for `follows_only_through_bindings`. *followable*
says whether the scan follows every write the program makes on a built-in object: none reaches
one through a value read without spelling its owner (`_BuiltinReach`), no watched name is
handed off, nor read as a property and handed off or written through, where the scan records
the escape by key alone (`_KEY_RECORDED_ROOTS`), and, under the model that suspects code it
cannot read, no call runs through a key the reflection check leaves uncounted
(`_constructs_through_an_unread_key`). A fold that reads a built-in's chain or converts an
object without spelling its owner asks it on top of the questions about *names*.
The two are produced together because they are read off the same nodes and must not disagree:
a name recorded in one for a reason the other cannot express is a name one caller refuses and
the other clears. Where a write cannot be pinned to a key, *keys* records the name unbounded
rather than omitting it, so `keys` never reports less about a name than `names` does.
"""
names: frozenset[str]
keys: dict[str, frozenset[str] | None]
aliases: _IntrinsicAliases
followable: bool
def key_written(self, name: str, key: str) -> bool:
"""
Whether the program writes the property *key* on a chain rooted at the global *name*:
`Object.prototype.constructor = C`, `delete Object.getPrototypeOf`, or a descriptor
installed for that key. A name whose written keys cannot be bounded answers `True` for
every key, and so does a name outside `KEYED_WRITE_ROOTS`, which the scan records nothing
about at all. See `refinery.lib.scripts.js.analysis.effects.EffectModel.global_key_written`
for why the question is asked of a name rather than of a receiver.
"""
if name not in KEYED_WRITE_ROOTS:
return True
keys = self.keys.get(name, frozenset())
return keys is None or key in keys
def follows_only_through_bindings(self, value: Node) -> bool:
"""
Whether a write through what *value* evaluates to is one this scan or the semantic model it
read records where the value is spelled or a binding holds it, and never through a property
holding it. The scan follows a value through the bindings that hold it and trusts `Object`,
`Function`, `Boolean` and `Buffer` when they escape (`ESCAPE_RECORDED_ROOTS`), and the
semantic model finds a write to a global through a name holding the global object, not
through a property holding it. Stored in a property, `scope.P = Object.prototype` lets
`scope.P.p = 1` write `Object.prototype` unrecorded; stored in a binding,
`var P = Object.prototype` makes `P.p = 1` a recorded write.
A value may carry what any of its `denoting_leaves` evaluates to, and one of them qualifies
where the scan's aliases resolve it to a root the scan watches, where it may be the global
object, or where the model was not built over it and so cannot say. The foot of a member
chain is no value the chain evaluates to: it qualifies through the root it names, as
`Object` does for `Object.prototype`, and never as the global object, since what
`window.document` evaluates to is a property of that object.
"""
model = self.aliases.model
for leaf in denoting_leaves(value):
if not model.knows(leaf):
return True
if isinstance(leaf, JsIdentifier) and self.aliases.names_denoted_by(leaf) & ESCAPE_WATCHED_ROOTS:
return True
if not _is_member_object(leaf) and model.may_be_the_global_object(leaf):
return True
return False
def records_a_read(self, node: Node, names: frozenset[str]) -> bool:
"""
Whether the scan records the watched *names* read at *node*: what *node* evaluates to is
written through or escapes (`_records_a_watched_read`), the test the scan applies to a
property read naming one of them.
"""
return _records_a_watched_read(self.aliases.model, self.aliases, node, names)
def denotes_a_watched_root(self, binding: Binding) -> bool:
"""
Whether a value *binding* may hold denotes a root the scan watches, so that a write through
a name resolving to the binding is one the scan attributes to that root.
"""
return bool(self.aliases.names_of(binding) & ESCAPE_WATCHED_ROOTS)
def roots_unwritten(self, owner: str, roots: frozenset[str]) -> bool:
"""
Whether the program writes neither *owner* nor any prototype in *roots*. A property read
resolves against the whole prototype chain rather than one prototype, so each name the chain
passes through has to answer the same question
`refinery.lib.scripts.js.analysis.effects.EffectModel.trusted_prototype` asks of the owner
alone.
"""
return all(name not in self.names for name in (owner, *roots))
def chain_roots_unwritten(self, value_type: type) -> bool:
"""
Whether the program writes no prototype a plain property read on a value of *value_type*
consults, so such a read can run no accessor the program installed there. See
`refinery.lib.scripts.js.analysis.effects.EffectModel.chain_roots_unwritten` for what the
answer leaves out and who may ask it.
"""
owner = PROTOTYPE_OWNERS.get(value_type.__name__)
if owner is None:
return False
return self.roots_unwritten(owner, INHERITED_CHAIN_ROOTS)
def records_more_than(self, other: IntrinsicWrites) -> bool:
"""
Whether this scan records a written name, a key written on a name, or a name whose written
keys it cannot bound, that the scan *other* does not record.
"""
if self.names - other.names:
return True
for name, keys in self.keys.items():
known = other.keys.get(name, frozenset())
if known is None:
continue
if keys is None or keys - known:
return True
return False
class SpeculationState(enum.Enum):
"""
Where a `WriteSpeculation` stands.
"""
ASSUMING = 'assuming'
DISCHARGED = 'discharged'
REFUTED = 'refuted'
class SpeculationRefuted(Exception):
"""
Raised by `WriteSpeculation.observe` at a build of the scan that records more than the build
before it, and at every build after that one.
"""
class WriteSpeculation:
"""
The record of a run that assumes every write on a built-in object the scan does not follow
(`IntrinsicWrites.followable`) writes nothing. While it assumes, `observe` presents each build
of the scan as following every write and compares it with the build before it: one that
records more (`IntrinsicWrites.records_more_than`) refutes the assumption. A build that follows
every write and records no more discharges it, and every later build is presented as it is.
A run that ends discharged may keep its result. Each fold it made before the discharge read the
record of some build and assumed nothing outside that record is written. No build records more
than the one before, so the record of the discharging build, R, is part of every earlier one.
Take an execution and the first write outside the record a fold relied on, if one happens: up to
that moment every fold's premise held, so the discharged tree behaves alike up to it and makes
the same write. That tree has no write the scan does not follow, so the write is in R, and so in
every earlier record: no fold relied on its absence. So no such write happens. Code a splice
brings in after the discharge is read by the scan as it is, as in any run.
"""
def __init__(self):
self.state = SpeculationState.ASSUMING
self._record: IntrinsicWrites | None = None
@property
def assuming(self) -> bool:
return self.state is SpeculationState.ASSUMING
@property
def discharged(self) -> bool:
return self.state is SpeculationState.DISCHARGED
def observe(self, writes: IntrinsicWrites) -> IntrinsicWrites:
"""
The build *writes* of the scan as the run reads it: presented as following every write
while the assumption holds, and as it is once the assumption is discharged. Raises
`SpeculationRefuted` where *writes* records more than the build before it, and at every
build once that has happened.
"""
if self.state is SpeculationState.DISCHARGED:
return writes
if self.state is SpeculationState.REFUTED or (
self._record is not None and writes.records_more_than(self._record)
):
self.state = SpeculationState.REFUTED
raise SpeculationRefuted
self._record = writes
if writes.followable:
self.state = SpeculationState.DISCHARGED
return writes
return writes._replace(followable=True)
class UnfollowedWriteRefusals:
"""
The record one deobfuscation run keeps of whether a question was answered no because the scan
does not follow every write on a built-in object (`IntrinsicWrites.followable`), on any tree of
the run. Only such an answer can refuse a fold, so a run whose last tree is not followed runs
again under a `WriteSpeculation` only where one was given.
"""
def __init__(self):
self.refused = False
def note(self) -> None:
"""
Record that a question was answered no because the scan does not follow every write.
"""
self.refused = True
def build_intrinsic_writes(model: SemanticModel) -> IntrinsicWrites:
"""
The set of global names the program does anything with beyond reading them, and the property
keys it writes on each of the names whose keys are watched.
A name belongs to the first for binding it in any scope, assigning to it, writing or updating or
deleting a property anywhere along a chain rooted at it (`Object.prototype.x = 1`,
`Math.PI++`), installing a descriptor on it with `Object.defineProperty`, or handing it to
code whose writes this analysis cannot enumerate.
This is the per-name counterpart of
`refinery.lib.scripts.js.analysis.effects._intrinsics_pristine`, which answers the same question
for a fixed root set but collapses it to one program-wide flag. Keeping the answer per name is
what lets a program that patches `Object.prototype` still have its `Math.floor` calls folded;
the flag cannot express that, because one disturbed root disables every other one.
Bindings cover every *assignment* form too, so no separate scan of write-role identifiers is
needed: a bare `Math = 1` introduces an implicit-global binding for `Math`, as do the
destructuring and `for`-target forms. Only property writes, deletes, and descriptor installs —
which leave the name itself a plain read — need the explicit branches below.
A write target names the intrinsic it patches only when the program spells it out.
`var m = Math; m.floor = f` patches `Math` while mentioning it nowhere in the assignment, so the
chain root is resolved through the values its binding may hold rather than taken as the name it
is spelled with.
A write need not be *anywhere* in the program for a name to belong here. Handing an intrinsic to
code whose writes this analysis cannot enumerate — `patch(Math)` for a `patch` it cannot resolve
— leaves the name looking untouched while its properties are replaced, so an intrinsic that
escapes is recorded as written. `_value_escapes` decides which uses hand the value over.
The keyed answer is the same scan read for the property a write names rather than only for the
name it is rooted at, and it exists because the per-name question is too coarse for a caller
that cares which property was replaced — a file patching `Object.prototype.z` has written
`Object`, and refusing everything about `Object` on that basis refuses the very files the
question is asked about.
Only the *final* key of a chain is recorded, which is the one a write replaces:
`Object.prototype.z = 9` replaces `z` and leaves `prototype` and `constructor` alone, so
recording the keys it passes through would report a file as having patched the mechanism a
caller is asking about when it did nothing of the kind. Every other route by which a name's
properties can change — a binding that shadows it, a value that escapes, a computed key, a
descriptor read from a value this analysis cannot read — bounds no key at all and is recorded
as unbounded.
"""
names: set[str] = set()
keys: dict[str, set[str] | None] = {}
def record_keys(found: frozenset[str], key: str | None) -> None:
for name in found & KEYED_WRITE_ROOTS:
if key is None:
keys[name] = None
continue
known = keys.setdefault(name, set())
if known is not None:
known.add(key)
def record(found: frozenset[str], key: str | None) -> None:
names.update(found)
record_keys(found, key)
pending = [model.root_scope]
while pending:
scope = pending.pop()
record(frozenset(scope.bindings), None)
pending.extend(scope.children)
aliases = _IntrinsicAliases(model)
reach = _BuiltinReach(model, aliases)
followable = True
def record_escape(watched: frozenset[str]) -> None:
nonlocal followable
record_keys(watched, None)
names.update(watched & ESCAPE_RECORDED_ROOTS)
if watched & _KEY_RECORDED_ROOTS:
followable = False
for node in model.root.walk():
if followable and (reach.trips_at(node) or _constructs_through_an_unread_key(model, node)):
followable = False
if isinstance(node, JsIdentifier):
if not _names_a_member_property(node) and reference_role(node) is Role.READ:
watched = aliases.names_denoted_by(node) & ESCAPE_WATCHED_ROOTS
if watched and _value_escapes(model, aliases, node, watched):
record_escape(watched)
continue
if isinstance(node, JsMemberExpression):
if not key_may_be(node, ESCAPE_WATCHED_ROOTS):
continue
key = key_text(node)
watched = ESCAPE_WATCHED_ROOTS if key is None else frozenset({key})
if (
_records_a_watched_read(model, aliases, node, watched)
and key_is(node, ESCAPE_WATCHED_ROOTS, model.assumed_keys)
):
record_escape(watched)
continue
if isinstance(node, JsCallExpression):
for install in accessor_installs(node):
for base in _denoted_roots(install.target):
record(aliases.names_denoted_by(base), install.key)
if install.key is not None and model.may_be_the_global_object(install.target):
record(frozenset({install.key}), None)
continue
for member in _written_members(_write_target(node)):
for base in _member_chain_roots(member):
record(aliases.names_denoted_by(base), key_text(member))
written_global = model.may_name_a_global(member)
if written_global is not None:
record(frozenset({written_global}), None)
return IntrinsicWrites(
frozenset(names),
{name: None if written is None else frozenset(written) for name, written in keys.items()},
aliases,
followable,
)
def _records_a_watched_read(
model: SemanticModel,
aliases: _IntrinsicAliases,
node: Node,
names: frozenset[str],
) -> bool:
"""
Whether what *node* evaluates to, which may be one of the watched *names*, is written through
(`_written_through`) or escapes (`_value_escapes`).
"""
return _written_through(node) or _value_escapes(model, aliases, node, names)
def _written_members(target: Node | None) -> Iterator[JsMemberExpression]:
"""
Every member access a write to *target* may store through. A plain member target is that one
access; a pattern holds one per position it assigns into, which `[Object.prototype.z] = [9]` and
`({k: Object.prototype.z} = o)` both write a property through while naming no member target at
the top. A pattern is read by walking it, so a member standing in a computed key inside one is
yielded too — which over-reports a write and is the direction this whole scan fails in.
"""
cursor = strip_parens(target)
if isinstance(cursor, JsMemberExpression):
yield cursor
elif cursor is not None and not isinstance(cursor, JsIdentifier):
for node in cursor.walk():
if isinstance(node, JsMemberExpression):
yield node
def _value_escapes(
model: SemanticModel,
aliases: _IntrinsicAliases,
node: Node,
names: frozenset[str],
depth: int = 0,
) -> bool:
"""
Whether the value read at *node* — which may denote the intrinsics *names* — reaches code that
could write a property on it without this analysis seeing the write. A property read names its
value at the member access, so a caller asks about `g.String` there, not at the name `String`.
The question is deliberately inverted. Tracking where an intrinsic *flows to* would need a
binder from each argument to its parameter, and that binder reaches none of the routes an
obfuscator actually uses: a callback that receives the value, a function that returns it,
`arguments`, spread, rest, or a method on an object literal. Asking instead whether the value
leaves a position whose effect is *known* covers all of them at once, and fails in the safe
direction by construction — an unrecognized position counts as an escape, which costs an
unfolded call rather than a wrong value.
The positions that hand nothing over, and so must stay free or every fold collapses:
- a member base, when the key cannot yield the intrinsic's own mutable surface
(`Math.floor(1.7)`)
- a callee of a call or of `new`, which consumes it (`parseInt(x)`, `new RegExp(p)`)
- an operator operand, which reads a value without capturing the object (`typeof Math`), except
the left of `instanceof`, which is handed to the right operand's `Symbol.hasInstance` method
- a hole of an untagged template; a tagged template hands its holes to the tag as arguments
- the test of a branch or a loop, which converts it to a boolean and calls nothing on an object
(`if (String.prototype)`)
- a rebinding whose target the alias analysis still resolves to the same names (`var m = Math`)
- a return of a function whose every call the text shows, where the value at none of those
calls escapes and none is written through (`_returned_value_escapes`)
That last arm is what makes one predicate serve both this scan and the parameter-escape question
inside `_callee_is_write_free`, rather than two walks differing by a flag. A rebinding is safe
precisely when a later write through the new name is still attributed back, which is a question
`_IntrinsicAliases` already answers: `var m = Math` is spared because `m` denotes `Math`, while
`save = o` inside a callee is not, because a parameter denotes nothing and the attribution chain
dies there. The *names* being non-empty is therefore load-bearing — an empty set is a subset of
everything, and would spare the very case the arm exists to catch.
"""
value = _forwarded_value(node)
if value is None:
return False
target = _rebinding_target(value)
if target is not None:
return not (names and names <= _names_bound_to(model, aliases, target))
parent = value.parent
if (
isinstance(parent, JsReturnStatement)
or isinstance(parent, JsArrowFunctionExpression) and parent.body is value
):
return _returned_value_escapes(model, aliases, value, names, depth)
return _hands_over(model, aliases, value, depth)
def _rebinding_target(value: Node) -> Node | None:
"""
The target *value* is stored into, where it is the initializer of a declarator or the right
side of an assignment, or `None` where it stands anywhere else.
"""
parent = value.parent
if isinstance(parent, JsVariableDeclarator) and parent.init is value:
return parent.id
if isinstance(parent, JsAssignmentExpression) and parent.right is value:
return parent.left
return None
def _hands_over(model: SemanticModel, aliases: _IntrinsicAliases, value: Node, depth: int) -> bool:
"""
Whether the position *value* stands in hands its value to code that could write a property on
it unseen: every position but those `_value_escapes` lists as handing nothing over, where a
rebinding (`_rebinding_target`) and a return count as handing it over. Whether either of those
spares the value depends on the names it may denote, which only `_value_escapes` knows.
"""
parent = value.parent
if isinstance(parent, _TESTED_FORMS) and parent.test is value:
return False
if isinstance(parent, JsNewExpression) and parent.callee is value:
return False
if isinstance(parent, JsCallExpression):
if parent.callee is value:
return False
callee = unambiguous_callee(model, parent)
return not _callee_is_write_free(model, aliases, callee, depth + 1)
if isinstance(parent, JsBinaryExpression) and parent.operator == 'instanceof':
return parent.left is value
if isinstance(parent, JsTemplateLiteral):
return isinstance(parent.parent, JsTaggedTemplateExpression)
if isinstance(parent, (JsUnaryExpression, JsBinaryExpression)):
return False
return True
def _returned_value_escapes(
model: SemanticModel,
aliases: _IntrinsicAliases,
value: Node,
names: frozenset[str],
depth: int,
) -> bool:
"""
Whether *value*, returned by the function enclosing it, escapes where that function returns it:
every call and construction of the function is shown (`SemanticModel.shown_calls`), the value
escapes at none of them, and none is the foot of a member chain written through or installed on
(`_written_through`), a write the scan attributes to the name at the foot of a chain and so
never to a call. A generator or an async function hands its value to an object, which escapes.
"""
function = enclosing_function(value)
if not isinstance(function, FUNCTION_NODES) or depth > _CALLEE_DEPTH_LIMIT or function.is_async:
return True
if isinstance(function, (JsFunctionDeclaration, JsFunctionExpression)) and function.generator:
return True
calls = model.shown_calls(function)
if calls is None:
return True
return any(
_written_through(call) or _value_escapes(model, aliases, call, names, depth + 1)
for call in calls
)
def _written_through(value: Node) -> bool:
"""
Whether a member chain whose foot is *value*, looking through the forms that forward it, is
written or deleted somewhere along it, or invokes on what it holds an accessor install or a
method that writes its receiver (`_invokes_a_write`).
"""
cursor = value
while (parent := cursor.parent) is not None:
if isinstance(parent, JsMemberExpression):
if parent.object is not cursor:
return False
if is_member_write_target(parent) or _invokes_a_write(parent):
return True
elif not isinstance(parent, _VALUE_FORWARDING_NODES):
return False
cursor = parent
return False
def _invokes_a_write(member: JsMemberExpression) -> bool:
"""
Whether *member* is invoked under a key that may be an accessor installer
(`refinery.lib.scripts.js.model.ACCESSOR_INSTALL_METHODS`) or a method that writes the object
it is read off (`refinery.lib.scripts.js.builtins.RECEIVER_WRITING_METHODS`):
`globalThis.Array.prototype.push(5)` stores an element every array then inherits.
"""
return is_invocation_target(member) and key_may_be(member, _WRITING_METHODS)
def _names_a_member_property(node: JsIdentifier) -> bool:
"""
Whether *node* is the name a dotted member access reads, whose value is the access itself.
"""
parent = node.parent
if not isinstance(parent, JsMemberExpression):
return False
return parent.property is node and not parent.computed
def _write_target(node: Node) -> Node | None:
"""
The target the write at *node* stores through: the left of an assignment, the operand of an
update or a `delete`, or the head of a `for`-`in` or `for`-`of`; `None` where *node* writes
nothing.
"""
if isinstance(node, JsAssignmentExpression):
return node.left
if isinstance(node, JsUpdateExpression):
return node.argument
if isinstance(node, JsUnaryExpression) and node.operator == 'delete':
return node.operand
if isinstance(node, (JsForInStatement, JsForOfStatement)):
return node.left
return None
def _forwarded_value(node: Node) -> Node | None:
"""
The outermost expression still carrying *node*'s value, or `None` when no enclosing form can
hand that value on. The walk looks through the forms that forward a value unchanged —
parentheses, the operands of `||`/`&&`/`??`, the branches of a conditional, the last expression
of a sequence, and a spread — and through a member access only when its key reaches the
intrinsic's own surface, since `p(Array.prototype)` hands over an object whose properties decide
what `[1, 2].join()` means while `p(Math.PI)` hands over a number nobody consults.
Strictly outward through `parent`, so unlike `_denoted_roots` — which recurses *into* an
expression and needs `_DENOTED_ROOT_DEPTH_LIMIT` — this terminates on the finite path to the
root without a limit.
"""
cursor: Node = node
while True:
parent = cursor.parent
if parent is None:
return None
if isinstance(parent, JsMemberExpression) and parent.object is cursor:
if not _reaches_intrinsic_surface(parent):
return None
cursor = parent
continue
if isinstance(parent, JsConditionalExpression) and parent.test is cursor:
return cursor
if isinstance(parent, _VALUE_FORWARDING_NODES):
cursor = parent
continue
return cursor
def _reaches_intrinsic_surface(member: JsMemberExpression) -> bool:
"""
Whether reading *member*'s key off an intrinsic can yield an object sharing that intrinsic's
mutable surface. A key not known may be any of them, so it counts: a missed reach is a name
that keeps its trust while the program patches it.
"""
return key_may_be(member, _SURFACE_KEYS)
def _names_bound_to(
model: SemanticModel, aliases: _IntrinsicAliases, target: Node | None
) -> frozenset[str]:
"""
The intrinsic names a value stored into *target* may still be found under, so a rebinding that
keeps the value reachable by name is not an escape. A destructuring or member target yields
nothing, since neither leaves a name this analysis resolves writes through.
Both binding lookups are needed. A declarator id is not a *reference*, so `resolve` finds
nothing for it and only `refinery.lib.scripts.js.analysis.model.SemanticModel.binding_of`
answers; an assignment target is a reference and only `resolve` does.
"""
if not isinstance(target, JsIdentifier):
return frozenset()
binding = model.binding_of(target) or model.resolve(target)
if binding is None:
return frozenset({target.name})
return aliases.names_of(binding)
def _callee_is_write_free(
model: SemanticModel,
aliases: _IntrinsicAliases,
func: JsFunctionDeclaration | JsFunctionExpression | JsArrowFunctionExpression | None,
depth: int,
) -> bool:
"""
Whether a call to *func* provably writes no property reachable from one of its parameters, and
lets no parameter escape further. This is what keeps the escape rule from refusing every call:
`log(Math)` for a `log` that only reads `o.PI` hands the intrinsic to code whose writes are
fully enumerable, so `Math` keeps its trust.
Fails closed on every way the enumeration could be incomplete — an unresolvable callee, a rest
or destructured parameter whose contents no name tracks, a reachable `arguments` object, or
recursion past `_CALLEE_DEPTH_LIMIT`. Each of those means a parameter's value could be written
through somewhere this scan does not look. The writes are read off the targets as
`build_intrinsic_writes` reads them (`_written_members`): a pattern, `[p.now] = [f]`, and the
head of a `for`-`in` or `for`-`of` store through a member as a plain assignment does.
The parameter-escape question routes back through `_value_escapes` rather than scanning for
identifiers, so a callee that merely *reads* through its parameter (`return o.PI`) stays
write-free while one that passes it on (`g(o)`) does not. That reuse also answers the return
case: a parameter returned goes wherever each call of the callee puts its value, and one a call
may receive another way escapes, as do `return [o]` and `return { m: o }`.
The answer depends on *func* and *depth* alone, so one scan decides it once for each pair
(`_IntrinsicAliases.write_free`): a function that returns its parameter is asked about at each
of its calls, and deciding it walks every one of them (`_returned_value_escapes`).
"""
if func is None or depth > _CALLEE_DEPTH_LIMIT:
return False
key = func, depth
known = aliases.write_free.get(key)
if known is None:
known = aliases.write_free[key] = _parameters_stay_unwritten(model, aliases, func, depth)
return known
def _parameters_stay_unwritten(
model: SemanticModel,
aliases: _IntrinsicAliases,
func: JsFunctionDeclaration | JsFunctionExpression | JsArrowFunctionExpression,
depth: int,
) -> bool:
"""
The answer `_callee_is_write_free` gives for *func* at *depth*, decided anew.
"""
scope = model.parameter_scope(func)
if scope is None:
return False
if not isinstance(func, JsArrowFunctionExpression):
binding = scope.bindings.get('arguments')
if binding is not None and (model.references(binding) or model.reflection_can_reach(binding)):
return False
names = [param.name for param in func.params if isinstance(param, JsIdentifier)]
if len(names) != len(func.params):
return False
params = frozenset(names)
if not params:
return True
body = getattr(func, 'body', None)
if body is None:
return False
for node in body.walk():
if any(
base.name in params
for member in _written_members(_write_target(node))
for base in _member_chain_roots(member)
):
return False
if isinstance(node, JsIdentifier) and node.name in params:
if not _names_a_member_property(node) and reference_role(node) is Role.READ:
names = aliases.names_denoted_by(node)
if _value_escapes(model, aliases, node, names, depth):
return False
return True
def unambiguous_callee(
model: SemanticModel, call: JsCallExpression
) -> JsFunctionDeclaration | JsFunctionExpression | JsArrowFunctionExpression | None:
"""
The function *call* certainly invokes for a consumer that cannot order the call against a
reassignment of the callee's name, or `None`. The module-scope form of
`refinery.lib.scripts.js.analysis.effects.EffectModel.unambiguous_callee`, which delegates here:
the answer needs only the model, so `build_intrinsic_writes`, which runs before any effect model
exists, can still ask it.
"""
callee = call.callee
if isinstance(callee, (JsFunctionExpression, JsArrowFunctionExpression)):
return callee
if not isinstance(callee, JsIdentifier):
return None
return unambiguous_function(model, model.resolve(callee))
def unambiguous_function(
model: SemanticModel, binding: Binding | None
) -> JsFunctionDeclaration | JsFunctionExpression | JsArrowFunctionExpression | None:
"""
The module-scope form of
`refinery.lib.scripts.js.analysis.effects.EffectModel.unambiguous_function`, which delegates
here. See that method for which bindings qualify.
"""
value = model.singular_value(binding)
return value if isinstance(value, FUNCTION_NODES) else None
class _IntrinsicAliases:
"""
Resolves the identifier at the root of a write target to every name it may denote, so that a
property write reaching an intrinsic through a local is attributed to the intrinsic rather than
to the local. `var m = Math; m.floor = f` must record `Math`, or the write stays invisible to
every consumer of `IntrinsicWrites.names` and `Math.floor(1.7)` goes on folding to the built-in.
A *may* analysis on purpose: every value a binding can hold contributes, so one branch assigning
an intrinsic is enough to poison the name. The direction is forced — missing an alias yields a
wrong value, while an extra name yields an unfolded call — and it matches how the
accessor-install targets are already over-approximated, `Object.defineProperty(0 || Math, …)`
among them.
The backward step is `_denoted_roots`, the same value-preserving walk the install-target scan
uses, reused rather than reimplemented for two reasons. It already looks through exactly the
forms a fold collapses (`||`, `&&`, `??`, conditional, sequence-last, assignment-RHS, parens),
which is what keeps this answer stable while passes run — the pinning contract in
`refinery.lib.scripts.js.analysis.cache.ModelCache` requires that this set never *grow* across a
pass. And it stops at a call, so `var s = String.fromCharCode(x)` does not alias `String`: the
local holds the *result*. Its member-chain arm over-approximates in the one remaining direction
— `var n = Array.length` reports `Array` — which is sound and costs nothing.
"""
def __init__(self, model: SemanticModel):
self.model = model
self._answers: dict[Binding, frozenset[str]] = {}
self._sources: dict[Binding, tuple[frozenset[str], tuple[Binding, ...]]] = {}
self.write_free: dict[tuple[Node, int], bool] = {}
"""
What `_callee_is_write_free` decided of each callee at each depth, during the scan that
built this relation and threads it through every question it asks.
"""
def names_denoted_by(self, node: JsIdentifier) -> frozenset[str]:
"""
Every intrinsic-root name *node* may denote, including its own when it names one directly. A
name that resolves to no binding is a free global and denotes itself.
"""
if node.name in PURE_INTRINSIC_ROOTS:
return frozenset({node.name})
binding = self.model.resolve(node)
if binding is None:
return frozenset({node.name})
return self.names_of(binding)
def names_of(self, binding: Binding) -> frozenset[str]:
"""
Every name a value of *binding* may denote. The binding-level entry point, for a caller
holding a binding rather than a reference to it — resolving a *write* target, whose
declaration id is not a reference at all.
A name holding another name denotes everything that one does, through chains of any length
and through cycles (`a = b; b = a`). The bindings not yet answered that *binding* reaches
are split into strongly connected components: the members of one reach the same bindings and
share one answer, and each component comes after every component it reaches, whose answers
it takes over. A binding that holds no name still unanswered is a component of its own and
is answered without the search, which is how most bindings are asked about.
"""
answer = self._answers.get(binding)
if answer is not None:
return answer
if not self._unanswered_sources(binding):
self._answer([binding])
else:
for component in strongly_connected_components((binding,), self._unanswered_sources):
self._answer(component)
return self._answers[binding]
def _answer(self, component: list[Binding]):
"""
Give every member of *component* the one answer they share: the names each of them denotes
directly, and the answer of every binding outside the component that one of them holds.
"""
members = set(component)
found: set[str] = set()
for member in component:
names, sources = self._sources_of(member)
found.update(names)
for source in sources:
if source not in members:
found.update(self._answers[source])
answer = frozenset(found)
for member in component:
self._answers[member] = answer
def _unanswered_sources(self, binding: Binding) -> list[Binding]:
return [source for source in self._sources_of(binding)[1] if source not in self._answers]
def _sources_of(self, binding: Binding) -> tuple[frozenset[str], tuple[Binding, ...]]:
"""
The names a value of *binding* denotes directly, and the bindings of the names it holds,
memoized per binding. A root spelled as a pure intrinsic denotes it even where a local of
that name exists, and a root that resolves to no binding is a free global denoting itself.
"""
known = self._sources.get(binding)
if known is None:
names: set[str] = set()
sources: list[Binding] = []
for root in _binding_value_roots(binding):
if root.name in PURE_INTRINSIC_ROOTS:
names.add(root.name)
continue
source = self.model.resolve(root)
if source is None:
names.add(root.name)
else:
sources.append(source)
known = self._sources[binding] = frozenset(names), tuple(sources)
return known
def _binding_value_roots(binding: Binding) -> Iterator[JsIdentifier]:
"""
Every name a value of *binding* may denote, over its declarations' initializers and the right
side of every plain assignment to it. Both are needed: a binding declared empty and assigned
later (`var m; m = Math`) holds the intrinsic just as one initialized with it does.
A write the model cannot pin to a value
(`refinery.lib.scripts.js.analysis.model.Binding.indefinite_writes`) contributes as well. This
is a *may* analysis, and `arguments[k] = Math` stores the intrinsic under a parameter's name
whether or not the text says which parameter or whether the call supplied it; leaving it out is
exactly the missed alias the caller's docstring names as a wrong answer.
The may-side sibling of `refinery.lib.scripts.js.analysis.model.SemanticModel.binding_values`,
deliberately wider than its readable channels: a compound assignment's right side contributes
here where the must-query refuses the whole binding, because `_value_escapes`'s rebinding arm
spares a target only while the names it may denote are still attributed back, and narrowing this
walk would turn those rebindings into escapes. A channel neither walk reads — an intrinsic
stored through a pattern, a parameter fed at a call site — is covered at its source instead: the
intrinsic *read* that fed it escapes, which withdraws the key trust there.
"""
for declaration in binding.declarations:
parent = getattr(declaration, 'parent', None)
initializer = getattr(parent, 'init', None)
if initializer is not None:
yield from _denoted_roots(initializer)
for reference in (*binding.writes, *binding.indefinite_writes):
parent = getattr(reference, 'parent', None)
if isinstance(parent, JsAssignmentExpression) and parent.left is reference:
yield from _denoted_roots(parent.right)
def denoting_leaves(node: Node | None, depth: int = 0) -> Iterator[Node]:
"""
Every node whose value *node* may evaluate to, looking through the value-preserving forms a
constant fold collapses: parentheses, the operands of `||`/`&&`/`??` and the branches of a
conditional (either side may be the value), the last expression of a sequence, and the right
side of an assignment. A member access yields the foot of its chain, `Object` for
`Object.prototype.x`, which is what a write through the access is attributed to.
Resolving only the syntactic form would make an answer read off these leaves change as folds
fire — `Math || 0` names nothing until it collapses to `Math` — which is precisely what a
consumer holding the answer across a pass cannot tolerate.
"""
if depth > _DENOTED_ROOT_DEPTH_LIMIT:
return
cursor = strip_parens(node)
if cursor is None:
return
if isinstance(cursor, JsMemberExpression):
foot = _member_chain_foot(cursor)
if foot is not None:
yield foot
elif isinstance(cursor, JsLogicalExpression):
yield from denoting_leaves(cursor.left, depth + 1)
yield from denoting_leaves(cursor.right, depth + 1)
elif isinstance(cursor, JsConditionalExpression):
yield from denoting_leaves(cursor.consequent, depth + 1)
yield from denoting_leaves(cursor.alternate, depth + 1)
elif isinstance(cursor, JsSequenceExpression):
if cursor.expressions:
yield from denoting_leaves(cursor.expressions[-1], depth + 1)
elif isinstance(cursor, JsAssignmentExpression):
yield from denoting_leaves(cursor.right, depth + 1)
else:
yield cursor
def _denoted_roots(node: Node | None) -> Iterator[JsIdentifier]:
"""
Every name *node* may denote: the identifiers among its `denoting_leaves`.
"""
for leaf in denoting_leaves(node):
if isinstance(leaf, JsIdentifier):
yield leaf
def _member_chain_foot(node: JsMemberExpression) -> Node | None:
"""
The node at the foot of the member-access chain *node*, looking through parentheses.
"""
cursor: Node | None = node
while isinstance(cursor, JsMemberExpression):
cursor = strip_parens(cursor.object)
return cursor
def _is_member_object(node: Node) -> bool:
"""
Whether *node* is the object a member access reads its property off, looking through
parentheses.
"""
cursor = node
while isinstance(parent := cursor.parent, JsParenthesizedExpression):
cursor = parent
return isinstance(parent, JsMemberExpression) and parent.object is cursor
def _member_chain_roots(node: Node | None) -> Iterator[JsIdentifier]:
"""
The names the foot of a member-access chain may evaluate to (`_denoted_roots`), none where
*node* is no member access. `Math.prototype.x` roots at `Math`, so a write anywhere along the
chain is attributed to the name that owns it; testing only the immediate `.object` would miss
every nested write. A foot written `(0, Math)`, `(Math || m)` or `c ? Math : Date` roots at
every name it may be, as an escape through those forms is attributed
(`_VALUE_FORWARDING_NODES`).
"""
cursor = strip_parens(node)
if isinstance(cursor, JsMemberExpression):
yield from _denoted_roots(_member_chain_foot(cursor))
Global variables
var SPECIES_KEYS-
The two property names that decide what an allocating
Array.prototypemethod returns. ArraySpeciesCreate readsconstructor[Symbol.species]off the receiver, and__proto__replaces the prototype thatconstructorlookup walks, so writing either can make the "newly created" array be a shared object — or make the call throw, by leaving a primitive where a constructor is expected. var PROTOTYPE_OWNERS-
The intrinsic whose prototype supplies the methods of each interpreter value type, keyed by type name so this module needs no import from the interpreter that depends on it. A method call on a literal receiver names no global at the call site, so trusting it means asking whether this prototype is intact:
String.prototype.toUpperCase = fis a write toString, and it changes what'ab'.toUpperCase()means even though the expression mentions no identifier at all.Every type in the interpreter's value domain is named here directly, including
JsBuffer, whose methods come fromBuffer.prototyperather than theArray.prototypeitslistbase would suggest. A function value is represented as its own AST node, so the two function node names appear here as value types rather than as syntax; both inherit fromFunction.prototype. Lookup is exact rather than a walk up the Python MRO, which would silently answerArrayfor any futurelistsubclass instead of refusing. A type with no entry is never trusted through this route. var INHERITED_CHAIN_ROOTS-
The prototypes every value inherits from beyond the one that owns its own methods.
Object.prototyperoots every chain, so a getter installed there is reached by a plain read on an array literal and onMathalike — which is why reading a property is a strictly stronger requirement than calling a method. var KEYED_WRITE_ROOTS-
The global names whose written property keys
IntrinsicWrites.key_written()bounds. A name outside this set is one the scan records no key for, so answeringFalseabout it would read an absence of evidence as evidence of absence; that predicate answersTruefor every key of such a name instead.The set is the union of the names this module already watches for a reason: the intrinsics whose methods are trusted, the roots a plain property read walks through, and the owners of the prototypes that supply each value type's members. A caller asking about a name outside it is asking a question this scan was not built to answer, and is told so.
var ESCAPE_RECORDED_ROOTS-
The global names an escape records as written by name, not only by key: the names a fold trusts to run code. A fold runs the methods of the pure roots, a fold that decides what a regular expression answers trusts
RegExp, sop(RegExp)for apthat replacesRegExp.prototype.testmust withdraw that trust, a fold that decides a comparison of the clock trustsDate, whichReflect.set(Date, 'now', f)must withdraw, and a fold that converts an array iterator trusts the prototypeIteratorholds, which every such iterator inherits.ObjectandFunctionare left out on purpose. Real samples handObjectto a call this scan cannot resolve, and recording that escape refuses every fold that trustsObject.prototypein them; the precise answer follows the escaped value to its writes, which this scan does not do. var ESCAPE_WATCHED_ROOTS-
The global names whose escape the scan looks for: every name it records keys for, and every name
ESCAPE_RECORDED_ROOTSrecords by name.
Functions
def build_intrinsic_writes(model)-
The set of global names the program does anything with beyond reading them, and the property keys it writes on each of the names whose keys are watched.
A name belongs to the first for binding it in any scope, assigning to it, writing or updating or deleting a property anywhere along a chain rooted at it (
Object.prototype.x = 1,Math.PI++), installing a descriptor on it withObject.defineProperty, or handing it to code whose writes this analysis cannot enumerate.This is the per-name counterpart of
refinery.lib.scripts.js.analysis.effects._intrinsics_pristine, which answers the same question for a fixed root set but collapses it to one program-wide flag. Keeping the answer per name is what lets a program that patchesObject.prototypestill have itsMath.floorcalls folded; the flag cannot express that, because one disturbed root disables every other one.Bindings cover every assignment form too, so no separate scan of write-role identifiers is needed: a bare
Math = 1introduces an implicit-global binding forMath, as do the destructuring andfor-target forms. Only property writes, deletes, and descriptor installs — which leave the name itself a plain read — need the explicit branches below.A write target names the intrinsic it patches only when the program spells it out.
var m = Math; m.floor = fpatchesMathwhile mentioning it nowhere in the assignment, so the chain root is resolved through the values its binding may hold rather than taken as the name it is spelled with.A write need not be anywhere in the program for a name to belong here. Handing an intrinsic to code whose writes this analysis cannot enumerate —
patch(Math)for apatchit cannot resolve — leaves the name looking untouched while its properties are replaced, so an intrinsic that escapes is recorded as written._value_escapesdecides which uses hand the value over.The keyed answer is the same scan read for the property a write names rather than only for the name it is rooted at, and it exists because the per-name question is too coarse for a caller that cares which property was replaced — a file patching
Object.prototype.zhas writtenObject, and refusing everything aboutObjecton that basis refuses the very files the question is asked about.Only the final key of a chain is recorded, which is the one a write replaces:
Object.prototype.z = 9replaceszand leavesprototypeandconstructoralone, so recording the keys it passes through would report a file as having patched the mechanism a caller is asking about when it did nothing of the kind. Every other route by which a name's properties can change — a binding that shadows it, a value that escapes, a computed key, a descriptor read from a value this analysis cannot read — bounds no key at all and is recorded as unbounded.Expand source code Browse git
def build_intrinsic_writes(model: SemanticModel) -> IntrinsicWrites: """ The set of global names the program does anything with beyond reading them, and the property keys it writes on each of the names whose keys are watched. A name belongs to the first for binding it in any scope, assigning to it, writing or updating or deleting a property anywhere along a chain rooted at it (`Object.prototype.x = 1`, `Math.PI++`), installing a descriptor on it with `Object.defineProperty`, or handing it to code whose writes this analysis cannot enumerate. This is the per-name counterpart of `refinery.lib.scripts.js.analysis.effects._intrinsics_pristine`, which answers the same question for a fixed root set but collapses it to one program-wide flag. Keeping the answer per name is what lets a program that patches `Object.prototype` still have its `Math.floor` calls folded; the flag cannot express that, because one disturbed root disables every other one. Bindings cover every *assignment* form too, so no separate scan of write-role identifiers is needed: a bare `Math = 1` introduces an implicit-global binding for `Math`, as do the destructuring and `for`-target forms. Only property writes, deletes, and descriptor installs — which leave the name itself a plain read — need the explicit branches below. A write target names the intrinsic it patches only when the program spells it out. `var m = Math; m.floor = f` patches `Math` while mentioning it nowhere in the assignment, so the chain root is resolved through the values its binding may hold rather than taken as the name it is spelled with. A write need not be *anywhere* in the program for a name to belong here. Handing an intrinsic to code whose writes this analysis cannot enumerate — `patch(Math)` for a `patch` it cannot resolve — leaves the name looking untouched while its properties are replaced, so an intrinsic that escapes is recorded as written. `_value_escapes` decides which uses hand the value over. The keyed answer is the same scan read for the property a write names rather than only for the name it is rooted at, and it exists because the per-name question is too coarse for a caller that cares which property was replaced — a file patching `Object.prototype.z` has written `Object`, and refusing everything about `Object` on that basis refuses the very files the question is asked about. Only the *final* key of a chain is recorded, which is the one a write replaces: `Object.prototype.z = 9` replaces `z` and leaves `prototype` and `constructor` alone, so recording the keys it passes through would report a file as having patched the mechanism a caller is asking about when it did nothing of the kind. Every other route by which a name's properties can change — a binding that shadows it, a value that escapes, a computed key, a descriptor read from a value this analysis cannot read — bounds no key at all and is recorded as unbounded. """ names: set[str] = set() keys: dict[str, set[str] | None] = {} def record_keys(found: frozenset[str], key: str | None) -> None: for name in found & KEYED_WRITE_ROOTS: if key is None: keys[name] = None continue known = keys.setdefault(name, set()) if known is not None: known.add(key) def record(found: frozenset[str], key: str | None) -> None: names.update(found) record_keys(found, key) pending = [model.root_scope] while pending: scope = pending.pop() record(frozenset(scope.bindings), None) pending.extend(scope.children) aliases = _IntrinsicAliases(model) reach = _BuiltinReach(model, aliases) followable = True def record_escape(watched: frozenset[str]) -> None: nonlocal followable record_keys(watched, None) names.update(watched & ESCAPE_RECORDED_ROOTS) if watched & _KEY_RECORDED_ROOTS: followable = False for node in model.root.walk(): if followable and (reach.trips_at(node) or _constructs_through_an_unread_key(model, node)): followable = False if isinstance(node, JsIdentifier): if not _names_a_member_property(node) and reference_role(node) is Role.READ: watched = aliases.names_denoted_by(node) & ESCAPE_WATCHED_ROOTS if watched and _value_escapes(model, aliases, node, watched): record_escape(watched) continue if isinstance(node, JsMemberExpression): if not key_may_be(node, ESCAPE_WATCHED_ROOTS): continue key = key_text(node) watched = ESCAPE_WATCHED_ROOTS if key is None else frozenset({key}) if ( _records_a_watched_read(model, aliases, node, watched) and key_is(node, ESCAPE_WATCHED_ROOTS, model.assumed_keys) ): record_escape(watched) continue if isinstance(node, JsCallExpression): for install in accessor_installs(node): for base in _denoted_roots(install.target): record(aliases.names_denoted_by(base), install.key) if install.key is not None and model.may_be_the_global_object(install.target): record(frozenset({install.key}), None) continue for member in _written_members(_write_target(node)): for base in _member_chain_roots(member): record(aliases.names_denoted_by(base), key_text(member)) written_global = model.may_name_a_global(member) if written_global is not None: record(frozenset({written_global}), None) return IntrinsicWrites( frozenset(names), {name: None if written is None else frozenset(written) for name, written in keys.items()}, aliases, followable, ) def unambiguous_callee(model, call)-
The function call certainly invokes for a consumer that cannot order the call against a reassignment of the callee's name, or
None. The module-scope form ofEffectModel.unambiguous_callee(), which delegates here: the answer needs only the model, sobuild_intrinsic_writes(), which runs before any effect model exists, can still ask it.Expand source code Browse git
def unambiguous_callee( model: SemanticModel, call: JsCallExpression ) -> JsFunctionDeclaration | JsFunctionExpression | JsArrowFunctionExpression | None: """ The function *call* certainly invokes for a consumer that cannot order the call against a reassignment of the callee's name, or `None`. The module-scope form of `refinery.lib.scripts.js.analysis.effects.EffectModel.unambiguous_callee`, which delegates here: the answer needs only the model, so `build_intrinsic_writes`, which runs before any effect model exists, can still ask it. """ callee = call.callee if isinstance(callee, (JsFunctionExpression, JsArrowFunctionExpression)): return callee if not isinstance(callee, JsIdentifier): return None return unambiguous_function(model, model.resolve(callee)) def unambiguous_function(model, binding)-
The module-scope form of
EffectModel.unambiguous_function(), which delegates here. See that method for which bindings qualify.Expand source code Browse git
def unambiguous_function( model: SemanticModel, binding: Binding | None ) -> JsFunctionDeclaration | JsFunctionExpression | JsArrowFunctionExpression | None: """ The module-scope form of `refinery.lib.scripts.js.analysis.effects.EffectModel.unambiguous_function`, which delegates here. See that method for which bindings qualify. """ value = model.singular_value(binding) return value if isinstance(value, FUNCTION_NODES) else None def denoting_leaves(node, depth=0)-
Every node whose value node may evaluate to, looking through the value-preserving forms a constant fold collapses: parentheses, the operands of
||/&&/??and the branches of a conditional (either side may be the value), the last expression of a sequence, and the right side of an assignment. A member access yields the foot of its chain,ObjectforObject.prototype.x, which is what a write through the access is attributed to.Resolving only the syntactic form would make an answer read off these leaves change as folds fire —
Math || 0names nothing until it collapses toMath— which is precisely what a consumer holding the answer across a pass cannot tolerate.Expand source code Browse git
def denoting_leaves(node: Node | None, depth: int = 0) -> Iterator[Node]: """ Every node whose value *node* may evaluate to, looking through the value-preserving forms a constant fold collapses: parentheses, the operands of `||`/`&&`/`??` and the branches of a conditional (either side may be the value), the last expression of a sequence, and the right side of an assignment. A member access yields the foot of its chain, `Object` for `Object.prototype.x`, which is what a write through the access is attributed to. Resolving only the syntactic form would make an answer read off these leaves change as folds fire — `Math || 0` names nothing until it collapses to `Math` — which is precisely what a consumer holding the answer across a pass cannot tolerate. """ if depth > _DENOTED_ROOT_DEPTH_LIMIT: return cursor = strip_parens(node) if cursor is None: return if isinstance(cursor, JsMemberExpression): foot = _member_chain_foot(cursor) if foot is not None: yield foot elif isinstance(cursor, JsLogicalExpression): yield from denoting_leaves(cursor.left, depth + 1) yield from denoting_leaves(cursor.right, depth + 1) elif isinstance(cursor, JsConditionalExpression): yield from denoting_leaves(cursor.consequent, depth + 1) yield from denoting_leaves(cursor.alternate, depth + 1) elif isinstance(cursor, JsSequenceExpression): if cursor.expressions: yield from denoting_leaves(cursor.expressions[-1], depth + 1) elif isinstance(cursor, JsAssignmentExpression): yield from denoting_leaves(cursor.right, depth + 1) else: yield cursor
Classes
class IntrinsicWrites (names, keys, aliases, followable)-
What one scan of a program says about the globals it writes: names, the set a caller asks about a whole name with, and keys, the properties each of the watched names was written at — or
Nonefor a name whose written keys this scan cannot bound. aliases is the relation the scan resolved write targets through, kept forfollows_only_through_bindings. followable says whether the scan follows every write the program makes on a built-in object: none reaches one through a value read without spelling its owner (_BuiltinReach), no watched name is handed off, nor read as a property and handed off or written through, where the scan records the escape by key alone (_KEY_RECORDED_ROOTS), and, under the model that suspects code it cannot read, no call runs through a key the reflection check leaves uncounted (_constructs_through_an_unread_key). A fold that reads a built-in's chain or converts an object without spelling its owner asks it on top of the questions about names.The two are produced together because they are read off the same nodes and must not disagree: a name recorded in one for a reason the other cannot express is a name one caller refuses and the other clears. Where a write cannot be pinned to a key, keys records the name unbounded rather than omitting it, so
keysnever reports less about a name thannamesdoes.Expand source code Browse git
class IntrinsicWrites(NamedTuple): """ What one scan of a program says about the globals it writes: *names*, the set a caller asks about a whole name with, and *keys*, the properties each of the watched names was written at — or `None` for a name whose written keys this scan cannot bound. *aliases* is the relation the scan resolved write targets through, kept for `follows_only_through_bindings`. *followable* says whether the scan follows every write the program makes on a built-in object: none reaches one through a value read without spelling its owner (`_BuiltinReach`), no watched name is handed off, nor read as a property and handed off or written through, where the scan records the escape by key alone (`_KEY_RECORDED_ROOTS`), and, under the model that suspects code it cannot read, no call runs through a key the reflection check leaves uncounted (`_constructs_through_an_unread_key`). A fold that reads a built-in's chain or converts an object without spelling its owner asks it on top of the questions about *names*. The two are produced together because they are read off the same nodes and must not disagree: a name recorded in one for a reason the other cannot express is a name one caller refuses and the other clears. Where a write cannot be pinned to a key, *keys* records the name unbounded rather than omitting it, so `keys` never reports less about a name than `names` does. """ names: frozenset[str] keys: dict[str, frozenset[str] | None] aliases: _IntrinsicAliases followable: bool def key_written(self, name: str, key: str) -> bool: """ Whether the program writes the property *key* on a chain rooted at the global *name*: `Object.prototype.constructor = C`, `delete Object.getPrototypeOf`, or a descriptor installed for that key. A name whose written keys cannot be bounded answers `True` for every key, and so does a name outside `KEYED_WRITE_ROOTS`, which the scan records nothing about at all. See `refinery.lib.scripts.js.analysis.effects.EffectModel.global_key_written` for why the question is asked of a name rather than of a receiver. """ if name not in KEYED_WRITE_ROOTS: return True keys = self.keys.get(name, frozenset()) return keys is None or key in keys def follows_only_through_bindings(self, value: Node) -> bool: """ Whether a write through what *value* evaluates to is one this scan or the semantic model it read records where the value is spelled or a binding holds it, and never through a property holding it. The scan follows a value through the bindings that hold it and trusts `Object`, `Function`, `Boolean` and `Buffer` when they escape (`ESCAPE_RECORDED_ROOTS`), and the semantic model finds a write to a global through a name holding the global object, not through a property holding it. Stored in a property, `scope.P = Object.prototype` lets `scope.P.p = 1` write `Object.prototype` unrecorded; stored in a binding, `var P = Object.prototype` makes `P.p = 1` a recorded write. A value may carry what any of its `denoting_leaves` evaluates to, and one of them qualifies where the scan's aliases resolve it to a root the scan watches, where it may be the global object, or where the model was not built over it and so cannot say. The foot of a member chain is no value the chain evaluates to: it qualifies through the root it names, as `Object` does for `Object.prototype`, and never as the global object, since what `window.document` evaluates to is a property of that object. """ model = self.aliases.model for leaf in denoting_leaves(value): if not model.knows(leaf): return True if isinstance(leaf, JsIdentifier) and self.aliases.names_denoted_by(leaf) & ESCAPE_WATCHED_ROOTS: return True if not _is_member_object(leaf) and model.may_be_the_global_object(leaf): return True return False def records_a_read(self, node: Node, names: frozenset[str]) -> bool: """ Whether the scan records the watched *names* read at *node*: what *node* evaluates to is written through or escapes (`_records_a_watched_read`), the test the scan applies to a property read naming one of them. """ return _records_a_watched_read(self.aliases.model, self.aliases, node, names) def denotes_a_watched_root(self, binding: Binding) -> bool: """ Whether a value *binding* may hold denotes a root the scan watches, so that a write through a name resolving to the binding is one the scan attributes to that root. """ return bool(self.aliases.names_of(binding) & ESCAPE_WATCHED_ROOTS) def roots_unwritten(self, owner: str, roots: frozenset[str]) -> bool: """ Whether the program writes neither *owner* nor any prototype in *roots*. A property read resolves against the whole prototype chain rather than one prototype, so each name the chain passes through has to answer the same question `refinery.lib.scripts.js.analysis.effects.EffectModel.trusted_prototype` asks of the owner alone. """ return all(name not in self.names for name in (owner, *roots)) def chain_roots_unwritten(self, value_type: type) -> bool: """ Whether the program writes no prototype a plain property read on a value of *value_type* consults, so such a read can run no accessor the program installed there. See `refinery.lib.scripts.js.analysis.effects.EffectModel.chain_roots_unwritten` for what the answer leaves out and who may ask it. """ owner = PROTOTYPE_OWNERS.get(value_type.__name__) if owner is None: return False return self.roots_unwritten(owner, INHERITED_CHAIN_ROOTS) def records_more_than(self, other: IntrinsicWrites) -> bool: """ Whether this scan records a written name, a key written on a name, or a name whose written keys it cannot bound, that the scan *other* does not record. """ if self.names - other.names: return True for name, keys in self.keys.items(): known = other.keys.get(name, frozenset()) if known is None: continue if keys is None or keys - known: return True return FalseAncestors
- builtins.tuple
Instance variables
var names-
Alias for field number 0
Expand source code Browse git
class IntrinsicWrites(NamedTuple): """ What one scan of a program says about the globals it writes: *names*, the set a caller asks about a whole name with, and *keys*, the properties each of the watched names was written at — or `None` for a name whose written keys this scan cannot bound. *aliases* is the relation the scan resolved write targets through, kept for `follows_only_through_bindings`. *followable* says whether the scan follows every write the program makes on a built-in object: none reaches one through a value read without spelling its owner (`_BuiltinReach`), no watched name is handed off, nor read as a property and handed off or written through, where the scan records the escape by key alone (`_KEY_RECORDED_ROOTS`), and, under the model that suspects code it cannot read, no call runs through a key the reflection check leaves uncounted (`_constructs_through_an_unread_key`). A fold that reads a built-in's chain or converts an object without spelling its owner asks it on top of the questions about *names*. The two are produced together because they are read off the same nodes and must not disagree: a name recorded in one for a reason the other cannot express is a name one caller refuses and the other clears. Where a write cannot be pinned to a key, *keys* records the name unbounded rather than omitting it, so `keys` never reports less about a name than `names` does. """ names: frozenset[str] keys: dict[str, frozenset[str] | None] aliases: _IntrinsicAliases followable: bool def key_written(self, name: str, key: str) -> bool: """ Whether the program writes the property *key* on a chain rooted at the global *name*: `Object.prototype.constructor = C`, `delete Object.getPrototypeOf`, or a descriptor installed for that key. A name whose written keys cannot be bounded answers `True` for every key, and so does a name outside `KEYED_WRITE_ROOTS`, which the scan records nothing about at all. See `refinery.lib.scripts.js.analysis.effects.EffectModel.global_key_written` for why the question is asked of a name rather than of a receiver. """ if name not in KEYED_WRITE_ROOTS: return True keys = self.keys.get(name, frozenset()) return keys is None or key in keys def follows_only_through_bindings(self, value: Node) -> bool: """ Whether a write through what *value* evaluates to is one this scan or the semantic model it read records where the value is spelled or a binding holds it, and never through a property holding it. The scan follows a value through the bindings that hold it and trusts `Object`, `Function`, `Boolean` and `Buffer` when they escape (`ESCAPE_RECORDED_ROOTS`), and the semantic model finds a write to a global through a name holding the global object, not through a property holding it. Stored in a property, `scope.P = Object.prototype` lets `scope.P.p = 1` write `Object.prototype` unrecorded; stored in a binding, `var P = Object.prototype` makes `P.p = 1` a recorded write. A value may carry what any of its `denoting_leaves` evaluates to, and one of them qualifies where the scan's aliases resolve it to a root the scan watches, where it may be the global object, or where the model was not built over it and so cannot say. The foot of a member chain is no value the chain evaluates to: it qualifies through the root it names, as `Object` does for `Object.prototype`, and never as the global object, since what `window.document` evaluates to is a property of that object. """ model = self.aliases.model for leaf in denoting_leaves(value): if not model.knows(leaf): return True if isinstance(leaf, JsIdentifier) and self.aliases.names_denoted_by(leaf) & ESCAPE_WATCHED_ROOTS: return True if not _is_member_object(leaf) and model.may_be_the_global_object(leaf): return True return False def records_a_read(self, node: Node, names: frozenset[str]) -> bool: """ Whether the scan records the watched *names* read at *node*: what *node* evaluates to is written through or escapes (`_records_a_watched_read`), the test the scan applies to a property read naming one of them. """ return _records_a_watched_read(self.aliases.model, self.aliases, node, names) def denotes_a_watched_root(self, binding: Binding) -> bool: """ Whether a value *binding* may hold denotes a root the scan watches, so that a write through a name resolving to the binding is one the scan attributes to that root. """ return bool(self.aliases.names_of(binding) & ESCAPE_WATCHED_ROOTS) def roots_unwritten(self, owner: str, roots: frozenset[str]) -> bool: """ Whether the program writes neither *owner* nor any prototype in *roots*. A property read resolves against the whole prototype chain rather than one prototype, so each name the chain passes through has to answer the same question `refinery.lib.scripts.js.analysis.effects.EffectModel.trusted_prototype` asks of the owner alone. """ return all(name not in self.names for name in (owner, *roots)) def chain_roots_unwritten(self, value_type: type) -> bool: """ Whether the program writes no prototype a plain property read on a value of *value_type* consults, so such a read can run no accessor the program installed there. See `refinery.lib.scripts.js.analysis.effects.EffectModel.chain_roots_unwritten` for what the answer leaves out and who may ask it. """ owner = PROTOTYPE_OWNERS.get(value_type.__name__) if owner is None: return False return self.roots_unwritten(owner, INHERITED_CHAIN_ROOTS) def records_more_than(self, other: IntrinsicWrites) -> bool: """ Whether this scan records a written name, a key written on a name, or a name whose written keys it cannot bound, that the scan *other* does not record. """ if self.names - other.names: return True for name, keys in self.keys.items(): known = other.keys.get(name, frozenset()) if known is None: continue if keys is None or keys - known: return True return False var keys-
Alias for field number 1
Expand source code Browse git
class IntrinsicWrites(NamedTuple): """ What one scan of a program says about the globals it writes: *names*, the set a caller asks about a whole name with, and *keys*, the properties each of the watched names was written at — or `None` for a name whose written keys this scan cannot bound. *aliases* is the relation the scan resolved write targets through, kept for `follows_only_through_bindings`. *followable* says whether the scan follows every write the program makes on a built-in object: none reaches one through a value read without spelling its owner (`_BuiltinReach`), no watched name is handed off, nor read as a property and handed off or written through, where the scan records the escape by key alone (`_KEY_RECORDED_ROOTS`), and, under the model that suspects code it cannot read, no call runs through a key the reflection check leaves uncounted (`_constructs_through_an_unread_key`). A fold that reads a built-in's chain or converts an object without spelling its owner asks it on top of the questions about *names*. The two are produced together because they are read off the same nodes and must not disagree: a name recorded in one for a reason the other cannot express is a name one caller refuses and the other clears. Where a write cannot be pinned to a key, *keys* records the name unbounded rather than omitting it, so `keys` never reports less about a name than `names` does. """ names: frozenset[str] keys: dict[str, frozenset[str] | None] aliases: _IntrinsicAliases followable: bool def key_written(self, name: str, key: str) -> bool: """ Whether the program writes the property *key* on a chain rooted at the global *name*: `Object.prototype.constructor = C`, `delete Object.getPrototypeOf`, or a descriptor installed for that key. A name whose written keys cannot be bounded answers `True` for every key, and so does a name outside `KEYED_WRITE_ROOTS`, which the scan records nothing about at all. See `refinery.lib.scripts.js.analysis.effects.EffectModel.global_key_written` for why the question is asked of a name rather than of a receiver. """ if name not in KEYED_WRITE_ROOTS: return True keys = self.keys.get(name, frozenset()) return keys is None or key in keys def follows_only_through_bindings(self, value: Node) -> bool: """ Whether a write through what *value* evaluates to is one this scan or the semantic model it read records where the value is spelled or a binding holds it, and never through a property holding it. The scan follows a value through the bindings that hold it and trusts `Object`, `Function`, `Boolean` and `Buffer` when they escape (`ESCAPE_RECORDED_ROOTS`), and the semantic model finds a write to a global through a name holding the global object, not through a property holding it. Stored in a property, `scope.P = Object.prototype` lets `scope.P.p = 1` write `Object.prototype` unrecorded; stored in a binding, `var P = Object.prototype` makes `P.p = 1` a recorded write. A value may carry what any of its `denoting_leaves` evaluates to, and one of them qualifies where the scan's aliases resolve it to a root the scan watches, where it may be the global object, or where the model was not built over it and so cannot say. The foot of a member chain is no value the chain evaluates to: it qualifies through the root it names, as `Object` does for `Object.prototype`, and never as the global object, since what `window.document` evaluates to is a property of that object. """ model = self.aliases.model for leaf in denoting_leaves(value): if not model.knows(leaf): return True if isinstance(leaf, JsIdentifier) and self.aliases.names_denoted_by(leaf) & ESCAPE_WATCHED_ROOTS: return True if not _is_member_object(leaf) and model.may_be_the_global_object(leaf): return True return False def records_a_read(self, node: Node, names: frozenset[str]) -> bool: """ Whether the scan records the watched *names* read at *node*: what *node* evaluates to is written through or escapes (`_records_a_watched_read`), the test the scan applies to a property read naming one of them. """ return _records_a_watched_read(self.aliases.model, self.aliases, node, names) def denotes_a_watched_root(self, binding: Binding) -> bool: """ Whether a value *binding* may hold denotes a root the scan watches, so that a write through a name resolving to the binding is one the scan attributes to that root. """ return bool(self.aliases.names_of(binding) & ESCAPE_WATCHED_ROOTS) def roots_unwritten(self, owner: str, roots: frozenset[str]) -> bool: """ Whether the program writes neither *owner* nor any prototype in *roots*. A property read resolves against the whole prototype chain rather than one prototype, so each name the chain passes through has to answer the same question `refinery.lib.scripts.js.analysis.effects.EffectModel.trusted_prototype` asks of the owner alone. """ return all(name not in self.names for name in (owner, *roots)) def chain_roots_unwritten(self, value_type: type) -> bool: """ Whether the program writes no prototype a plain property read on a value of *value_type* consults, so such a read can run no accessor the program installed there. See `refinery.lib.scripts.js.analysis.effects.EffectModel.chain_roots_unwritten` for what the answer leaves out and who may ask it. """ owner = PROTOTYPE_OWNERS.get(value_type.__name__) if owner is None: return False return self.roots_unwritten(owner, INHERITED_CHAIN_ROOTS) def records_more_than(self, other: IntrinsicWrites) -> bool: """ Whether this scan records a written name, a key written on a name, or a name whose written keys it cannot bound, that the scan *other* does not record. """ if self.names - other.names: return True for name, keys in self.keys.items(): known = other.keys.get(name, frozenset()) if known is None: continue if keys is None or keys - known: return True return False var aliases-
Alias for field number 2
Expand source code Browse git
class IntrinsicWrites(NamedTuple): """ What one scan of a program says about the globals it writes: *names*, the set a caller asks about a whole name with, and *keys*, the properties each of the watched names was written at — or `None` for a name whose written keys this scan cannot bound. *aliases* is the relation the scan resolved write targets through, kept for `follows_only_through_bindings`. *followable* says whether the scan follows every write the program makes on a built-in object: none reaches one through a value read without spelling its owner (`_BuiltinReach`), no watched name is handed off, nor read as a property and handed off or written through, where the scan records the escape by key alone (`_KEY_RECORDED_ROOTS`), and, under the model that suspects code it cannot read, no call runs through a key the reflection check leaves uncounted (`_constructs_through_an_unread_key`). A fold that reads a built-in's chain or converts an object without spelling its owner asks it on top of the questions about *names*. The two are produced together because they are read off the same nodes and must not disagree: a name recorded in one for a reason the other cannot express is a name one caller refuses and the other clears. Where a write cannot be pinned to a key, *keys* records the name unbounded rather than omitting it, so `keys` never reports less about a name than `names` does. """ names: frozenset[str] keys: dict[str, frozenset[str] | None] aliases: _IntrinsicAliases followable: bool def key_written(self, name: str, key: str) -> bool: """ Whether the program writes the property *key* on a chain rooted at the global *name*: `Object.prototype.constructor = C`, `delete Object.getPrototypeOf`, or a descriptor installed for that key. A name whose written keys cannot be bounded answers `True` for every key, and so does a name outside `KEYED_WRITE_ROOTS`, which the scan records nothing about at all. See `refinery.lib.scripts.js.analysis.effects.EffectModel.global_key_written` for why the question is asked of a name rather than of a receiver. """ if name not in KEYED_WRITE_ROOTS: return True keys = self.keys.get(name, frozenset()) return keys is None or key in keys def follows_only_through_bindings(self, value: Node) -> bool: """ Whether a write through what *value* evaluates to is one this scan or the semantic model it read records where the value is spelled or a binding holds it, and never through a property holding it. The scan follows a value through the bindings that hold it and trusts `Object`, `Function`, `Boolean` and `Buffer` when they escape (`ESCAPE_RECORDED_ROOTS`), and the semantic model finds a write to a global through a name holding the global object, not through a property holding it. Stored in a property, `scope.P = Object.prototype` lets `scope.P.p = 1` write `Object.prototype` unrecorded; stored in a binding, `var P = Object.prototype` makes `P.p = 1` a recorded write. A value may carry what any of its `denoting_leaves` evaluates to, and one of them qualifies where the scan's aliases resolve it to a root the scan watches, where it may be the global object, or where the model was not built over it and so cannot say. The foot of a member chain is no value the chain evaluates to: it qualifies through the root it names, as `Object` does for `Object.prototype`, and never as the global object, since what `window.document` evaluates to is a property of that object. """ model = self.aliases.model for leaf in denoting_leaves(value): if not model.knows(leaf): return True if isinstance(leaf, JsIdentifier) and self.aliases.names_denoted_by(leaf) & ESCAPE_WATCHED_ROOTS: return True if not _is_member_object(leaf) and model.may_be_the_global_object(leaf): return True return False def records_a_read(self, node: Node, names: frozenset[str]) -> bool: """ Whether the scan records the watched *names* read at *node*: what *node* evaluates to is written through or escapes (`_records_a_watched_read`), the test the scan applies to a property read naming one of them. """ return _records_a_watched_read(self.aliases.model, self.aliases, node, names) def denotes_a_watched_root(self, binding: Binding) -> bool: """ Whether a value *binding* may hold denotes a root the scan watches, so that a write through a name resolving to the binding is one the scan attributes to that root. """ return bool(self.aliases.names_of(binding) & ESCAPE_WATCHED_ROOTS) def roots_unwritten(self, owner: str, roots: frozenset[str]) -> bool: """ Whether the program writes neither *owner* nor any prototype in *roots*. A property read resolves against the whole prototype chain rather than one prototype, so each name the chain passes through has to answer the same question `refinery.lib.scripts.js.analysis.effects.EffectModel.trusted_prototype` asks of the owner alone. """ return all(name not in self.names for name in (owner, *roots)) def chain_roots_unwritten(self, value_type: type) -> bool: """ Whether the program writes no prototype a plain property read on a value of *value_type* consults, so such a read can run no accessor the program installed there. See `refinery.lib.scripts.js.analysis.effects.EffectModel.chain_roots_unwritten` for what the answer leaves out and who may ask it. """ owner = PROTOTYPE_OWNERS.get(value_type.__name__) if owner is None: return False return self.roots_unwritten(owner, INHERITED_CHAIN_ROOTS) def records_more_than(self, other: IntrinsicWrites) -> bool: """ Whether this scan records a written name, a key written on a name, or a name whose written keys it cannot bound, that the scan *other* does not record. """ if self.names - other.names: return True for name, keys in self.keys.items(): known = other.keys.get(name, frozenset()) if known is None: continue if keys is None or keys - known: return True return False var followable-
Alias for field number 3
Expand source code Browse git
class IntrinsicWrites(NamedTuple): """ What one scan of a program says about the globals it writes: *names*, the set a caller asks about a whole name with, and *keys*, the properties each of the watched names was written at — or `None` for a name whose written keys this scan cannot bound. *aliases* is the relation the scan resolved write targets through, kept for `follows_only_through_bindings`. *followable* says whether the scan follows every write the program makes on a built-in object: none reaches one through a value read without spelling its owner (`_BuiltinReach`), no watched name is handed off, nor read as a property and handed off or written through, where the scan records the escape by key alone (`_KEY_RECORDED_ROOTS`), and, under the model that suspects code it cannot read, no call runs through a key the reflection check leaves uncounted (`_constructs_through_an_unread_key`). A fold that reads a built-in's chain or converts an object without spelling its owner asks it on top of the questions about *names*. The two are produced together because they are read off the same nodes and must not disagree: a name recorded in one for a reason the other cannot express is a name one caller refuses and the other clears. Where a write cannot be pinned to a key, *keys* records the name unbounded rather than omitting it, so `keys` never reports less about a name than `names` does. """ names: frozenset[str] keys: dict[str, frozenset[str] | None] aliases: _IntrinsicAliases followable: bool def key_written(self, name: str, key: str) -> bool: """ Whether the program writes the property *key* on a chain rooted at the global *name*: `Object.prototype.constructor = C`, `delete Object.getPrototypeOf`, or a descriptor installed for that key. A name whose written keys cannot be bounded answers `True` for every key, and so does a name outside `KEYED_WRITE_ROOTS`, which the scan records nothing about at all. See `refinery.lib.scripts.js.analysis.effects.EffectModel.global_key_written` for why the question is asked of a name rather than of a receiver. """ if name not in KEYED_WRITE_ROOTS: return True keys = self.keys.get(name, frozenset()) return keys is None or key in keys def follows_only_through_bindings(self, value: Node) -> bool: """ Whether a write through what *value* evaluates to is one this scan or the semantic model it read records where the value is spelled or a binding holds it, and never through a property holding it. The scan follows a value through the bindings that hold it and trusts `Object`, `Function`, `Boolean` and `Buffer` when they escape (`ESCAPE_RECORDED_ROOTS`), and the semantic model finds a write to a global through a name holding the global object, not through a property holding it. Stored in a property, `scope.P = Object.prototype` lets `scope.P.p = 1` write `Object.prototype` unrecorded; stored in a binding, `var P = Object.prototype` makes `P.p = 1` a recorded write. A value may carry what any of its `denoting_leaves` evaluates to, and one of them qualifies where the scan's aliases resolve it to a root the scan watches, where it may be the global object, or where the model was not built over it and so cannot say. The foot of a member chain is no value the chain evaluates to: it qualifies through the root it names, as `Object` does for `Object.prototype`, and never as the global object, since what `window.document` evaluates to is a property of that object. """ model = self.aliases.model for leaf in denoting_leaves(value): if not model.knows(leaf): return True if isinstance(leaf, JsIdentifier) and self.aliases.names_denoted_by(leaf) & ESCAPE_WATCHED_ROOTS: return True if not _is_member_object(leaf) and model.may_be_the_global_object(leaf): return True return False def records_a_read(self, node: Node, names: frozenset[str]) -> bool: """ Whether the scan records the watched *names* read at *node*: what *node* evaluates to is written through or escapes (`_records_a_watched_read`), the test the scan applies to a property read naming one of them. """ return _records_a_watched_read(self.aliases.model, self.aliases, node, names) def denotes_a_watched_root(self, binding: Binding) -> bool: """ Whether a value *binding* may hold denotes a root the scan watches, so that a write through a name resolving to the binding is one the scan attributes to that root. """ return bool(self.aliases.names_of(binding) & ESCAPE_WATCHED_ROOTS) def roots_unwritten(self, owner: str, roots: frozenset[str]) -> bool: """ Whether the program writes neither *owner* nor any prototype in *roots*. A property read resolves against the whole prototype chain rather than one prototype, so each name the chain passes through has to answer the same question `refinery.lib.scripts.js.analysis.effects.EffectModel.trusted_prototype` asks of the owner alone. """ return all(name not in self.names for name in (owner, *roots)) def chain_roots_unwritten(self, value_type: type) -> bool: """ Whether the program writes no prototype a plain property read on a value of *value_type* consults, so such a read can run no accessor the program installed there. See `refinery.lib.scripts.js.analysis.effects.EffectModel.chain_roots_unwritten` for what the answer leaves out and who may ask it. """ owner = PROTOTYPE_OWNERS.get(value_type.__name__) if owner is None: return False return self.roots_unwritten(owner, INHERITED_CHAIN_ROOTS) def records_more_than(self, other: IntrinsicWrites) -> bool: """ Whether this scan records a written name, a key written on a name, or a name whose written keys it cannot bound, that the scan *other* does not record. """ if self.names - other.names: return True for name, keys in self.keys.items(): known = other.keys.get(name, frozenset()) if known is None: continue if keys is None or keys - known: return True return False
Methods
def key_written(self, name, key)-
Whether the program writes the property key on a chain rooted at the global name:
Object.prototype.constructor = C,delete Object.getPrototypeOf, or a descriptor installed for that key. A name whose written keys cannot be bounded answersTruefor every key, and so does a name outsideKEYED_WRITE_ROOTS, which the scan records nothing about at all. SeeEffectModel.global_key_written()for why the question is asked of a name rather than of a receiver.Expand source code Browse git
def key_written(self, name: str, key: str) -> bool: """ Whether the program writes the property *key* on a chain rooted at the global *name*: `Object.prototype.constructor = C`, `delete Object.getPrototypeOf`, or a descriptor installed for that key. A name whose written keys cannot be bounded answers `True` for every key, and so does a name outside `KEYED_WRITE_ROOTS`, which the scan records nothing about at all. See `refinery.lib.scripts.js.analysis.effects.EffectModel.global_key_written` for why the question is asked of a name rather than of a receiver. """ if name not in KEYED_WRITE_ROOTS: return True keys = self.keys.get(name, frozenset()) return keys is None or key in keys def follows_only_through_bindings(self, value)-
Whether a write through what value evaluates to is one this scan or the semantic model it read records where the value is spelled or a binding holds it, and never through a property holding it. The scan follows a value through the bindings that hold it and trusts
Object,Function,BooleanandBufferwhen they escape (ESCAPE_RECORDED_ROOTS), and the semantic model finds a write to a global through a name holding the global object, not through a property holding it. Stored in a property,scope.P = Object.prototypeletsscope.P.p = 1writeObject.prototypeunrecorded; stored in a binding,var P = Object.prototypemakesP.p = 1a recorded write.A value may carry what any of its
denoting_leaves()evaluates to, and one of them qualifies where the scan's aliases resolve it to a root the scan watches, where it may be the global object, or where the model was not built over it and so cannot say. The foot of a member chain is no value the chain evaluates to: it qualifies through the root it names, asObjectdoes forObject.prototype, and never as the global object, since whatwindow.documentevaluates to is a property of that object.Expand source code Browse git
def follows_only_through_bindings(self, value: Node) -> bool: """ Whether a write through what *value* evaluates to is one this scan or the semantic model it read records where the value is spelled or a binding holds it, and never through a property holding it. The scan follows a value through the bindings that hold it and trusts `Object`, `Function`, `Boolean` and `Buffer` when they escape (`ESCAPE_RECORDED_ROOTS`), and the semantic model finds a write to a global through a name holding the global object, not through a property holding it. Stored in a property, `scope.P = Object.prototype` lets `scope.P.p = 1` write `Object.prototype` unrecorded; stored in a binding, `var P = Object.prototype` makes `P.p = 1` a recorded write. A value may carry what any of its `denoting_leaves` evaluates to, and one of them qualifies where the scan's aliases resolve it to a root the scan watches, where it may be the global object, or where the model was not built over it and so cannot say. The foot of a member chain is no value the chain evaluates to: it qualifies through the root it names, as `Object` does for `Object.prototype`, and never as the global object, since what `window.document` evaluates to is a property of that object. """ model = self.aliases.model for leaf in denoting_leaves(value): if not model.knows(leaf): return True if isinstance(leaf, JsIdentifier) and self.aliases.names_denoted_by(leaf) & ESCAPE_WATCHED_ROOTS: return True if not _is_member_object(leaf) and model.may_be_the_global_object(leaf): return True return False def records_a_read(self, node, names)-
Whether the scan records the watched names read at node: what node evaluates to is written through or escapes (
_records_a_watched_read), the test the scan applies to a property read naming one of them.Expand source code Browse git
def records_a_read(self, node: Node, names: frozenset[str]) -> bool: """ Whether the scan records the watched *names* read at *node*: what *node* evaluates to is written through or escapes (`_records_a_watched_read`), the test the scan applies to a property read naming one of them. """ return _records_a_watched_read(self.aliases.model, self.aliases, node, names) def denotes_a_watched_root(self, binding)-
Whether a value binding may hold denotes a root the scan watches, so that a write through a name resolving to the binding is one the scan attributes to that root.
Expand source code Browse git
def denotes_a_watched_root(self, binding: Binding) -> bool: """ Whether a value *binding* may hold denotes a root the scan watches, so that a write through a name resolving to the binding is one the scan attributes to that root. """ return bool(self.aliases.names_of(binding) & ESCAPE_WATCHED_ROOTS) def roots_unwritten(self, owner, roots)-
Whether the program writes neither owner nor any prototype in roots. A property read resolves against the whole prototype chain rather than one prototype, so each name the chain passes through has to answer the same question
EffectModel.trusted_prototype()asks of the owner alone.Expand source code Browse git
def roots_unwritten(self, owner: str, roots: frozenset[str]) -> bool: """ Whether the program writes neither *owner* nor any prototype in *roots*. A property read resolves against the whole prototype chain rather than one prototype, so each name the chain passes through has to answer the same question `refinery.lib.scripts.js.analysis.effects.EffectModel.trusted_prototype` asks of the owner alone. """ return all(name not in self.names for name in (owner, *roots)) def chain_roots_unwritten(self, value_type)-
Whether the program writes no prototype a plain property read on a value of value_type consults, so such a read can run no accessor the program installed there. See
EffectModel.chain_roots_unwritten()for what the answer leaves out and who may ask it.Expand source code Browse git
def chain_roots_unwritten(self, value_type: type) -> bool: """ Whether the program writes no prototype a plain property read on a value of *value_type* consults, so such a read can run no accessor the program installed there. See `refinery.lib.scripts.js.analysis.effects.EffectModel.chain_roots_unwritten` for what the answer leaves out and who may ask it. """ owner = PROTOTYPE_OWNERS.get(value_type.__name__) if owner is None: return False return self.roots_unwritten(owner, INHERITED_CHAIN_ROOTS) def records_more_than(self, other)-
Whether this scan records a written name, a key written on a name, or a name whose written keys it cannot bound, that the scan other does not record.
Expand source code Browse git
def records_more_than(self, other: IntrinsicWrites) -> bool: """ Whether this scan records a written name, a key written on a name, or a name whose written keys it cannot bound, that the scan *other* does not record. """ if self.names - other.names: return True for name, keys in self.keys.items(): known = other.keys.get(name, frozenset()) if known is None: continue if keys is None or keys - known: return True return False
class SpeculationState (*args, **kwds)-
Where a
WriteSpeculationstands.Expand source code Browse git
class SpeculationState(enum.Enum): """ Where a `WriteSpeculation` stands. """ ASSUMING = 'assuming' DISCHARGED = 'discharged' REFUTED = 'refuted'Ancestors
- enum.Enum
Class variables
var ASSUMING-
The type of the None singleton.
var DISCHARGED-
The type of the None singleton.
var REFUTED-
The type of the None singleton.
class SpeculationRefuted (*args, **kwargs)-
Raised by
WriteSpeculation.observe()at a build of the scan that records more than the build before it, and at every build after that one.Expand source code Browse git
class SpeculationRefuted(Exception): """ Raised by `WriteSpeculation.observe` at a build of the scan that records more than the build before it, and at every build after that one. """Ancestors
- builtins.Exception
- builtins.BaseException
class WriteSpeculation-
The record of a run that assumes every write on a built-in object the scan does not follow (
IntrinsicWrites.followable) writes nothing. While it assumes,observepresents each build of the scan as following every write and compares it with the build before it: one that records more (IntrinsicWrites.records_more_than()) refutes the assumption. A build that follows every write and records no more discharges it, and every later build is presented as it is.A run that ends discharged may keep its result. Each fold it made before the discharge read the record of some build and assumed nothing outside that record is written. No build records more than the one before, so the record of the discharging build, R, is part of every earlier one. Take an execution and the first write outside the record a fold relied on, if one happens: up to that moment every fold's premise held, so the discharged tree behaves alike up to it and makes the same write. That tree has no write the scan does not follow, so the write is in R, and so in every earlier record: no fold relied on its absence. So no such write happens. Code a splice brings in after the discharge is read by the scan as it is, as in any run.
Expand source code Browse git
class WriteSpeculation: """ The record of a run that assumes every write on a built-in object the scan does not follow (`IntrinsicWrites.followable`) writes nothing. While it assumes, `observe` presents each build of the scan as following every write and compares it with the build before it: one that records more (`IntrinsicWrites.records_more_than`) refutes the assumption. A build that follows every write and records no more discharges it, and every later build is presented as it is. A run that ends discharged may keep its result. Each fold it made before the discharge read the record of some build and assumed nothing outside that record is written. No build records more than the one before, so the record of the discharging build, R, is part of every earlier one. Take an execution and the first write outside the record a fold relied on, if one happens: up to that moment every fold's premise held, so the discharged tree behaves alike up to it and makes the same write. That tree has no write the scan does not follow, so the write is in R, and so in every earlier record: no fold relied on its absence. So no such write happens. Code a splice brings in after the discharge is read by the scan as it is, as in any run. """ def __init__(self): self.state = SpeculationState.ASSUMING self._record: IntrinsicWrites | None = None @property def assuming(self) -> bool: return self.state is SpeculationState.ASSUMING @property def discharged(self) -> bool: return self.state is SpeculationState.DISCHARGED def observe(self, writes: IntrinsicWrites) -> IntrinsicWrites: """ The build *writes* of the scan as the run reads it: presented as following every write while the assumption holds, and as it is once the assumption is discharged. Raises `SpeculationRefuted` where *writes* records more than the build before it, and at every build once that has happened. """ if self.state is SpeculationState.DISCHARGED: return writes if self.state is SpeculationState.REFUTED or ( self._record is not None and writes.records_more_than(self._record) ): self.state = SpeculationState.REFUTED raise SpeculationRefuted self._record = writes if writes.followable: self.state = SpeculationState.DISCHARGED return writes return writes._replace(followable=True)Instance variables
var assuming-
Expand source code Browse git
@property def assuming(self) -> bool: return self.state is SpeculationState.ASSUMING var discharged-
Expand source code Browse git
@property def discharged(self) -> bool: return self.state is SpeculationState.DISCHARGED
Methods
def observe(self, writes)-
The build writes of the scan as the run reads it: presented as following every write while the assumption holds, and as it is once the assumption is discharged. Raises
SpeculationRefutedwhere writes records more than the build before it, and at every build once that has happened.Expand source code Browse git
def observe(self, writes: IntrinsicWrites) -> IntrinsicWrites: """ The build *writes* of the scan as the run reads it: presented as following every write while the assumption holds, and as it is once the assumption is discharged. Raises `SpeculationRefuted` where *writes* records more than the build before it, and at every build once that has happened. """ if self.state is SpeculationState.DISCHARGED: return writes if self.state is SpeculationState.REFUTED or ( self._record is not None and writes.records_more_than(self._record) ): self.state = SpeculationState.REFUTED raise SpeculationRefuted self._record = writes if writes.followable: self.state = SpeculationState.DISCHARGED return writes return writes._replace(followable=True)
class UnfollowedWriteRefusals-
The record one deobfuscation run keeps of whether a question was answered no because the scan does not follow every write on a built-in object (
IntrinsicWrites.followable), on any tree of the run. Only such an answer can refuse a fold, so a run whose last tree is not followed runs again under aWriteSpeculationonly where one was given.Expand source code Browse git
class UnfollowedWriteRefusals: """ The record one deobfuscation run keeps of whether a question was answered no because the scan does not follow every write on a built-in object (`IntrinsicWrites.followable`), on any tree of the run. Only such an answer can refuse a fold, so a run whose last tree is not followed runs again under a `WriteSpeculation` only where one was given. """ def __init__(self): self.refused = False def note(self) -> None: """ Record that a question was answered no because the scan does not follow every write. """ self.refused = TrueMethods
def note(self)-
Record that a question was answered no because the scan does not follow every write.
Expand source code Browse git
def note(self) -> None: """ Record that a question was answered no because the scan does not follow every write. """ self.refused = True