Skip to content

Commit 9925c0c

Browse files
committed
refactor: prepare for non-trapping integrity trait
1 parent b23724f commit 9925c0c

11 files changed

+119
-60
lines changed

packages/captp/src/trap.js

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Lifted mostly from `@endo/eventual-send/src/E.js`.
22

3+
const { freeze } = Object;
4+
35
/**
46
* Default implementation of Trap for near objects.
57
*
@@ -62,11 +64,21 @@ const TrapProxyHandler = (x, trapImpl) => {
6264
*/
6365
export const makeTrap = trapImpl => {
6466
const Trap = x => {
67+
/**
68+
* `freeze` but not `harden` the proxy target so it remains trapping.
69+
* @see https://github.com/endojs/endo/blob/master/packages/ses/docs/preparing-for-stabilize.md
70+
*/
71+
const target = freeze(() => {});
6572
const handler = TrapProxyHandler(x, trapImpl);
66-
return harden(new Proxy(() => {}, handler));
73+
return new Proxy(target, handler);
6774
};
6875

6976
const makeTrapGetterProxy = x => {
77+
/**
78+
* `freeze` but not `harden` the proxy target so it remains trapping.
79+
* @see https://github.com/endojs/endo/blob/master/packages/ses/docs/preparing-for-stabilize.md
80+
*/
81+
const target = freeze(Object.create(null));
7082
const handler = harden({
7183
...baseFreezableProxyHandler,
7284
has(_target, _prop) {
@@ -77,7 +89,7 @@ export const makeTrap = trapImpl => {
7789
return trapImpl.get(x, prop);
7890
},
7991
});
80-
return new Proxy(Object.create(null), handler);
92+
return new Proxy(target, handler);
8193
};
8294
Trap.get = makeTrapGetterProxy;
8395

packages/eventual-send/src/E.js

+15-10
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,6 +171,16 @@ const makeEGetProxyHandler = (x, HandledPromise) =>
171171
* @param {HandledPromiseConstructor} HandledPromise
172172
*/
173173
const makeE = HandledPromise => {
174+
/**
175+
* `freeze` but not `harden` the proxy target so it remains trapping.
176+
* @see https://github.com/endojs/endo/blob/master/packages/ses/docs/preparing-for-stabilize.md
177+
*/
178+
const funcTarget = freeze(() => {});
179+
/**
180+
* `freeze` but not `harden` the proxy target so it remains trapping.
181+
* @see https://github.com/endojs/endo/blob/master/packages/ses/docs/preparing-for-stabilize.md
182+
*/
183+
const objTarget = freeze(create(null));
174184
return harden(
175185
assign(
176186
/**
@@ -183,7 +193,7 @@ const makeE = HandledPromise => {
183193
* @returns {ECallableOrMethods<RemoteFunctions<T>>} method/function call proxy
184194
*/
185195
// @ts-expect-error XXX typedef
186-
x => harden(new Proxy(() => {}, makeEProxyHandler(x, HandledPromise))),
196+
x => new Proxy(funcTarget, makeEProxyHandler(x, HandledPromise)),
187197
{
188198
/**
189199
* E.get(x) returns a proxy on which you can get arbitrary properties.
@@ -196,11 +206,8 @@ const makeE = HandledPromise => {
196206
* @returns {EGetters<LocalRecord<T>>} property get proxy
197207
* @readonly
198208
*/
199-
get: x =>
200-
// @ts-expect-error XXX typedef
201-
harden(
202-
new Proxy(create(null), makeEGetProxyHandler(x, HandledPromise)),
203-
),
209+
// @ts-expect-error XXX typedef
210+
get: x => new Proxy(objTarget, makeEGetProxyHandler(x, HandledPromise)),
204211

205212
/**
206213
* E.resolve(x) converts x to a handled promise. It is
@@ -224,9 +231,7 @@ const makeE = HandledPromise => {
224231
*/
225232
sendOnly: x =>
226233
// @ts-expect-error XXX typedef
227-
harden(
228-
new Proxy(() => {}, makeESendOnlyProxyHandler(x, HandledPromise)),
229-
),
234+
new Proxy(funcTarget, makeESendOnlyProxyHandler(x, HandledPromise)),
230235

231236
/**
232237
* E.when(x, res, rej) is equivalent to

packages/eventual-send/src/handled-promise.js

+3
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,9 @@ export const makeHandledPromise = () => {
309309
if (proxyOpts) {
310310
const {
311311
handler: proxyHandler,
312+
// The proxy target can be frozen but should not be hardened
313+
// so it remains trapping.
314+
// See https://github.com/endojs/endo/blob/master/packages/ses/docs/preparing-for-stabilize.md
312315
target: proxyTarget,
313316
revokerCallback,
314317
} = proxyOpts;

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/marshal-stringify.js

+12-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,12 @@ const badArrayHandler = harden({
2325
},
2426
});
2527

26-
const badArray = harden(new Proxy(harden([]), badArrayHandler));
28+
/**
29+
* `freeze` but not `harden` the proxy target so it remains trapping.
30+
* @see https://github.com/endojs/endo/blob/master/packages/ses/docs/preparing-for-stabilize.md
31+
*/
32+
const arrayTarget = freeze(/** @type {any[]} */ ([]));
33+
const badArray = new Proxy(arrayTarget, badArrayHandler);
2734

2835
const { serialize, unserialize } = makeMarshal(
2936
doNotConvertValToSlot,
@@ -48,7 +55,10 @@ harden(stringify);
4855
*/
4956
const parse = str =>
5057
unserialize(
51-
harden({
58+
// `freeze` but not `harden` since the `badArray` proxy and its target
59+
// must remain trapping.
60+
// See https://github.com/endojs/endo/blob/master/packages/ses/docs/preparing-for-stabilize.md
61+
freeze({
5262
body: str,
5363
slots: badArray,
5464
}),

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/test/passStyleOf.test.js

+32-33
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, freeze } = Object;
1717
const { ownKeys } = Reflect;
1818

1919
test('passStyleOf basic success cases', t => {
@@ -193,16 +193,22 @@ test('passStyleOf testing remotables', t => {
193193

194194
const tagRecord1 = harden(makeTagishRecord('Alleged: manually constructed'));
195195
/** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */
196-
const farObj1 = harden({
197-
__proto__: tagRecord1,
198-
});
196+
const farObj1 = harden({ __proto__: tagRecord1 });
199197
t.is(passStyleOf(farObj1), 'remotable');
200198

201199
const tagRecord2 = makeTagishRecord('Alleged: tagRecord not hardened');
202-
/** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */
203-
const farObj2 = Object.freeze({
204-
__proto__: tagRecord2,
205-
});
200+
/**
201+
* TODO In order to run this test before we have explicit support for a
202+
* non-trapping integrity trait, we have to `freeze` here but not `harden`.
203+
* However, once we do have that support, and `passStyleOf` checks that
204+
* its argument is also non-trapping, we still need to avoid `harden`
205+
* because that would also hardden `__proto__`. So we will need to
206+
* explicitly make this non-trapping, which we cannot yet express.
207+
* @see https://github.com/endojs/endo/blob/master/packages/ses/docs/preparing-for-stabilize.md
208+
*
209+
* @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385
210+
*/
211+
const farObj2 = freeze({ __proto__: tagRecord2 });
206212
if (harden.isFake) {
207213
t.is(passStyleOf(farObj2), 'remotable');
208214
} else {
@@ -212,39 +218,27 @@ test('passStyleOf testing remotables', t => {
212218
});
213219
}
214220

215-
const tagRecord3 = Object.freeze(
216-
makeTagishRecord('Alleged: both manually frozen'),
217-
);
221+
const tagRecord3 = harden(makeTagishRecord('Alleged: both manually frozen'));
218222
/** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */
219-
const farObj3 = Object.freeze({
220-
__proto__: tagRecord3,
221-
});
223+
const farObj3 = harden({ __proto__: tagRecord3 });
222224
t.is(passStyleOf(farObj3), 'remotable');
223225

224226
const tagRecord4 = harden(makeTagishRecord('Remotable'));
225227
/** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */
226-
const farObj4 = harden({
227-
__proto__: tagRecord4,
228-
});
228+
const farObj4 = harden({ __proto__: tagRecord4 });
229229
t.is(passStyleOf(farObj4), 'remotable');
230230

231231
const tagRecord5 = harden(makeTagishRecord('Not alleging'));
232-
const farObj5 = harden({
233-
__proto__: tagRecord5,
234-
});
232+
const farObj5 = harden({ __proto__: tagRecord5 });
235233
t.throws(() => passStyleOf(farObj5), {
236234
message:
237235
/For now, iface "Not alleging" must be "Remotable" or begin with "Alleged: " or "DebugName: "; unimplemented/,
238236
});
239237

240238
const tagRecord6 = harden(makeTagishRecord('Alleged: manually constructed'));
241-
const farObjProto6 = harden({
242-
__proto__: tagRecord6,
243-
});
239+
const farObjProto6 = harden({ __proto__: tagRecord6 });
244240
/** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */
245-
const farObj6 = harden({
246-
__proto__: farObjProto6,
247-
});
241+
const farObj6 = harden({ __proto__: farObjProto6 });
248242
t.is(passStyleOf(farObj6), 'remotable', 'tagRecord grandproto is accepted');
249243

250244
// Our current agoric-sdk plans for far classes are to create a class-like
@@ -323,12 +317,8 @@ test('passStyleOf testing remotables', t => {
323317
const fauxTagRecordB = harden(
324318
makeTagishRecord('Alleged: manually constructed', harden({})),
325319
);
326-
const farObjProtoB = harden({
327-
__proto__: fauxTagRecordB,
328-
});
329-
const farObjB = harden({
330-
__proto__: farObjProtoB,
331-
});
320+
const farObjProtoB = harden({ __proto__: fauxTagRecordB });
321+
const farObjB = harden({ __proto__: farObjProtoB });
332322
t.throws(() => passStyleOf(farObjB), {
333323
message:
334324
'cannot serialize Remotables with non-methods like "Symbol(passStyle)" in "[Alleged: manually constructed]"',
@@ -387,7 +377,16 @@ test('remotables - safety from the gibson042 attack', t => {
387377
},
388378
);
389379

390-
const makeInput = () => Object.freeze({ __proto__: mercurialProto });
380+
/**
381+
* TODO In order to run this test before we have explicit support for a
382+
* non-trapping integrity trait, we have to `freeze` here but not `harden`.
383+
* However, once we do have that support, and `passStyleOf` checks that
384+
* its argument is also non-trapping, we still need to avoid `harden`
385+
* because that would also hardden `__proto__`. So we will need to
386+
* explicitly make this non-trapping, which we cannot yet express.
387+
* @see https://github.com/endojs/endo/blob/master/packages/ses/docs/preparing-for-stabilize.md
388+
*/
389+
const makeInput = () => freeze({ __proto__: mercurialProto });
391390
const input1 = makeInput();
392391
const input2 = makeInput();
393392

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Preparing for the Non-trapping Integrity Trait
2+
3+
The [Stabilize proposal](https://github.com/tc39/proposal-stabilize) is currently at stage 1 of the tc39 process. It proposes three distinct integrity traits whose current placeholder names are:
4+
- ***fixed***: would mitigate the return-override mistake by preventing objects with this trait from being stamped with new class-private-fields.
5+
- ***overridable***: would mitigate the assignment-override mistake by enabling non-writable properties inherited from an object with this trait to be overridden by property assignment on an inheriting object.
6+
- ***non-trapping***: would mitigate proxy-based reentrancy hazards by having a proxy whose target carries this trait never trap to its handler, but rather just perform the default action directly on this non-trapping target.
7+
8+
Draft PR [feat(no-trapping-shim): ponyfill and shim for the no-trapping integrity trait #2673](https://github.com/endojs/endo/pull/2673) is a ponyfill and shim for this non-trapping integrity trait. The names it introduces are placeholders, since the bikeshedding process for these names has not yet concluded.
9+
10+
Draft PR [feat(ses,pass-style): use non-trapping integrity trait for safety #2675](https://github.com/endojs/endo/pull/2675) uses this support for the non-trapping integity trait to mitigate reentrancy attacks from hardened objects, expecially passable copy-data objects like copyLists, copyRecords, and taggeds. To do so, it makes two fundamental changes:
11+
- Where `harden` made the object at every step frozen, that PR changes `harden` to also make those objects non-trapping.
12+
- Where `passStyleOf` checked that objects are frozen, that PR changes `passStyleOf` to also check that those objects are non-trapping.
13+
14+
## How proxy code should prepare
15+
16+
[#2673](https://github.com/endojs/endo/pull/2673) will *by default* produce proxies that refuse to be made non-trapping. An explicit handler trap (whose name is TBD) will need to be explicitly provided to make a proxy that allows itself to be made non-trapping. This is the right default, because proxies on frozen almost-empty objects can still have useful trap behavior for their `get`, `set`, `has`, and `apply` traps. Even on a frozen target
17+
- The `get`, `set`, and `has` traps applied to a non-own property name are still general traps that can have useful trapping behavior.
18+
- The `apply` trap can ignore the target's call behavior and just do its own thing.
19+
20+
However, to prepare for these changes, we need to avoid hardening both such proxies and their targets. We need to avoid hardening their target because this will bypass the traps. We need to avoid hardening the proxy because such proxies will *by default* refuse to be made non-trapping, and thus refuse to be hardened.
21+
22+
## How passable objects should prepare
23+
24+
Although we think of `passStyleOf` as requiring its input to be hardened, `passStyleOf` instead checked that each relevant object is frozen. Manually freezing all objects reachable from a root object had been equivalent to hardening that root object. With these changes, even such manual transitive freezing will not make an object passable. To prepare for these changes, use `harden` explicitly instead.

packages/ses/src/commons.js

-6
Original file line numberDiff line numberDiff line change
@@ -269,12 +269,6 @@ export const finalizationRegistryUnregister =
269269
export const getConstructorOf = fn =>
270270
reflectGet(getPrototypeOf(fn), 'constructor');
271271

272-
/**
273-
* immutableObject
274-
* An immutable (frozen) empty object that is safe to share.
275-
*/
276-
export const immutableObject = freeze(create(null));
277-
278272
/**
279273
* isObject tests whether a value is an object.
280274
* Today, this is equivalent to:

packages/ses/src/sloppy-globals-scope-terminator.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@ import {
33
create,
44
freeze,
55
getOwnPropertyDescriptors,
6-
immutableObject,
76
reflectSet,
87
} from './commons.js';
98
import {
109
strictScopeTerminatorHandler,
1110
alwaysThrowHandler,
1211
} from './strict-scope-terminator.js';
1312

13+
/**
14+
* `freeze` but not `harden` the proxy target so it remains trapping.
15+
* Thus, it should not be shared outside this module.
16+
* @see https://github.com/endojs/endo/blob/master/packages/ses/docs/preparing-for-stabilize.md
17+
*/
18+
const onlyFrozenObject = freeze(create(null));
19+
1420
/*
1521
* createSloppyGlobalsScopeTerminator()
1622
* strictScopeTerminatorHandler manages a scopeTerminator Proxy which serves as
@@ -45,7 +51,7 @@ export const createSloppyGlobalsScopeTerminator = globalObject => {
4551
);
4652

4753
const sloppyGlobalsScopeTerminator = new Proxy(
48-
immutableObject,
54+
onlyFrozenObject,
4955
sloppyGlobalsScopeTerminatorHandler,
5056
);
5157

packages/ses/src/strict-scope-terminator.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@ import {
77
freeze,
88
getOwnPropertyDescriptors,
99
globalThis,
10-
immutableObject,
1110
} from './commons.js';
1211
import { assert } from './error/assert.js';
1312

1413
const { Fail, quote: q } = assert;
1514

15+
/**
16+
* `freeze` but not `harden` the proxy target so it remains trapping.
17+
* Thus, it should not be shared outside this module.
18+
* @see https://github.com/endojs/endo/blob/master/packages/ses/docs/preparing-for-stabilize.md
19+
*/
20+
const onlyFrozenObject = freeze(create(null));
21+
1622
/**
1723
* alwaysThrowHandler
1824
* This is an object that throws if any property is called. It's used as
@@ -21,7 +27,7 @@ const { Fail, quote: q } = assert;
2127
* create one and share it between all Proxy handlers.
2228
*/
2329
export const alwaysThrowHandler = new Proxy(
24-
immutableObject,
30+
onlyFrozenObject,
2531
freeze({
2632
get(_shadow, prop) {
2733
Fail`Please report unexpected scope handler trap: ${q(String(prop))}`;
@@ -88,6 +94,6 @@ export const strictScopeTerminatorHandler = freeze(
8894
);
8995

9096
export const strictScopeTerminator = new Proxy(
91-
immutableObject,
97+
onlyFrozenObject,
9298
strictScopeTerminatorHandler,
9399
);

0 commit comments

Comments
 (0)