Skip to content

Commit 2cd4a04

Browse files
warnerjfparadis
authored andcommitted
add endowments as second argument to r.evaluate
1 parent 886977b commit 2cd4a04

File tree

4 files changed

+128
-61
lines changed

4 files changed

+128
-61
lines changed

shim/src/evaluators.js

+61-55
Original file line numberDiff line numberDiff line change
@@ -108,73 +108,79 @@ function createScopedEvaluatorFactory(unsafeRec, constants) {
108108
`);
109109
}
110110

111-
export function createSafeEvaluator(unsafeRec, safeGlobal) {
111+
export function createSafeEvaluatorFactory(unsafeRec, safeGlobal) {
112112
const { unsafeFunction } = unsafeRec;
113113

114-
// This proxy has several functions:
115-
// 1. works with the sentinel to alternate between direct eval and confined eval.
116-
// 2. shadows all properties of the unsafe global by declaring them as undefined.
117-
// 3. resolves all existing properties of the safe global.
118-
// 4. uses an empty object as the target, with the safe global as its prototype,
119-
// to bypass a proxy invariant that would prevent alternating between different
120-
// values of eval if the user was to freeze the eval property on the safe global.
121114
const scopeHandler = new ScopeHandler(unsafeRec);
122-
const scopeTarget = create(safeGlobal);
123-
const scopeProxy = new Proxy(scopeTarget, scopeHandler);
124-
125115
const optimizableGlobals = getOptimizableGlobals(safeGlobal);
126116
const scopedEvaluatorFactory = createScopedEvaluatorFactory(unsafeRec, optimizableGlobals);
127-
const scopedEvaluator = scopedEvaluatorFactory(scopeProxy);
128-
129-
// We use the the concise method syntax to create an eval without a
130-
// [[Construct]] behavior (such that the invocation "new eval()" throws
131-
// TypeError: eval is not a constructor"), but which still accepts a 'this'
132-
// binding.
133-
const safeEval = {
134-
eval(src) {
135-
src = `${src}`;
136-
rejectImportExpressions(src);
137-
scopeHandler.useUnsafeEvaluator = true;
138-
let err;
139-
try {
140-
// Ensure that "this" resolves to the safe global.
141-
return apply(scopedEvaluator, safeGlobal, [src]);
142-
} catch (e) {
143-
// stash the child-code error in hopes of debugging the internal failure
144-
err = e;
145-
throw e;
146-
} finally {
147-
// belt and suspenders: the proxy switches this off immediately after
148-
// the first access, but just in case we clear it here too
149-
if (scopeHandler.useUnsafeEvaluator !== false) {
150-
scopeHandler.useUnsafeEvaluator = false;
151-
throwTantrum('handler sets useUnsafeEvaluator = false', err);
117+
118+
function factory(endowments) {
119+
const scopeTarget = create(safeGlobal, getOwnPropertyDescriptors(endowments));
120+
const scopeProxy = new Proxy(scopeTarget, scopeHandler);
121+
const scopedEvaluator = scopedEvaluatorFactory(scopeProxy);
122+
123+
// We use the the concise method syntax to create an eval without a
124+
// [[Construct]] behavior (such that the invocation "new eval()" throws
125+
// TypeError: eval is not a constructor"), but which still accepts a
126+
// 'this' binding.
127+
const safeEval = {
128+
eval(src) {
129+
src = `${src}`;
130+
rejectImportExpressions(src);
131+
scopeHandler.useUnsafeEvaluator = true;
132+
let err;
133+
try {
134+
// Ensure that "this" resolves to the safe global.
135+
return apply(scopedEvaluator, safeGlobal, [src]);
136+
} catch (e) {
137+
// stash the child-code error in hopes of debugging the internal failure
138+
err = e;
139+
throw e;
140+
} finally {
141+
// belt and suspenders: the proxy switches this off immediately after
142+
// the first access, but just in case we clear it here too
143+
if (scopeHandler.useUnsafeEvaluator !== false) {
144+
scopeHandler.useUnsafeEvaluator = false;
145+
throwTantrum('handler sets useUnsafeEvaluator = false', err);
146+
}
152147
}
153148
}
154-
}
155-
}.eval;
149+
}.eval;
156150

157-
// safeEval's prototype is currently the primal realm's Function.prototype,
158-
// which we must not let escape. To make 'eval instanceof Function' be true
159-
// inside the realm, we need to point it at the RootRealm's value.
151+
// safeEval's prototype is currently the primal realm's
152+
// Function.prototype, which we must not let escape. To make 'eval
153+
// instanceof Function' be true inside the realm, we need to point it at
154+
// the RootRealm's value.
160155

161-
// Ensure that eval from any compartment in a root realm is an
162-
// instance of Function in any compartment of the same root realm.
163-
setPrototypeOf(safeEval, unsafeFunction.prototype);
156+
// Ensure that eval from any compartment in a root realm is an instance
157+
// of Function in any compartment of the same root realm.
158+
setPrototypeOf(safeEval, unsafeFunction.prototype);
164159

165-
assert(getPrototypeOf(safeEval).constructor !== Function, 'hide Function');
166-
assert(getPrototypeOf(safeEval).constructor !== unsafeFunction, 'hide unsafeFunction');
160+
assert(getPrototypeOf(safeEval).constructor !== Function, 'hide Function');
161+
assert(getPrototypeOf(safeEval).constructor !== unsafeFunction, 'hide unsafeFunction');
167162

168-
// note: be careful to not leak our primal Function.prototype by setting
169-
// this to a plain arrow function. Now that we have safeEval, use it.
170-
defineProperty(safeEval, 'toString', {
171-
value: safeEval("() => 'function eval() { [shim code] }'"),
172-
writable: false,
173-
enumerable: false,
174-
configurable: true
175-
});
163+
// note: be careful to not leak our primal Function.prototype by setting
164+
// this to a plain arrow function. Now that we have safeEval, use it.
165+
defineProperty(safeEval, 'toString', {
166+
value: safeEval("() => 'function eval() { [shim code] }'"),
167+
writable: false,
168+
enumerable: false,
169+
configurable: true
170+
});
171+
172+
return safeEval;
173+
}
174+
175+
return factory;
176+
}
177+
178+
export function createSafeEvaluator(safeEvaluatorFactory) {
179+
return safeEvaluatorFactory({});
180+
}
176181

177-
return safeEval;
182+
export function createSafeEvaluatorWhichTakesEndowments(safeEvaluatorFactory) {
183+
return (x, endowments) => safeEvaluatorFactory(endowments)(x);
178184
}
179185

180186
/**

shim/src/realm.js

+18-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { createRealmFacade } from './realmFacade';
22
import { createNewUnsafeRec, createCurrentUnsafeRec } from './unsafeRec';
3-
import { createSafeEvaluator, createFunctionEvaluator } from './evaluators';
3+
import {
4+
createSafeEvaluatorFactory,
5+
createSafeEvaluator,
6+
createSafeEvaluatorWhichTakesEndowments,
7+
createFunctionEvaluator
8+
} from './evaluators';
49
import { create, defineProperty, defineProperties, freeze, arrayConcat } from './commons';
510

611
// Create a registry to mimic a private static members on the realm classes.
@@ -88,14 +93,19 @@ function createRealmRec(unsafeRec) {
8893
const { sharedGlobalDescs, unsafeGlobal } = unsafeRec;
8994

9095
const safeGlobal = create(unsafeGlobal.Object.prototype);
91-
const safeEval = createSafeEvaluator(unsafeRec, safeGlobal);
96+
const safeEvaluatorFactory = createSafeEvaluatorFactory(unsafeRec, safeGlobal);
97+
const safeEval = createSafeEvaluator(safeEvaluatorFactory);
98+
const safeEvalWhichTakesEndowments = createSafeEvaluatorWhichTakesEndowments(
99+
safeEvaluatorFactory
100+
);
92101
const safeFunction = createFunctionEvaluator(unsafeRec, safeEval);
93102

94103
setDefaultBindings(sharedGlobalDescs, safeGlobal, safeEval, safeFunction);
95104

96105
const realmRec = freeze({
97106
safeGlobal,
98107
safeEval,
108+
safeEvalWhichTakesEndowments,
99109
safeFunction
100110
});
101111

@@ -153,9 +163,12 @@ function getRealmGlobal(self) {
153163
return safeGlobal;
154164
}
155165

156-
function realmEvaluate(self, x) {
157-
const { safeEval } = getRealmRecForRealmInstance(self);
158-
return safeEval(x);
166+
function realmEvaluate(self, x, endowments = {}) {
167+
// todo: don't pass in primal-realm objects like {}, for safety. OTOH its
168+
// properties are copied onto the new global 'target'.
169+
// todo: figure out a way to membrane away the contents to safety.
170+
const { safeEvalWhichTakesEndowments } = getRealmRecForRealmInstance(self);
171+
return safeEvalWhichTakesEndowments(x, endowments);
159172
}
160173

161174
// Define Realm onto new sharedGlobalDescs, so it can be defined in the

shim/src/scopeHandler.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
// * hide the unsafeGlobal which lives on the scope chain above the 'with'
1212
// * ensure the Proxy invariants despite some global properties being frozen
1313

14-
import { getPrototypeOf } from './commons';
14+
import { getPrototypeOf, objectHasOwnProperty } from './commons';
1515

1616
export class ScopeHandler {
1717
// Properties stored on the handler are not available from the proxy.
@@ -49,6 +49,7 @@ export class ScopeHandler {
4949
return target.eval;
5050
}
5151

52+
// todo: shim integrity, capture Symbol.unscopables
5253
if (prop === Symbol.unscopables) {
5354
// Safe to return a primal realm Object here because the only code that
5455
// can do a get() on a non-string is the internals of with() itself,
@@ -70,6 +71,10 @@ export class ScopeHandler {
7071
// Set the value on the shadow. The target itself is an empty
7172
// object that is only used to prevent a froxen eval property.
7273
// todo: use this.shadowTarget, for speedup
74+
if (objectHasOwnProperty(target, prop)) {
75+
// todo: shim integrity: TypeError, String
76+
throw new TypeError(`do not modify endowments like ${String(prop)}`);
77+
}
7378
getPrototypeOf(target)[prop] = value;
7479
// Return true after successful set.
7580
return true;

shim/test/realm/test-endowments.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import test from 'tape';
2+
import Realm from '../../src/realm';
3+
4+
test('eval-with-endowments', t => {
5+
const r = Realm.makeRootRealm();
6+
t.equal(r.evaluate(`endow1 + 2`, { endow1: 1 }), 3);
7+
8+
t.end();
9+
});
10+
11+
test('endowments are not shared between calls to r.evaluate', t => {
12+
const r = Realm.makeRootRealm();
13+
t.equal(r.evaluate(`4`, { endow1: 1 }), 4);
14+
t.throws(() => r.evaluate(`endow1`), ReferenceError);
15+
t.throws(() => r.evaluate(`endow2`), ReferenceError);
16+
17+
t.end();
18+
});
19+
20+
test('endowments are mutable but not shared between calls to r.evaluate', t => {
21+
const r = Realm.makeRootRealm();
22+
// fixed a bug: the Handlers 'get' finds the property on the target (which
23+
// has the endowments), the subsequent 'set' modifies it on the safeGlobal
24+
// (via getPrototypeOf(target)), then the next 'get' pulls the original
25+
// value from the target again
26+
// t.equal(r.evaluate(`endow1 = 4; endow1`, { endow1: 1 }), 4);
27+
28+
// we fixed this by rejecting assignments to endowments
29+
t.throws(() => r.evaluate(`endow1 = 4`, { endow1: 1 }), TypeError);
30+
t.throws(() => r.evaluate(`endow1 += 4`, { endow1: 1 }), TypeError);
31+
t.throws(() => r.evaluate(`endow1`), ReferenceError);
32+
33+
// assignment to global works even when an endowment shadows it
34+
t.equal(r.evaluate(`this.endow1 = 4; this.endow1`, { endow1: 1 }), 4);
35+
36+
// the modified global is now visible when there is no endowment to shadow it
37+
t.equal(r.evaluate(`endow1`), 4);
38+
39+
// endowments shadow globals
40+
t.equal(r.evaluate(`endow1`, { endow1: 44 }), 44);
41+
42+
t.end();
43+
});

0 commit comments

Comments
 (0)