Split mechanism and policy in the leaking visitor (#2594)

Summary:
The leaking, havocing, materialization and alias tracking implementation consists of two parts. The first part helps compute values reachable from other values with various constraints. The second part is the one that actually uses these reachable values and performs the action needed. This PR splits the first (mechanism) part from the second (policy) part.

Reachability can be used by invoking the following function:

```js
computeReachableObjectsAndBindings(
    realm: Realm,
    rootValue: Value,
    filterValue: Value => boolean,
    readOnly?: boolean
  ): [Set<ObjectValue>, Set<Binding>]
```
...to compute all possible values that `rootValue` could evaluate to. It returns a tuple consisting of a set of reachable objects and a set of reachable bindings.

`filterValue` limits the values that we traverse. For leaking, `filterValue` checks that a value has not already been leaked (since interactions with leaked values cause leaking), if a value is certainly not an object, if it is frozen, in scope etc. You could use this to find particular types of reachable objects.

If the `readOnly` flag is set, then only read bindings are returned.
Pull Request resolved: https://github.com/facebook/prepack/pull/2594

Differential Revision: D10488695

Pulled By: sb98052

fbshipit-source-id: c8f0a63148f4cccd6434ebe07bebfb38dcd5cf08
This commit is contained in:
Sapan Bhatia 2018-10-22 13:39:29 -07:00 committed by Facebook Github Bot
parent f309cf0049
commit d6da6bf8ab
6 changed files with 474 additions and 789 deletions

View File

@ -14,6 +14,7 @@ import { CreateImplementation } from "./methods/create.js";
import { EnvironmentImplementation } from "./methods/environment.js";
import { FunctionImplementation } from "./methods/function.js";
import { LeakImplementation, MaterializeImplementation } from "./utils/leak.js";
import { ReachabilityImplementation } from "./utils/reachability.js";
import { JoinImplementation } from "./methods/join.js";
import { PathConditionsImplementation, PathImplementation } from "./utils/paths.js";
import { PropertiesImplementation } from "./methods/properties.js";
@ -30,6 +31,7 @@ export default function() {
Singletons.setFunctions(new FunctionImplementation());
Singletons.setLeak(new LeakImplementation());
Singletons.setMaterialize(new MaterializeImplementation());
Singletons.setReachability(new ReachabilityImplementation());
Singletons.setJoin(new JoinImplementation());
Singletons.setPath(new PathImplementation());
Singletons.setPathConditions((val: PathConditions | void) => new PathConditionsImplementation(val));

View File

@ -20,6 +20,7 @@ import type {
PathType,
PathConditions,
PropertiesType,
ReachabilityType,
ToType,
UtilsType,
WidenType,
@ -31,6 +32,7 @@ export let Environment: EnvironmentType = (null: any);
export let Functions: FunctionType = (null: any);
export let Leak: LeakType = (null: any);
export let Materialize: MaterializeType = (null: any);
export let Reachability: ReachabilityType = (null: any);
export let Join: JoinType = (null: any);
export let Path: PathType = (null: any);
export let createPathConditions: (PathConditions | void) => PathConditions = (null: any);
@ -62,6 +64,9 @@ export function setMaterialize(singleton: MaterializeType): void {
Materialize = singleton;
}
export function setReachability(singleton: ReachabilityType): void {
Reachability = singleton;
}
export function setJoin(singleton: JoinType): void {
Join = singleton;
}

View File

@ -403,9 +403,16 @@ export type LeakType = {
export type MaterializeType = {
materializeObject(realm: Realm, object: ObjectValue): void,
computeReachableObjects(realm: Realm, value: Value): Set<ObjectValue>,
};
export type ReachabilityType = {
computeReachableObjectsAndBindings(
realm: Realm,
rootValue: Value,
filterValue: (Value) => boolean,
readOnly?: boolean
): [Set<ObjectValue>, Set<Binding>],
};
export type PropertiesType = {
// ECMA262 9.1.9.1
OrdinarySet(realm: Realm, O: ObjectValue, P: PropertyKeyValue, V: Value, Receiver: Value): boolean,

View File

@ -11,106 +11,16 @@
import { CompilerDiagnostic, FatalError } from "../errors.js";
import type { Realm } from "../realm.js";
import type { Descriptor, PropertyBinding, ObjectKind } from "../types.js";
import {
leakBinding,
DeclarativeEnvironmentRecord,
FunctionEnvironmentRecord,
ObjectEnvironmentRecord,
GlobalEnvironmentRecord,
} from "../environment.js";
import {
AbstractValue,
ArrayValue,
BoundFunctionValue,
ECMAScriptSourceFunctionValue,
EmptyValue,
FunctionValue,
NativeFunctionValue,
ObjectValue,
PrimitiveValue,
ProxyValue,
Value,
} from "../values/index.js";
import { leakBinding } from "../environment.js";
import { AbstractValue, ArrayValue, EmptyValue, ObjectValue, Value } from "../values/index.js";
import { TestIntegrityLevel } from "../methods/index.js";
import * as t from "@babel/types";
import traverse from "@babel/traverse";
import type { BabelTraversePath } from "@babel/traverse";
import type { BabelNodeSourceLocation } from "@babel/types";
import invariant from "../invariant.js";
import { HeapInspector } from "../utils/HeapInspector.js";
import { Logger } from "../utils/logger.js";
import { isReactElement } from "../react/utils.js";
import { PropertyDescriptor, AbstractJoinedDescriptor } from "../descriptors.js";
type LeakedFunctionInfo = {
unboundReads: Set<string>,
unboundWrites: Set<string>,
};
function visitName(
path: BabelTraversePath,
state: LeakedFunctionInfo,
name: string,
read: boolean,
write: boolean
): void {
// Is the name bound to some local identifier? If so, we don't need to do anything
if (path.scope.hasBinding(name, /*noGlobals*/ true)) return;
// Otherwise, let's record that there's an unbound identifier
if (read) state.unboundReads.add(name);
if (write) state.unboundWrites.add(name);
}
function ignorePath(path: BabelTraversePath): boolean {
let parent = path.parent;
return t.isLabeledStatement(parent) || t.isBreakStatement(parent) || t.isContinueStatement(parent);
}
let LeakedClosureRefVisitor = {
ReferencedIdentifier(path: BabelTraversePath, state: LeakedFunctionInfo): void {
if (ignorePath(path)) return;
let innerName = path.node.name;
if (innerName === "arguments") {
return;
}
visitName(path, state, innerName, true, false);
},
"AssignmentExpression|UpdateExpression"(path: BabelTraversePath, state: LeakedFunctionInfo): void {
let doesRead = path.node.operator !== "=";
for (let name in path.getBindingIdentifiers()) {
visitName(path, state, name, doesRead, true);
}
},
};
function getLeakedFunctionInfo(value: FunctionValue) {
// TODO: This should really be cached on a per AST basis in case we have
// many uses of the same closure. It should ideally share this cache
// and data with ResidualHeapVisitor.
invariant(value instanceof ECMAScriptSourceFunctionValue);
invariant(value.constructor === ECMAScriptSourceFunctionValue);
let functionInfo = {
unboundReads: new Set(),
unboundWrites: new Set(),
};
let formalParameters = value.$FormalParameters;
invariant(formalParameters != null);
let code = value.$ECMAScriptCode;
invariant(code != null);
traverse(
t.file(t.program([t.expressionStatement(t.functionExpression(null, formalParameters, code))])),
LeakedClosureRefVisitor,
null,
functionInfo
);
traverse.cache.clear();
return functionInfo;
}
import { Reachability } from "../singletons.js";
import { PropertyDescriptor } from "../descriptors.js";
function materializeObject(realm: Realm, object: ObjectValue, getCachingHeapInspector?: () => HeapInspector): void {
let generator = realm.generator;
@ -170,397 +80,6 @@ function materializeObject(realm: Realm, object: ObjectValue, getCachingHeapInsp
}
}
}
class ObjectValueLeakingVisitor {
realm: Realm;
// ObjectValues to visit if they're reachable.
objectsTrackedForLeaks: Set<ObjectValue>;
// Values that has been visited.
visitedValues: Set<Value>;
_heapInspector: HeapInspector;
constructor(realm: Realm, objectsTrackedForLeaks: Set<ObjectValue>) {
this.realm = realm;
this.objectsTrackedForLeaks = objectsTrackedForLeaks;
this.visitedValues = new Set();
}
mustVisit(val: Value): boolean {
if (val instanceof ObjectValue) {
// For Objects we only need to visit it if it is tracked
// as a newly created object that might still be mutated.
// Abstract values gets their arguments visited.
if (!this.objectsTrackedForLeaks.has(val)) return false;
}
if (this.visitedValues.has(val)) return false;
this.visitedValues.add(val);
return true;
}
visitObjectProperty(binding: PropertyBinding): void {
let desc = binding.descriptor;
if (desc === undefined) return; //deleted
this.visitDescriptor(desc);
}
visitObjectProperties(obj: ObjectValue, kind?: ObjectKind): void {
// visit symbol properties
for (let [, propertyBindingValue] of obj.symbols) {
invariant(propertyBindingValue);
this.visitObjectProperty(propertyBindingValue);
}
// visit string properties
for (let [, propertyBindingValue] of obj.properties) {
invariant(propertyBindingValue);
this.visitObjectProperty(propertyBindingValue);
}
// inject properties with computed names
if (obj.unknownProperty !== undefined) {
let desc = obj.unknownProperty.descriptor;
this.visitObjectPropertiesWithComputedNamesDescriptor(desc);
}
// prototype
this.visitObjectPrototype(obj);
if (TestIntegrityLevel(this.realm, obj, "frozen")) return;
// if this object wasn't already leaked, we need mark it as leaked
// so that any mutation and property access get tracked after this.
if (obj.mightNotBeLeakedObject()) {
obj.leak();
// materialization is a common operation and needs to be invoked
// whenever non-final values need to be made available at intermediate
// points in a program's control flow. An object can be materialized by
// calling materializeObject(). Sometimes, objects
// are materialized in cohorts (such as during leaking).
// In these cases, we provide a caching mechanism for HeapInspector().
let makeAndCacheHeapInspector = () => {
let heapInspector = this._heapInspector;
if (heapInspector !== undefined) return heapInspector;
else {
heapInspector = new HeapInspector(this.realm, new Logger(this.realm, /*internalDebug*/ false));
this._heapInspector = heapInspector;
return heapInspector;
}
};
invariant(this.realm.generator !== undefined);
materializeObject(this.realm, obj, makeAndCacheHeapInspector);
}
}
visitObjectPrototype(obj: ObjectValue): void {
let proto = obj.$Prototype;
this.visitValue(proto);
}
visitObjectPropertiesWithComputedNamesDescriptor(desc: void | Descriptor): void {
if (desc !== undefined) {
if (desc instanceof PropertyDescriptor) {
let val = desc.value;
invariant(val instanceof AbstractValue);
this.visitObjectPropertiesWithComputedNames(val);
} else if (desc instanceof AbstractJoinedDescriptor) {
this.visitValue(desc.joinCondition);
this.visitObjectPropertiesWithComputedNamesDescriptor(desc.descriptor1);
this.visitObjectPropertiesWithComputedNamesDescriptor(desc.descriptor2);
} else {
invariant(false, "unknown descriptor");
}
}
}
visitObjectPropertiesWithComputedNames(absVal: AbstractValue): void {
if (absVal.kind === "widened property") return;
if (absVal.kind === "template for prototype member expression") return;
if (absVal.kind === "conditional") {
let cond = absVal.args[0];
invariant(cond instanceof AbstractValue);
if (cond.kind === "template for property name condition") {
let P = cond.args[0];
invariant(P instanceof AbstractValue);
let V = absVal.args[1];
let earlier_props = absVal.args[2];
if (earlier_props instanceof AbstractValue) this.visitObjectPropertiesWithComputedNames(earlier_props);
this.visitValue(P);
this.visitValue(V);
} else {
// conditional assignment
this.visitValue(cond);
let consequent = absVal.args[1];
if (consequent instanceof AbstractValue) {
this.visitObjectPropertiesWithComputedNames(consequent);
}
let alternate = absVal.args[2];
if (alternate instanceof AbstractValue) {
this.visitObjectPropertiesWithComputedNames(alternate);
}
}
} else {
this.visitValue(absVal);
}
}
visitDescriptor(desc: void | Descriptor): void {
if (desc === undefined) {
} else if (desc instanceof PropertyDescriptor) {
if (desc.value !== undefined) this.visitValue(desc.value);
if (desc.get !== undefined) this.visitValue(desc.get);
if (desc.set !== undefined) this.visitValue(desc.set);
} else if (desc instanceof AbstractJoinedDescriptor) {
this.visitValue(desc.joinCondition);
if (desc.descriptor1 !== undefined) this.visitDescriptor(desc.descriptor1);
if (desc.descriptor2 !== undefined) this.visitDescriptor(desc.descriptor2);
} else {
invariant(false, "unknown descriptor");
}
}
visitDeclarativeEnvironmentRecordBinding(
record: DeclarativeEnvironmentRecord,
remainingLeakedBindings: LeakedFunctionInfo
): void {
let bindings = record.bindings;
for (let bindingName of Object.keys(bindings)) {
let binding = bindings[bindingName];
// Check if this binding is referenced, and if so delete it from the set.
let isRead = remainingLeakedBindings.unboundReads.delete(bindingName);
let isWritten = remainingLeakedBindings.unboundWrites.delete(bindingName);
if (isRead) {
// If this binding can be read from the closure, its value has now leaked.
let value = binding.value;
if (value) {
this.visitValue(value);
}
}
if (isWritten || isRead) {
// If this binding could have been mutated from the closure, then the
// binding itself has now leaked, but not necessarily the value in it.
// TODO: We could tag a leaked binding as read and/or write. That way
// we don't have to leak values written to this binding if only the binding
// has been written to. We also don't have to leak reads from this binding
// if it is only read from.
leakBinding(binding);
}
}
}
visitValueMap(val: ObjectValue): void {
let kind = val.getKind();
let entries;
if (kind === "Map") {
entries = val.$MapData;
} else {
invariant(kind === "WeakMap");
entries = val.$WeakMapData;
}
invariant(entries !== undefined);
let len = entries.length;
for (let i = 0; i < len; i++) {
let entry = entries[i];
let key = entry.$Key;
let value = entry.$Value;
if (key === undefined || value === undefined) continue;
this.visitValue(key);
this.visitValue(value);
}
}
visitValueSet(val: ObjectValue): void {
let kind = val.getKind();
let entries;
if (kind === "Set") {
entries = val.$SetData;
} else {
invariant(kind === "WeakSet");
entries = val.$WeakSetData;
}
invariant(entries !== undefined);
let len = entries.length;
for (let i = 0; i < len; i++) {
let entry = entries[i];
if (entry === undefined) continue;
this.visitValue(entry);
}
}
visitValueFunction(val: FunctionValue): void {
if (!val.mightNotBeLeakedObject()) {
return;
}
this.visitObjectProperties(val);
if (val instanceof BoundFunctionValue) {
this.visitValue(val.$BoundTargetFunction);
this.visitValue(val.$BoundThis);
for (let boundArg of val.$BoundArguments) this.visitValue(boundArg);
return;
}
invariant(
!(val instanceof NativeFunctionValue),
"all native function values should have already been created outside this pure function"
);
let remainingLeakedBindings = getLeakedFunctionInfo(val);
let environment = val.$Environment;
while (environment) {
let record = environment.environmentRecord;
if (record instanceof ObjectEnvironmentRecord) {
this.visitValue(record.object);
continue;
}
if (record instanceof GlobalEnvironmentRecord) {
break;
}
invariant(record instanceof DeclarativeEnvironmentRecord);
this.visitDeclarativeEnvironmentRecordBinding(record, remainingLeakedBindings);
if (record instanceof FunctionEnvironmentRecord) {
// If this is a function environment, which is not tracked for leaks,
// we can bail out because its bindings should not be mutated in a
// pure function.
let fn = record.$FunctionObject;
if (!this.objectsTrackedForLeaks.has(fn)) {
break;
}
}
environment = environment.parent;
}
}
visitValueObject(val: ObjectValue): void {
if (!val.mightNotBeLeakedObject()) {
return;
}
let kind = val.getKind();
this.visitObjectProperties(val, kind);
switch (kind) {
case "RegExp":
case "Number":
case "String":
case "Boolean":
case "ReactElement":
case "ArrayBuffer":
case "Array":
return;
case "Date":
let dateValue = val.$DateValue;
invariant(dateValue !== undefined);
this.visitValue(dateValue);
return;
case "Float32Array":
case "Float64Array":
case "Int8Array":
case "Int16Array":
case "Int32Array":
case "Uint8Array":
case "Uint16Array":
case "Uint32Array":
case "Uint8ClampedArray":
case "DataView":
let buf = val.$ViewedArrayBuffer;
invariant(buf !== undefined);
this.visitValue(buf);
return;
case "Map":
case "WeakMap":
this.visitValueMap(val);
return;
case "Set":
case "WeakSet":
this.visitValueSet(val);
return;
default:
invariant(kind === "Object", `Object of kind ${kind} is not supported in calls to abstract functions.`);
invariant(val.$ParameterMap === undefined, `Arguments object is not supported in calls to abstract functions.`);
return;
}
}
visitValueProxy(val: ProxyValue): void {
this.visitValue(val.$ProxyTarget);
this.visitValue(val.$ProxyHandler);
}
visitAbstractValue(val: AbstractValue): void {
if (!val.mightBeObject()) {
// Only objects need to be leaked.
return;
}
if (val.values.isTop()) {
// If we don't know which object instances it might be,
// then it might be one of the arguments that created
// this value. See #2179.
if (val.kind === "conditional") {
// For a conditional, we only have to visit each case. Not the condition itself.
this.visitValue(val.args[1]);
this.visitValue(val.args[2]);
return;
}
// To ensure that we don't forget to provide arguments
// that can be havoced, we require at least one argument.
let whitelistedKind =
val.kind &&
(val.kind === "widened numeric property" || // TODO: Widened properties needs to be havocable.
val.kind.startsWith("abstractCounted"));
invariant(
whitelistedKind !== undefined || val.intrinsicName !== undefined || val.args.length > 0,
"Havoced unknown object requires havocable arguments"
);
// TODO: This is overly conservative. We recursively leak all the inputs
// to this operation whether or not they can possible be part of the
// result value or not.
for (let i = 0, n = val.args.length; i < n; i++) {
this.visitValue(val.args[i]);
}
return;
}
// If we know which object this might be, then leak each of them.
for (let element of val.values.getElements()) {
this.visitValue(element);
}
}
visitValue(val: Value): void {
if (val instanceof AbstractValue) {
if (this.mustVisit(val)) this.visitAbstractValue(val);
} else if (val.isIntrinsic()) {
// All intrinsic values exist from the beginning of time (except arrays with widened properties)...
// ...except for a few that come into existance as templates for abstract objects.
if (val instanceof ArrayValue && ArrayValue.isIntrinsicAndHasWidenedNumericProperty(val)) {
if (this.mustVisit(val)) this.visitValueObject(val);
} else {
this.mustVisit(val);
}
} else if (val instanceof EmptyValue) {
this.mustVisit(val);
} else if (val instanceof PrimitiveValue) {
this.mustVisit(val);
} else if (val instanceof ProxyValue) {
if (this.mustVisit(val)) this.visitValueProxy(val);
} else if (val instanceof FunctionValue) {
invariant(val instanceof FunctionValue);
if (this.mustVisit(val)) this.visitValueFunction(val);
} else {
invariant(val instanceof ObjectValue);
if (this.mustVisit(val)) this.visitValueObject(val);
}
}
}
function ensureFrozenValue(realm, value, loc): void {
// TODO: This should really check if it is recursively immutability.
@ -596,13 +115,57 @@ export class LeakImplementation {
// is invalid unless it's frozen.
ensureFrozenValue(realm, value, loc);
} else {
// If we're tracking a pure function, we can assume that only newly
// created objects and bindings, within it, are mutable. Any other
// object can safely be assumed to be deeply immutable as far as this
// pure function is concerned. However, any mutable object needs to
// be tainted as possibly having changed to anything.
let visitor = new ObjectValueLeakingVisitor(realm, objectsTrackedForLeaks);
visitor.visitValue(value);
// This function decides what values are descended into by leaking
function leakingFilter(val: Value) {
if (val instanceof AbstractValue) {
// To ensure that we don't forget to provide arguments
// that can be leaked, we require at least one argument.
let whitelistedKind = val.kind && val.kind.startsWith("abstractCounted");
invariant(
whitelistedKind !== undefined || val.intrinsicName !== undefined || val.args.length > 0,
"Leaked unknown object requires leakable arguments"
);
}
// We skip a value if one of the following holds:
// 1. It has certainly been leaked
// 2. It was not created in the current pure scope
// 3. It is not a frozen object
// 4. It certainly does not evaluate to an object
// 5. Is it an intrinsic, but not a widened array
return (
(!(val instanceof ObjectValue) ||
(val.mightNotBeLeakedObject() &&
objectsTrackedForLeaks.has(val) &&
!TestIntegrityLevel(realm, val, "frozen"))) &&
val.mightBeObject() &&
(!val.isIntrinsic() || (val instanceof ArrayValue && ArrayValue.isIntrinsicAndHasWidenedNumericProperty(val)))
);
}
let [reachableObjects, reachableBindings] = Reachability.computeReachableObjectsAndBindings(
realm,
value,
leakingFilter,
false /* readOnly */
);
let cachedHeapInspector;
let makeAndCacheHeapInspector = () => {
if (cachedHeapInspector === undefined) {
cachedHeapInspector = new HeapInspector(realm, new Logger(realm, /*internalDebug*/ false));
}
return cachedHeapInspector;
};
for (let val of reachableObjects) {
val.leak();
materializeObject(realm, val, makeAndCacheHeapInspector);
}
for (let binding of reachableBindings) {
leakBinding(binding);
}
}
}
}
@ -616,297 +179,4 @@ export class MaterializeImplementation {
val.makeFinal();
else materializeObject(realm, val);
}
// This routine materializes objects reachable from non-local bindings read
// by a function. It does this for the purpose of outlining calls to that function.
//
// Notes:
// - Locations that are only read need not materialize because their values are up-to-date
// at optimization time,
// - Locations that are written to are ignored, because we make the assumption, for now,
// that the function being outlined is pure.
// - Previously havoced locations (#2446) should be reloaded, but are currently rejected.
// - Specialization depends on the assumption that the Array op will only be used once.
// First, we will enforce it: #2448. Later we will relax it: #2454
computeReachableObjects(realm: Realm, rootValue: Value): Set<ObjectValue> {
invariant(realm.isInPureScope());
let reachableObjects: Set<ObjectValue> = new Set();
let visitedValues: Set<Value> = new Set();
computeFromValue(rootValue);
return reachableObjects;
function computeFromBindings(func: FunctionValue, nonLocalReadBindings: Set<string>): void {
invariant(func instanceof ECMAScriptSourceFunctionValue);
let environment = func.$Environment;
while (environment) {
let record = environment.environmentRecord;
if (record instanceof ObjectEnvironmentRecord) computeFromValue(record.object);
else if (record instanceof DeclarativeEnvironmentRecord || record instanceof FunctionEnvironmentRecord)
computeFromDeclarativeEnvironmentRecord(record, nonLocalReadBindings);
else if (record instanceof GlobalEnvironmentRecord) {
// TODO: #2484
break;
}
environment = environment.parent;
}
}
function computeFromDeclarativeEnvironmentRecord(
record: DeclarativeEnvironmentRecord,
nonLocalReadBindings: Set<string>
): void {
let environmentBindings = record.bindings;
for (let bindingName of Object.keys(environmentBindings)) {
let binding = environmentBindings[bindingName];
invariant(binding !== undefined);
let found = nonLocalReadBindings.delete(bindingName);
// Check what undefined could mean here, besides absent binding
// #2446
if (found && binding.value !== undefined) {
computeFromValue(binding.value);
}
}
}
function computeFromAbstractValue(value: AbstractValue): void {
if (value.values.isTop()) {
for (let arg of value.args) {
computeFromValue(arg);
}
} else {
// If we know which object this might be, then leak each of them.
for (let element of value.values.getElements()) {
computeFromValue(element);
}
}
}
function computeFromProxyValue(value: ProxyValue): void {
computeFromValue(value.$ProxyTarget);
computeFromValue(value.$ProxyHandler);
}
function computeFromValue(value: Value): void {
invariant(value !== undefined);
if (value.isIntrinsic() || value instanceof EmptyValue || value instanceof PrimitiveValue) {
visit(value);
} else if (value instanceof AbstractValue) {
ifNotVisited(value, computeFromAbstractValue);
} else if (value instanceof FunctionValue) {
ifNotVisited(value, computeFromFunctionValue);
} else if (value instanceof ObjectValue) {
ifNotVisited(value, computeFromObjectValue);
} else if (value instanceof ProxyValue) {
ifNotVisited(value, computeFromProxyValue);
}
}
function computeFromObjectValue(value: Value): void {
invariant(value instanceof ObjectValue);
let kind = value.getKind();
computeFromObjectProperties(value, kind);
switch (kind) {
case "RegExp":
case "Number":
case "String":
case "Boolean":
case "ReactElement":
case "ArrayBuffer":
case "Array":
break;
case "Date":
let dateValue = value.$DateValue;
invariant(dateValue !== undefined);
computeFromValue(dateValue);
break;
case "Float32Array":
case "Float64Array":
case "Int8Array":
case "Int16Array":
case "Int32Array":
case "Uint8Array":
case "Uint16Array":
case "Uint32Array":
case "Uint8ClampedArray":
case "DataView":
let buf = value.$ViewedArrayBuffer;
invariant(buf !== undefined);
computeFromValue(buf);
break;
case "Map":
case "WeakMap":
ifNotVisited(value, computeFromMap);
break;
case "Set":
case "WeakSet":
ifNotVisited(value, computeFromSet);
break;
default:
invariant(kind === "Object", `Object of kind ${kind} is not supported in calls to abstract functions.`);
invariant(
value.$ParameterMap === undefined,
`Arguments object is not supported in calls to abstract functions.`
);
break;
}
if (!reachableObjects.has(value)) reachableObjects.add(value);
}
function computeFromDescriptor(descriptor: Descriptor): void {
if (descriptor === undefined) {
} else if (descriptor instanceof PropertyDescriptor) {
if (descriptor.value !== undefined) computeFromValue(descriptor.value);
if (descriptor.get !== undefined) computeFromValue(descriptor.get);
if (descriptor.set !== undefined) computeFromValue(descriptor.set);
} else if (descriptor instanceof AbstractJoinedDescriptor) {
computeFromValue(descriptor.joinCondition);
if (descriptor.descriptor1 !== undefined) computeFromDescriptor(descriptor.descriptor1);
if (descriptor.descriptor2 !== undefined) computeFromDescriptor(descriptor.descriptor2);
} else {
invariant(false, "unknown descriptor");
}
}
function computeFromObjectPropertyBinding(binding: PropertyBinding): void {
let descriptor = binding.descriptor;
if (descriptor === undefined) return; //deleted
computeFromDescriptor(descriptor);
}
function computeFromObjectProperties(obj: ObjectValue, kind?: ObjectKind): void {
// symbol properties
for (let [, propertyBindingValue] of obj.symbols) {
invariant(propertyBindingValue);
computeFromObjectPropertyBinding(propertyBindingValue);
}
// string properties
for (let [, propertyBindingValue] of obj.properties) {
invariant(propertyBindingValue);
computeFromObjectPropertyBinding(propertyBindingValue);
}
// inject properties with computed names
if (obj.unknownProperty !== undefined) {
let descriptor = obj.unknownProperty.descriptor;
computeFromObjectPropertiesWithComputedNamesDescriptor(descriptor);
}
// prototype
computeFromObjectPrototype(obj);
}
function computeFromObjectPrototype(obj: ObjectValue) {
if (obj.$Prototype !== undefined) computeFromValue(obj.$Prototype);
}
function computeFromFunctionValue(fn: FunctionValue) {
computeFromObjectProperties(fn);
if (fn instanceof BoundFunctionValue) {
computeFromValue(fn.$BoundTargetFunction);
computeFromValue(fn.$BoundThis);
for (let boundArg of fn.$BoundArguments) computeFromValue(boundArg);
return;
}
invariant(
!(fn instanceof NativeFunctionValue),
"all native function values should have already been created outside this pure function"
);
let nonLocalReadBindings = nonLocalReadBindingsOfFunction(fn);
computeFromBindings(fn, nonLocalReadBindings);
}
function computeFromObjectPropertiesWithComputedNamesDescriptor(descriptor: void | Descriptor): void {
// TODO: #2484
notSupportedForTransitiveMaterialization();
}
function computeFromMap(val: ObjectValue): void {
let kind = val.getKind();
let entries;
if (kind === "Map") {
entries = val.$MapData;
} else {
invariant(kind === "WeakMap");
entries = val.$WeakMapData;
}
invariant(entries !== undefined);
let len = entries.length;
for (let i = 0; i < len; i++) {
let entry = entries[i];
let key = entry.$Key;
let value = entry.$Value;
if (key === undefined || value === undefined) continue;
computeFromValue(key);
computeFromValue(value);
}
}
function computeFromSet(val: ObjectValue): void {
let kind = val.getKind();
let entries;
if (kind === "Set") {
entries = val.$SetData;
} else {
invariant(kind === "WeakSet");
entries = val.$WeakSetData;
}
invariant(entries !== undefined);
let len = entries.length;
for (let i = 0; i < len; i++) {
let entry = entries[i];
if (entry === undefined) continue;
computeFromValue(entry);
}
}
function nonLocalReadBindingsOfFunction(func: FunctionValue) {
// unboundWrites is currently not used, but we leave it in place
// to reuse the function closure visitor implemented for leaking
let functionInfo = {
unboundReads: new Set(),
unboundWrites: new Set(),
};
invariant(func instanceof ECMAScriptSourceFunctionValue);
let formalParameters = func.$FormalParameters;
invariant(formalParameters != null);
let code = func.$ECMAScriptCode;
invariant(code != null);
traverse(
t.file(t.program([t.expressionStatement(t.functionExpression(null, formalParameters, code))])),
LeakedClosureRefVisitor,
null,
functionInfo
);
traverse.cache.clear();
// TODO #2478: add invariant that there are no write bindings
return functionInfo.unboundReads;
}
function ifNotVisited<T>(value: T, computeFrom: T => void): void {
if (!visitedValues.has(value)) {
visitedValues.add(value);
computeFrom(value);
}
}
function visit(value: Value): void {
visitedValues.add(value);
}
function notSupportedForTransitiveMaterialization() {
let error = new CompilerDiagnostic(
"Not supported for transitive materialization",
rootValue.expressionLocation,
"PP0041",
"FatalError"
);
realm.handleError(error);
throw new FatalError();
}
}
}

390
src/utils/reachability.js Normal file
View File

@ -0,0 +1,390 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
/* @flow strict-local */
// Implements routines to find reachable values for the purpose of leaked value analysis
import type { Realm } from "../realm.js";
import type { Descriptor, PropertyBinding, ObjectKind } from "../types.js";
import {
DeclarativeEnvironmentRecord,
FunctionEnvironmentRecord,
ObjectEnvironmentRecord,
GlobalEnvironmentRecord,
} from "../environment.js";
import {
AbstractValue,
BoundFunctionValue,
ECMAScriptSourceFunctionValue,
EmptyValue,
FunctionValue,
NativeFunctionValue,
ObjectValue,
PrimitiveValue,
ProxyValue,
Value,
} from "../values/index.js";
import * as t from "@babel/types";
import traverse from "@babel/traverse";
import type { BabelTraversePath } from "@babel/traverse";
import type { Binding } from "../environment.js";
import invariant from "../invariant.js";
import { PropertyDescriptor, AbstractJoinedDescriptor } from "../descriptors.js";
type FunctionBindingVisitorInfo = {
unboundReads: Set<string>,
unboundWrites: Set<string>,
};
function visitName(
path: BabelTraversePath,
state: FunctionBindingVisitorInfo,
name: string,
read: boolean,
write: boolean
): void {
// Is the name bound to some local identifier? If so, we don't need to do anything
if (path.scope.hasBinding(name, /*noGlobals*/ true)) return;
// Otherwise, let's record that there's an unbound identifier
if (read) state.unboundReads.add(name);
if (write) state.unboundWrites.add(name);
}
// Why are these paths ignored?
function ignorePath(path: BabelTraversePath): boolean {
let parent = path.parent;
return t.isLabeledStatement(parent) || t.isBreakStatement(parent) || t.isContinueStatement(parent);
}
let FunctionBindingVisitor = {
_readOnly: false,
ReferencedIdentifier(path: BabelTraversePath, state: FunctionBindingVisitorInfo): void {
if (ignorePath(path)) return;
let innerName = path.node.name;
if (innerName === "arguments") {
return;
}
visitName(path, state, innerName, true, false);
},
AssignmentExpression(path: BabelTraversePath, state: FunctionBindingVisitorInfo): void {
let doesRead = path.node.operator !== "=";
for (let name in path.getBindingIdentifiers()) {
visitName(path, state, name, doesRead, !this._readOnly);
}
},
UpdateExpression(path: BabelTraversePath, state: FunctionBindingVisitorInfo): void {
let doesRead = path.node.operator !== "=";
for (let name in path.getBindingIdentifiers()) {
visitName(path, state, name, doesRead, !this._readOnly);
}
},
};
export class ReachabilityImplementation {
computeReachableObjectsAndBindings(
realm: Realm,
rootValue: Value,
filterValue: Value => boolean,
readOnly?: boolean
): [Set<ObjectValue>, Set<Binding>] {
invariant(realm.isInPureScope());
let reachableObjects: Set<ObjectValue> = new Set();
let reachableBindings: Set<Binding> = new Set();
let visitedValues: Set<Value> = new Set();
computeFromValue(rootValue);
return [reachableObjects, reachableBindings];
function computeFromBindings(
func: FunctionValue,
nonLocalReadBindings: Set<string>,
nonLocalWriteBindings: Set<string>
): void {
invariant(func instanceof ECMAScriptSourceFunctionValue);
let environment = func.$Environment;
while (environment) {
let record = environment.environmentRecord;
if (record instanceof ObjectEnvironmentRecord) computeFromValue(record.object);
else if (record instanceof DeclarativeEnvironmentRecord || record instanceof FunctionEnvironmentRecord) {
computeFromDeclarativeEnvironmentRecord(record, nonLocalReadBindings, nonLocalWriteBindings);
if (record instanceof FunctionEnvironmentRecord) {
// We only ascend further in environments if the function is whitelisted
let fn = record.$FunctionObject;
if (!filterValue(fn)) break;
}
} else if (record instanceof GlobalEnvironmentRecord) {
// Reachability does not enumerate global bindings and objects.
// Prepack assumes that external functions will not mutate globals
// and that any references to globals need their final values.
break;
}
environment = environment.parent;
}
}
function computeFromDeclarativeEnvironmentRecord(
record: DeclarativeEnvironmentRecord,
nonLocalReadBindings: Set<string>,
nonLocalWriteBindings: Set<string>
): void {
let environmentBindings = record.bindings;
for (let bindingName of Object.keys(environmentBindings)) {
let binding = environmentBindings[bindingName];
invariant(binding !== undefined);
let readFound = nonLocalReadBindings.delete(bindingName);
let writeFound = readOnly !== undefined && readOnly === false && nonLocalWriteBindings.delete(bindingName);
// Check what undefined could mean here, besides absent binding
// #2446
let value = binding.value;
if (readFound && value !== undefined) {
computeFromValue(value);
}
if (readFound || writeFound) {
reachableBindings.add(binding);
}
}
}
function computeFromAbstractValue(value: AbstractValue): void {
if (value.kind === "conditional") {
// For a conditional, we only have to visit each case. Not the condition itself.
computeFromValue(value.args[1]);
computeFromValue(value.args[2]);
return;
}
if (value.values.isTop()) {
for (let arg of value.args) {
computeFromValue(arg);
}
} else {
// If we know which object this might be, then leak each of them.
for (let element of value.values.getElements()) {
computeFromValue(element);
}
}
}
function computeFromProxyValue(value: ProxyValue): void {
computeFromValue(value.$ProxyTarget);
computeFromValue(value.$ProxyHandler);
}
function computeFromValue(value: Value): void {
invariant(value !== undefined);
if (value instanceof EmptyValue || value instanceof PrimitiveValue) {
visit(value);
} else if (value instanceof AbstractValue) {
ifNotVisited(value, computeFromAbstractValue);
} else if (value instanceof FunctionValue) {
ifNotVisited(value, computeFromFunctionValue);
} else if (value instanceof ObjectValue) {
ifNotVisited(value, computeFromObjectValue);
} else if (value instanceof ProxyValue) {
ifNotVisited(value, computeFromProxyValue);
}
}
function computeFromObjectValue(value: Value): void {
invariant(value instanceof ObjectValue);
let kind = value.getKind();
computeFromObjectProperties(value, kind);
switch (kind) {
case "RegExp":
case "Number":
case "String":
case "Boolean":
case "ReactElement":
case "ArrayBuffer":
case "Array":
break;
case "Date":
let dateValue = value.$DateValue;
invariant(dateValue !== undefined);
computeFromValue(dateValue);
break;
case "Float32Array":
case "Float64Array":
case "Int8Array":
case "Int16Array":
case "Int32Array":
case "Uint8Array":
case "Uint16Array":
case "Uint32Array":
case "Uint8ClampedArray":
case "DataView":
let buf = value.$ViewedArrayBuffer;
invariant(buf !== undefined);
computeFromValue(buf);
break;
case "Map":
case "WeakMap":
ifNotVisited(value, computeFromMap);
break;
case "Set":
case "WeakSet":
ifNotVisited(value, computeFromSet);
break;
default:
invariant(kind === "Object", `Object of kind ${kind} is not supported in calls to abstract functions.`);
invariant(
value.$ParameterMap === undefined,
`Arguments object is not supported in calls to abstract functions.`
);
break;
}
if (!reachableObjects.has(value)) reachableObjects.add(value);
}
function computeFromDescriptor(descriptor: Descriptor): void {
if (descriptor === undefined) {
} else if (descriptor instanceof PropertyDescriptor) {
if (descriptor.value !== undefined) computeFromValue(descriptor.value);
if (descriptor.get !== undefined) computeFromValue(descriptor.get);
if (descriptor.set !== undefined) computeFromValue(descriptor.set);
} else if (descriptor instanceof AbstractJoinedDescriptor) {
computeFromValue(descriptor.joinCondition);
if (descriptor.descriptor1 !== undefined) computeFromDescriptor(descriptor.descriptor1);
if (descriptor.descriptor2 !== undefined) computeFromDescriptor(descriptor.descriptor2);
} else {
invariant(false, "unknown descriptor");
}
}
function computeFromObjectPropertyBinding(binding: PropertyBinding): void {
let descriptor = binding.descriptor;
if (descriptor === undefined) return; //deleted
computeFromDescriptor(descriptor);
}
function computeFromObjectProperties(obj: ObjectValue, kind?: ObjectKind): void {
// symbol properties
for (let [, propertyBindingValue] of obj.symbols) {
invariant(propertyBindingValue);
computeFromObjectPropertyBinding(propertyBindingValue);
}
// string properties
for (let [, propertyBindingValue] of obj.properties) {
invariant(propertyBindingValue);
computeFromObjectPropertyBinding(propertyBindingValue);
}
// unkonwn property
if (obj.unknownProperty !== undefined && obj.unknownProperty.descriptor !== undefined) {
computeFromDescriptor(obj.unknownProperty.descriptor);
}
// prototype
if (obj.$Prototype !== undefined) computeFromObjectPrototype(obj);
}
function computeFromObjectPrototype(obj: ObjectValue) {
computeFromValue(obj.$Prototype);
}
function computeFromFunctionValue(fn: FunctionValue) {
computeFromObjectProperties(fn);
if (fn instanceof BoundFunctionValue) {
computeFromValue(fn.$BoundTargetFunction);
computeFromValue(fn.$BoundThis);
for (let boundArg of fn.$BoundArguments) computeFromValue(boundArg);
return;
}
invariant(
!(fn instanceof NativeFunctionValue),
"all native function values should have already been created outside this pure function"
);
let [nonLocalReadBindings, nonLocalWriteBindings] = parsedBindingsOfFunction(fn);
computeFromBindings(fn, nonLocalReadBindings, nonLocalWriteBindings);
}
function computeFromMap(val: ObjectValue): void {
let kind = val.getKind();
let entries;
if (kind === "Map") {
entries = val.$MapData;
} else {
invariant(kind === "WeakMap");
entries = val.$WeakMapData;
}
invariant(entries !== undefined);
let len = entries.length;
for (let i = 0; i < len; i++) {
let entry = entries[i];
let key = entry.$Key;
let value = entry.$Value;
if (key === undefined || value === undefined) continue;
computeFromValue(key);
computeFromValue(value);
}
}
function computeFromSet(val: ObjectValue): void {
let kind = val.getKind();
let entries;
if (kind === "Set") {
entries = val.$SetData;
} else {
invariant(kind === "WeakSet");
entries = val.$WeakSetData;
}
invariant(entries !== undefined);
let len = entries.length;
for (let i = 0; i < len; i++) {
let entry = entries[i];
if (entry === undefined) continue;
computeFromValue(entry);
}
}
function parsedBindingsOfFunction(func: FunctionValue): [Set<string>, Set<string>] {
let functionInfo = {
unboundReads: new Set(),
unboundWrites: new Set(),
};
invariant(func instanceof ECMAScriptSourceFunctionValue);
let formalParameters = func.$FormalParameters;
invariant(formalParameters != null);
let code = func.$ECMAScriptCode;
invariant(code != null);
FunctionBindingVisitor._readOnly = !!readOnly;
traverse(
t.file(t.program([t.expressionStatement(t.functionExpression(null, formalParameters, code))])),
FunctionBindingVisitor,
null,
functionInfo
);
traverse.cache.clear();
return [functionInfo.unboundReads, functionInfo.unboundWrites];
}
function ifNotVisited<T>(value: T, computeFrom: T => void): void {
if (!(value instanceof Value) || (filterValue(value) && !visitedValues.has(value))) {
visitedValues.add(value);
computeFrom(value);
}
}
function visit(value: Value): void {
visitedValues.add(value);
}
}
}

View File

@ -21,7 +21,7 @@ import {
Value,
} from "./index.js";
import { IsAccessorDescriptor, IsPropertyKey, IsArrayIndex } from "../methods/is.js";
import { Leak, Materialize, Properties, To, Utils } from "../singletons.js";
import { Leak, Reachability, Properties, To, Utils } from "../singletons.js";
import { type OperationDescriptor } from "../utils/generator.js";
import invariant from "../invariant.js";
import { NestedOptimizedFunctionSideEffect } from "../errors.js";
@ -150,9 +150,20 @@ function modelUnknownPropertyOfSpecializedArray(
let effects = array.nestedOptimizedFunctionEffects.get(funcToModel);
if (effects !== undefined) {
invariant(effects.result instanceof SimpleNormalCompletion);
let reachableObjects = Materialize.computeReachableObjects(realm, effects.result.value);
let objectsTrackedForLeaks = realm.createdObjectsTrackedForLeaks;
let filterValues = o =>
!(o instanceof ObjectValue) ||
(!effects.createdObjects.has(o) &&
(objectsTrackedForLeaks === undefined || objectsTrackedForLeaks.has(o)));
let [reachableObjects, reachableBindings] = Reachability.computeReachableObjectsAndBindings(
realm,
effects.result.value,
filterValues,
true /* readOnly */
);
invariant(reachableBindings !== undefined);
for (let reachableObject of reachableObjects) {
if (!effects.createdObjects.has(reachableObject)) mayAliasedObjects.add(reachableObject);
mayAliasedObjects.add(reachableObject);
}
}
}