Skip to content

Commit dde0022

Browse files
committed
feat(ses,pass-style): use no-trapping integrity level for safety
1 parent f7d527c commit dde0022

16 files changed

+108
-35
lines changed

packages/eventual-send/src/E.js

+12-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { trackTurns } from './track-turns.js';
22
import { makeMessageBreakpointTester } from './message-breakpoints.js';
33

44
const { details: X, quote: q, Fail, error: makeError } = assert;
5-
const { assign, create } = Object;
5+
const { assign, create, freeze } = Object;
66

77
/**
88
* @import { HandledPromiseConstructor } from './types.js';
@@ -171,7 +171,14 @@ const makeEGetProxyHandler = (x, HandledPromise) =>
171171
* @param {HandledPromiseConstructor} HandledPromise
172172
*/
173173
const makeE = HandledPromise => {
174-
return harden(
174+
// Note the use of `freeze` rather than `harden` below. This is because
175+
// `harden` now implies no-trapping, and we depend on proxies with these
176+
// almost-empty targets to remain trapping for traps `get`, `apply`, and `set`
177+
// which can still be interesting even when the target is frozen.
178+
// `get` and `has`, if not naming an own property, are still general traps,
179+
// which we rely on. `apply`, surprisingly perhaps, is free to ignore the
180+
// target's call behavior and just do its own thing instead.
181+
return freeze(
175182
assign(
176183
/**
177184
* E(x) returns a proxy on which you can call arbitrary methods. Each of these
@@ -183,7 +190,7 @@ const makeE = HandledPromise => {
183190
* @returns {ECallableOrMethods<RemoteFunctions<T>>} method/function call proxy
184191
*/
185192
// @ts-expect-error XXX typedef
186-
x => harden(new Proxy(() => {}, makeEProxyHandler(x, HandledPromise))),
193+
x => freeze(new Proxy(() => {}, makeEProxyHandler(x, HandledPromise))),
187194
{
188195
/**
189196
* E.get(x) returns a proxy on which you can get arbitrary properties.
@@ -198,7 +205,7 @@ const makeE = HandledPromise => {
198205
*/
199206
get: x =>
200207
// @ts-expect-error XXX typedef
201-
harden(
208+
freeze(
202209
new Proxy(create(null), makeEGetProxyHandler(x, HandledPromise)),
203210
),
204211

@@ -224,7 +231,7 @@ const makeE = HandledPromise => {
224231
*/
225232
sendOnly: x =>
226233
// @ts-expect-error XXX typedef
227-
harden(
234+
freeze(
228235
new Proxy(() => {}, makeESendOnlyProxyHandler(x, HandledPromise)),
229236
),
230237

packages/exo/src/exo-makers.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ import { defendPrototype, defendPrototypeKit } from './exo-tools.js';
88
* @import {Amplify, ExoClassKitMethods, ExoClassMethods, FarClassOptions, Guarded, GuardedKit, ExoClassInterfaceGuardKit, IsInstance, KitContext, ExoClassInterfaceGuard, Methods, FacetName} from './types.js';
99
*/
1010

11-
const { create, seal, freeze, defineProperty, values } = Object;
11+
const {
12+
create,
13+
seal,
14+
defineProperty,
15+
values,
16+
// @ts-expect-error TS doesn't know this is on ObjectConstructor
17+
suppressTrapping,
18+
} = Object;
1219

1320
// Turn on to give each exo instance its own toStringTag value.
1421
const LABEL_INSTANCES = environmentOptionsListHas('DEBUG', 'label-instances');
@@ -92,7 +99,7 @@ export const defineExoClass = (
9299

93100
// Be careful not to freeze the state record
94101
/** @type {import('./types.js').ClassContext<ReturnType<I>,M>} */
95-
const context = freeze({ state, self });
102+
const context = suppressTrapping({ state, self });
96103
contextMap.set(self, context);
97104
if (finish) {
98105
finish(context);
@@ -173,7 +180,7 @@ export const defineExoClassKit = (
173180
});
174181
context.facets = facets;
175182
// Be careful not to freeze the state record
176-
freeze(context);
183+
suppressTrapping(context);
177184
if (finish) {
178185
finish(context);
179186
}

packages/far/test/marshal-far-function.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ test('Data can contain far functions', t => {
5858
const arrow = Far('arrow', a => a + 1);
5959
t.is(passStyleOf(harden({ x: 8, foo: arrow })), 'copyRecord');
6060
const mightBeMethod = a => a + 1;
61-
t.throws(() => passStyleOf(freeze({ x: 8, foo: mightBeMethod })), {
61+
t.throws(() => passStyleOf(harden({ x: 8, foo: mightBeMethod })), {
6262
message: /Remotables with non-methods like "x" /,
6363
});
6464
});

packages/marshal/src/encodeToCapData.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ const {
3030
is,
3131
entries,
3232
fromEntries,
33-
freeze,
33+
// @ts-expect-error TS doesn't know this is on ObjectConstructor
34+
suppressTrapping,
3435
} = Object;
3536

3637
/**
@@ -176,10 +177,10 @@ export const makeEncodeToCapData = (encodeOptions = {}) => {
176177
// We harden the entire capData encoding before we return it.
177178
// `encodeToCapData` requires that its input be Passable, and
178179
// therefore hardened.
179-
// The `freeze` here is needed anyway, because the `rest` is
180+
// The `suppressTrapping` here is needed anyway, because the `rest` is
180181
// freshly constructed by the `...` above, and we're using it
181182
// as imput in another call to `encodeToCapData`.
182-
result.rest = encodeToCapDataRecur(freeze(rest));
183+
result.rest = encodeToCapDataRecur(suppressTrapping(rest));
183184
}
184185
return result;
185186
}

packages/marshal/src/marshal-stringify.js

+16-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { makeMarshal } from './marshal.js';
55

66
/** @import {Passable} from '@endo/pass-style' */
77

8+
const { freeze } = Object;
9+
810
/** @type {import('./types.js').ConvertValToSlot<any>} */
911
const doNotConvertValToSlot = val =>
1012
Fail`Marshal's stringify rejects presences and promises ${val}`;
@@ -23,7 +25,13 @@ const badArrayHandler = harden({
2325
},
2426
});
2527

26-
const badArray = harden(new Proxy(harden([]), badArrayHandler));
28+
// Note the use of `freeze` rather than `harden` below. This is because
29+
// `harden` now implies no-trapping, and we depend on proxies with these
30+
// almost-empty targets to remain trapping for the `get` trap
31+
// which can still be interesting even when the target is frozen.
32+
// `get`, if not naming an own property, are still general traps,
33+
// which we rely on.
34+
const badArray = freeze(new Proxy(freeze([]), badArrayHandler));
2735

2836
const { serialize, unserialize } = makeMarshal(
2937
doNotConvertValToSlot,
@@ -48,7 +56,13 @@ harden(stringify);
4856
*/
4957
const parse = str =>
5058
unserialize(
51-
harden({
59+
// Note the use of `freeze` rather than `harden` below. This is because
60+
// `harden` now implies no-trapping, and we depend on proxies with these
61+
// almost-empty targets to remain trapping for the `get` trap
62+
// which can still be interesting even when the target is frozen.
63+
// `get`, if not naming an own property, are still general traps,
64+
// which we rely on.
65+
freeze({
5266
body: str,
5367
slots: badArray,
5468
}),

packages/marshal/test/marshal-far-function.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ test('Data can contain far functions', t => {
6060
const arrow = Far('arrow', a => a + 1);
6161
t.is(passStyleOf(harden({ x: 8, foo: arrow })), 'copyRecord');
6262
const mightBeMethod = a => a + 1;
63-
t.throws(() => passStyleOf(freeze({ x: 8, foo: mightBeMethod })), {
63+
t.throws(() => passStyleOf(harden({ x: 8, foo: mightBeMethod })), {
6464
message: /Remotables with non-methods like "x" /,
6565
});
6666
});

packages/pass-style/src/passStyle-helpers.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ const {
1111
getOwnPropertyDescriptor,
1212
getPrototypeOf,
1313
hasOwnProperty: objectHasOwnProperty,
14-
isFrozen,
1514
prototype: objectPrototype,
15+
// @ts-expect-error TS does not yet have `isNoTrapping` on ObjectConstructor
16+
isNoTrapping,
1617
} = Object;
1718
const { apply } = Reflect;
1819
const { toStringTag: toStringTagSymbol } = Symbol;
@@ -165,7 +166,7 @@ const makeCheckTagRecord = checkProto => {
165166
(isObject(tagRecord) ||
166167
(!!check &&
167168
CX(check)`A non-object cannot be a tagRecord: ${tagRecord}`)) &&
168-
(isFrozen(tagRecord) ||
169+
(isNoTrapping(tagRecord) ||
169170
(!!check && CX(check)`A tagRecord must be frozen: ${tagRecord}`)) &&
170171
(!isArray(tagRecord) ||
171172
(!!check && CX(check)`An array cannot be a tagRecord: ${tagRecord}`)) &&

packages/pass-style/src/passStyleOf.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ import { assertPassableString } from './string.js';
3131
/** @typedef {Exclude<PassStyle, PrimitiveStyle | "promise">} HelperPassStyle */
3232

3333
const { ownKeys } = Reflect;
34-
const { isFrozen, getOwnPropertyDescriptors, values } = Object;
34+
const {
35+
getOwnPropertyDescriptors,
36+
values,
37+
// @ts-expect-error TS does not yet have `isNoTrapping` on ObjectConstructor
38+
isNoTrapping,
39+
} = Object;
3540

3641
/**
3742
* @param {PassStyleHelper[]} passStyleHelpers
@@ -143,7 +148,7 @@ const makePassStyleOf = passStyleHelpers => {
143148
if (inner === null) {
144149
return 'null';
145150
}
146-
if (!isFrozen(inner)) {
151+
if (!isNoTrapping(inner)) {
147152
assert.fail(
148153
// TypedArrays get special treatment in harden()
149154
// and a corresponding special error message here.
@@ -177,7 +182,7 @@ const makePassStyleOf = passStyleHelpers => {
177182
return 'remotable';
178183
}
179184
case 'function': {
180-
isFrozen(inner) ||
185+
isNoTrapping(inner) ||
181186
Fail`Cannot pass non-frozen objects like ${inner}. Use harden()`;
182187
typeof inner.then !== 'function' ||
183188
Fail`Cannot pass non-promise thenables`;

packages/pass-style/src/remotable.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ const { ownKeys } = Reflect;
2424
const { isArray } = Array;
2525
const {
2626
getPrototypeOf,
27-
isFrozen,
2827
prototype: objectPrototype,
2928
getOwnPropertyDescriptors,
29+
// @ts-expect-error TS does not yet have `isNoTrapping` on ObjectConstructor
30+
isNoTrapping,
3031
} = Object;
3132

3233
/**
@@ -154,7 +155,7 @@ const checkRemotable = (val, check) => {
154155
if (confirmedRemotables.has(val)) {
155156
return true;
156157
}
157-
if (!isFrozen(val)) {
158+
if (!isNoTrapping(val)) {
158159
return (
159160
!!check && CX(check)`cannot serialize non-frozen objects like ${val}`
160161
);

packages/pass-style/src/safe-promise.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { assertChecker, hasOwnPropertyOf, CX } from './passStyle-helpers.js';
66

77
/** @import {Checker} from './types.js' */
88

9-
const { isFrozen, getPrototypeOf, getOwnPropertyDescriptor } = Object;
9+
const {
10+
getPrototypeOf,
11+
getOwnPropertyDescriptor,
12+
// @ts-expect-error TS does not yet have `isNoTrapping` on ObjectConstructor
13+
isNoTrapping,
14+
} = Object;
1015
const { ownKeys } = Reflect;
1116
const { toStringTag } = Symbol;
1217

@@ -88,7 +93,7 @@ const checkPromiseOwnKeys = (pr, check) => {
8893
if (
8994
typeof val === 'object' &&
9095
val !== null &&
91-
isFrozen(val) &&
96+
isNoTrapping(val) &&
9297
getPrototypeOf(val) === Object.prototype
9398
) {
9499
const subKeys = ownKeys(val);
@@ -132,7 +137,7 @@ const checkPromiseOwnKeys = (pr, check) => {
132137
*/
133138
const checkSafePromise = (pr, check) => {
134139
return (
135-
(isFrozen(pr) || CX(check)`${pr} - Must be frozen`) &&
140+
(isNoTrapping(pr) || CX(check)`${pr} - Must be frozen`) &&
136141
(isPromise(pr) || CX(check)`${pr} - Must be a promise`) &&
137142
(getPrototypeOf(pr) === Promise.prototype ||
138143
CX(check)`${pr} - Must inherit from Promise.prototype: ${q(

packages/pass-style/test/passStyleOf.test.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const harden = /** @type {import('ses').Harden & { isFake?: boolean }} */ (
1313
global.harden
1414
);
1515

16-
const { getPrototypeOf, defineProperty } = Object;
16+
const { getPrototypeOf, defineProperty, suppressTrapping } = Object;
1717
const { ownKeys } = Reflect;
1818

1919
test('passStyleOf basic success cases', t => {
@@ -200,7 +200,7 @@ test('passStyleOf testing remotables', t => {
200200

201201
const tagRecord2 = makeTagishRecord('Alleged: tagRecord not hardened');
202202
/** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */
203-
const farObj2 = Object.freeze({
203+
const farObj2 = suppressTrapping({
204204
__proto__: tagRecord2,
205205
});
206206
if (harden.isFake) {
@@ -212,11 +212,11 @@ test('passStyleOf testing remotables', t => {
212212
});
213213
}
214214

215-
const tagRecord3 = Object.freeze(
215+
const tagRecord3 = suppressTrapping(
216216
makeTagishRecord('Alleged: both manually frozen'),
217217
);
218218
/** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */
219-
const farObj3 = Object.freeze({
219+
const farObj3 = suppressTrapping({
220220
__proto__: tagRecord3,
221221
});
222222
t.is(passStyleOf(farObj3), 'remotable');
@@ -387,7 +387,7 @@ test('remotables - safety from the gibson042 attack', t => {
387387
},
388388
);
389389

390-
const makeInput = () => Object.freeze({ __proto__: mercurialProto });
390+
const makeInput = () => suppressTrapping({ __proto__: mercurialProto });
391391
const input1 = makeInput();
392392
const input2 = makeInput();
393393

packages/ses/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@
8585
"postpack": "git clean -f '*.d.ts*' '*.tsbuildinfo'"
8686
},
8787
"dependencies": {
88-
"@endo/env-options": "workspace:^"
88+
"@endo/env-options": "workspace:^",
89+
"@endo/no-trapping-shim": "^0.1.0"
8990
},
9091
"devDependencies": {
9192
"@endo/compartment-mapper": "workspace:^",

packages/ses/src/commons.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
/* global globalThis */
1515
/* eslint-disable no-restricted-globals */
1616

17+
import '@endo/no-trapping-shim/shim.js';
18+
1719
// We cannot use globalThis as the local name since it would capture the
1820
// lexical name.
1921
const universalThis = globalThis;
@@ -75,6 +77,11 @@ export const {
7577
setPrototypeOf,
7678
values,
7779
fromEntries,
80+
// https://github.com/endojs/endo/pull/2673
81+
// @ts-expect-error TS does not yet have this on ObjectConstructor.
82+
isNoTrapping,
83+
// @ts-expect-error TS does not yet have this on ObjectConstructor.
84+
suppressTrapping,
7885
} = Object;
7986

8087
export const {
@@ -125,6 +132,11 @@ export const {
125132
ownKeys,
126133
preventExtensions: reflectPreventExtensions,
127134
set: reflectSet,
135+
// https://github.com/endojs/endo/pull/2673
136+
// @ts-expect-error TS does not yet have this on typeof Reflect.
137+
isNoTrapping: reflectIsNoTrapping,
138+
// @ts-expect-error TS does not yet have this on typeof Reflect.
139+
suppressTrapping: reflectSuppressTrapping,
128140
} = Reflect;
129141

130142
export const { isArray, prototype: arrayPrototype } = Array;
@@ -273,7 +285,7 @@ export const getConstructorOf = fn =>
273285
* immutableObject
274286
* An immutable (frozen) empty object that is safe to share.
275287
*/
276-
export const immutableObject = freeze(create(null));
288+
export const immutableObject = suppressTrapping(create(null));
277289

278290
/**
279291
* isObject tests whether a value is an object.

packages/ses/src/make-hardener.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import {
3030
apply,
3131
arrayForEach,
3232
defineProperty,
33-
freeze,
3433
getOwnPropertyDescriptor,
3534
getOwnPropertyDescriptors,
3635
getPrototypeOf,
@@ -49,6 +48,8 @@ import {
4948
FERAL_STACK_GETTER,
5049
FERAL_STACK_SETTER,
5150
isError,
51+
isFrozen,
52+
suppressTrapping,
5253
} from './commons.js';
5354
import { assert } from './error/assert.js';
5455

@@ -182,8 +183,17 @@ export const makeHardener = () => {
182183
// Also throws if the object is an ArrayBuffer or any TypedArray.
183184
if (isTypedArray(obj)) {
184185
freezeTypedArray(obj);
186+
if (isFrozen(obj)) {
187+
// After `freezeTypedArray`, the typed array might actually be
188+
// frozen if
189+
// - it has no indexed properties
190+
// - it is backed by an Immutable ArrayBuffer as proposed.
191+
// In either case, this makes it a candidate to be made
192+
// non-trapping.
193+
suppressTrapping(obj);
194+
}
185195
} else {
186-
freeze(obj);
196+
suppressTrapping(obj);
187197
}
188198

189199
// we rely upon certain commitments of Object.freeze and proxies here

0 commit comments

Comments
 (0)