diff --git a/packages/captp/src/captp.js b/packages/captp/src/captp.js index abdfdd4966..ace8c15a8b 100644 --- a/packages/captp/src/captp.js +++ b/packages/captp/src/captp.js @@ -47,7 +47,7 @@ const reverseSlot = slot => { }; /** - * @typedef {object} CapTPImportExportTables + * @typedef {object} CapTPImportExportTables * @property {(value: any) => CapTPSlot} makeSlotForValue * @property {(slot: CapTPSlot, iface: string | undefined) => any} makeValueForSlot * @property {(slot: CapTPSlot) => boolean} hasImport @@ -58,12 +58,12 @@ const reverseSlot = slot => { * @property {(slot: CapTPSlot, value: any) => void} markAsExported * @property {(slot: CapTPSlot) => void} deleteExport * @property {() => void} didDisconnect - + * @typedef {object} MakeCapTPImportExportTablesOptions * @property {boolean} gcImports * @property {(slot: CapTPSlot) => void} releaseSlot * @property {(slot: CapTPSlot) => RemoteKit} makeRemoteKit - + * @param {MakeCapTPImportExportTablesOptions} options * @returns {CapTPImportExportTables} */ diff --git a/packages/captp/src/trap.js b/packages/captp/src/trap.js index 5b7c81afda..ec660e0a5d 100644 --- a/packages/captp/src/trap.js +++ b/packages/captp/src/trap.js @@ -1,5 +1,7 @@ // Lifted mostly from `@endo/eventual-send/src/E.js`. +const { freeze } = Object; + /** * Default implementation of Trap for near objects. * @@ -63,7 +65,10 @@ const TrapProxyHandler = (x, trapImpl) => { export const makeTrap = trapImpl => { const Trap = x => { const handler = TrapProxyHandler(x, trapImpl); - return harden(new Proxy(() => {}, handler)); + return new Proxy( + freeze(() => {}), + handler, + ); }; const makeTrapGetterProxy = x => { @@ -77,7 +82,7 @@ export const makeTrap = trapImpl => { return trapImpl.get(x, prop); }, }); - return new Proxy(Object.create(null), handler); + return new Proxy(freeze(Object.create(null)), handler); }; Trap.get = makeTrapGetterProxy; diff --git a/packages/eventual-send/src/E.js b/packages/eventual-send/src/E.js index a114e3ccb2..4c6bed01c3 100644 --- a/packages/eventual-send/src/E.js +++ b/packages/eventual-send/src/E.js @@ -2,7 +2,7 @@ import { trackTurns } from './track-turns.js'; import { makeMessageBreakpointTester } from './message-breakpoints.js'; const { details: X, quote: q, Fail, error: makeError } = assert; -const { assign, create } = Object; +const { assign, create, freeze } = Object; /** * @import { HandledPromiseConstructor } from './types.js'; @@ -171,6 +171,13 @@ const makeEGetProxyHandler = (x, HandledPromise) => * @param {HandledPromiseConstructor} HandledPromise */ const makeE = HandledPromise => { + // Note the use of `freeze` rather than `harden` below. This is because + // `harden` now implies no-trapping, and we depend on proxies with these + // almost-empty targets to remain trapping for traps `get`, `apply`, and `set` + // which can still be interesting even when the target is frozen. + // `get` and `has`, if not naming an own property, are still general traps, + // which we rely on. `apply`, surprisingly perhaps, is free to ignore the + // target's call behavior and just do its own thing instead. return harden( assign( /** @@ -182,8 +189,12 @@ const makeE = HandledPromise => { * @param {T} x target for method/function call * @returns {ECallableOrMethods>} method/function call proxy */ - // @ts-expect-error XXX typedef - x => harden(new Proxy(() => {}, makeEProxyHandler(x, HandledPromise))), + x => + // @ts-expect-error XXX typedef + new Proxy( + freeze(() => {}), + makeEProxyHandler(x, HandledPromise), + ), { /** * E.get(x) returns a proxy on which you can get arbitrary properties. @@ -198,8 +209,9 @@ const makeE = HandledPromise => { */ get: x => // @ts-expect-error XXX typedef - harden( - new Proxy(create(null), makeEGetProxyHandler(x, HandledPromise)), + new Proxy( + freeze(create(null)), + makeEGetProxyHandler(x, HandledPromise), ), /** @@ -224,8 +236,9 @@ const makeE = HandledPromise => { */ sendOnly: x => // @ts-expect-error XXX typedef - harden( - new Proxy(() => {}, makeESendOnlyProxyHandler(x, HandledPromise)), + new Proxy( + freeze(() => {}), + makeESendOnlyProxyHandler(x, HandledPromise), ), /** diff --git a/packages/eventual-send/src/handled-promise.js b/packages/eventual-send/src/handled-promise.js index 17785b3529..918660cb30 100644 --- a/packages/eventual-send/src/handled-promise.js +++ b/packages/eventual-send/src/handled-promise.js @@ -307,6 +307,8 @@ export const makeHandledPromise = () => { const { proxy: proxyOpts } = options; let presence; if (proxyOpts) { + // TODO for these cases, it will be unreasonably hard for all uses + // to avoid hardening the returned proxy. const { handler: proxyHandler, target: proxyTarget, diff --git a/packages/exo/src/exo-makers.js b/packages/exo/src/exo-makers.js index b3ffd13898..625ebba4de 100644 --- a/packages/exo/src/exo-makers.js +++ b/packages/exo/src/exo-makers.js @@ -8,7 +8,14 @@ import { defendPrototype, defendPrototypeKit } from './exo-tools.js'; * @import {Amplify, ExoClassKitMethods, ExoClassMethods, FarClassOptions, Guarded, GuardedKit, ExoClassInterfaceGuardKit, IsInstance, KitContext, ExoClassInterfaceGuard, Methods, FacetName} from './types.js'; */ -const { create, seal, freeze, defineProperty, values } = Object; +const { + create, + seal, + defineProperty, + values, + // @ts-expect-error TS doesn't know this is on ObjectConstructor + suppressTrapping, +} = Object; // Turn on to give each exo instance its own toStringTag value. const LABEL_INSTANCES = environmentOptionsListHas('DEBUG', 'label-instances'); @@ -92,7 +99,7 @@ export const defineExoClass = ( // Be careful not to freeze the state record /** @type {import('./types.js').ClassContext,M>} */ - const context = freeze({ state, self }); + const context = suppressTrapping({ state, self }); contextMap.set(self, context); if (finish) { finish(context); @@ -173,7 +180,7 @@ export const defineExoClassKit = ( }); context.facets = facets; // Be careful not to freeze the state record - freeze(context); + suppressTrapping(context); if (finish) { finish(context); } diff --git a/packages/far/test/marshal-far-function.test.js b/packages/far/test/marshal-far-function.test.js index c86c2b1fb6..28d544e6c6 100644 --- a/packages/far/test/marshal-far-function.test.js +++ b/packages/far/test/marshal-far-function.test.js @@ -58,7 +58,7 @@ test('Data can contain far functions', t => { const arrow = Far('arrow', a => a + 1); t.is(passStyleOf(harden({ x: 8, foo: arrow })), 'copyRecord'); const mightBeMethod = a => a + 1; - t.throws(() => passStyleOf(freeze({ x: 8, foo: mightBeMethod })), { + t.throws(() => passStyleOf(harden({ x: 8, foo: mightBeMethod })), { message: /Remotables with non-methods like "x" /, }); }); diff --git a/packages/marshal/src/encodeToCapData.js b/packages/marshal/src/encodeToCapData.js index 052b5795a1..aaba178cee 100644 --- a/packages/marshal/src/encodeToCapData.js +++ b/packages/marshal/src/encodeToCapData.js @@ -30,7 +30,8 @@ const { is, entries, fromEntries, - freeze, + // @ts-expect-error TS doesn't know this is on ObjectConstructor + suppressTrapping, } = Object; /** @@ -176,10 +177,10 @@ export const makeEncodeToCapData = (encodeOptions = {}) => { // We harden the entire capData encoding before we return it. // `encodeToCapData` requires that its input be Passable, and // therefore hardened. - // The `freeze` here is needed anyway, because the `rest` is + // The `suppressTrapping` here is needed anyway, because the `rest` is // freshly constructed by the `...` above, and we're using it // as imput in another call to `encodeToCapData`. - result.rest = encodeToCapDataRecur(freeze(rest)); + result.rest = encodeToCapDataRecur(suppressTrapping(rest)); } return result; } diff --git a/packages/marshal/src/marshal-stringify.js b/packages/marshal/src/marshal-stringify.js index e581cdefed..996a50b706 100644 --- a/packages/marshal/src/marshal-stringify.js +++ b/packages/marshal/src/marshal-stringify.js @@ -5,6 +5,8 @@ import { makeMarshal } from './marshal.js'; /** @import {Passable} from '@endo/pass-style' */ +const { freeze } = Object; + /** @type {import('./types.js').ConvertValToSlot} */ const doNotConvertValToSlot = val => Fail`Marshal's stringify rejects presences and promises ${val}`; @@ -23,7 +25,13 @@ const badArrayHandler = harden({ }, }); -const badArray = harden(new Proxy(harden([]), badArrayHandler)); +// Note the use of `freeze` rather than `harden` below. This is because +// `harden` now implies no-trapping, and we depend on proxies with these +// almost-empty targets to remain trapping for the `get` trap +// which can still be interesting even when the target is frozen. +// `get`, if not naming an own property, are still general traps, +// which we rely on. +const badArray = new Proxy(freeze([]), badArrayHandler); const { serialize, unserialize } = makeMarshal( doNotConvertValToSlot, @@ -48,7 +56,13 @@ harden(stringify); */ const parse = str => unserialize( - harden({ + // Note the use of `freeze` rather than `harden` below. This is because + // `harden` now implies no-trapping, and we depend on proxies with these + // almost-empty targets to remain trapping for the `get` trap + // which can still be interesting even when the target is frozen. + // `get`, if not naming an own property, are still general traps, + // which we rely on. + freeze({ body: str, slots: badArray, }), diff --git a/packages/marshal/test/marshal-far-function.test.js b/packages/marshal/test/marshal-far-function.test.js index b546b11621..223fdd27e5 100644 --- a/packages/marshal/test/marshal-far-function.test.js +++ b/packages/marshal/test/marshal-far-function.test.js @@ -60,7 +60,7 @@ test('Data can contain far functions', t => { const arrow = Far('arrow', a => a + 1); t.is(passStyleOf(harden({ x: 8, foo: arrow })), 'copyRecord'); const mightBeMethod = a => a + 1; - t.throws(() => passStyleOf(freeze({ x: 8, foo: mightBeMethod })), { + t.throws(() => passStyleOf(harden({ x: 8, foo: mightBeMethod })), { message: /Remotables with non-methods like "x" /, }); }); diff --git a/packages/pass-style/src/passStyle-helpers.js b/packages/pass-style/src/passStyle-helpers.js index 7107e11b89..ade0bb7fda 100644 --- a/packages/pass-style/src/passStyle-helpers.js +++ b/packages/pass-style/src/passStyle-helpers.js @@ -11,8 +11,9 @@ const { getOwnPropertyDescriptor, getPrototypeOf, hasOwnProperty: objectHasOwnProperty, - isFrozen, prototype: objectPrototype, + // @ts-expect-error TS does not yet have `isNoTrapping` on ObjectConstructor + isNoTrapping, } = Object; const { apply } = Reflect; const { toStringTag: toStringTagSymbol } = Symbol; @@ -165,7 +166,7 @@ const makeCheckTagRecord = checkProto => { (isObject(tagRecord) || (!!check && CX(check)`A non-object cannot be a tagRecord: ${tagRecord}`)) && - (isFrozen(tagRecord) || + (isNoTrapping(tagRecord) || (!!check && CX(check)`A tagRecord must be frozen: ${tagRecord}`)) && (!isArray(tagRecord) || (!!check && CX(check)`An array cannot be a tagRecord: ${tagRecord}`)) && diff --git a/packages/pass-style/src/passStyleOf.js b/packages/pass-style/src/passStyleOf.js index 7c4dd78b2e..de4286f1e2 100644 --- a/packages/pass-style/src/passStyleOf.js +++ b/packages/pass-style/src/passStyleOf.js @@ -31,7 +31,12 @@ import { assertPassableString } from './string.js'; /** @typedef {Exclude} HelperPassStyle */ const { ownKeys } = Reflect; -const { isFrozen, getOwnPropertyDescriptors, values } = Object; +const { + getOwnPropertyDescriptors, + values, + // @ts-expect-error TS does not yet have `isNoTrapping` on ObjectConstructor + isNoTrapping, +} = Object; /** * @param {PassStyleHelper[]} passStyleHelpers @@ -143,7 +148,7 @@ const makePassStyleOf = passStyleHelpers => { if (inner === null) { return 'null'; } - if (!isFrozen(inner)) { + if (!isNoTrapping(inner)) { assert.fail( // TypedArrays get special treatment in harden() // and a corresponding special error message here. @@ -177,7 +182,7 @@ const makePassStyleOf = passStyleHelpers => { return 'remotable'; } case 'function': { - isFrozen(inner) || + isNoTrapping(inner) || Fail`Cannot pass non-frozen objects like ${inner}. Use harden()`; typeof inner.then !== 'function' || Fail`Cannot pass non-promise thenables`; diff --git a/packages/pass-style/src/remotable.js b/packages/pass-style/src/remotable.js index af681c2335..f2ced58b75 100644 --- a/packages/pass-style/src/remotable.js +++ b/packages/pass-style/src/remotable.js @@ -24,9 +24,10 @@ const { ownKeys } = Reflect; const { isArray } = Array; const { getPrototypeOf, - isFrozen, prototype: objectPrototype, getOwnPropertyDescriptors, + // @ts-expect-error TS does not yet have `isNoTrapping` on ObjectConstructor + isNoTrapping, } = Object; /** @@ -154,7 +155,7 @@ const checkRemotable = (val, check) => { if (confirmedRemotables.has(val)) { return true; } - if (!isFrozen(val)) { + if (!isNoTrapping(val)) { return ( !!check && CX(check)`cannot serialize non-frozen objects like ${val}` ); diff --git a/packages/pass-style/src/safe-promise.js b/packages/pass-style/src/safe-promise.js index 407e2aab6a..c79a7e5aaf 100644 --- a/packages/pass-style/src/safe-promise.js +++ b/packages/pass-style/src/safe-promise.js @@ -6,7 +6,12 @@ import { assertChecker, hasOwnPropertyOf, CX } from './passStyle-helpers.js'; /** @import {Checker} from './types.js' */ -const { isFrozen, getPrototypeOf, getOwnPropertyDescriptor } = Object; +const { + getPrototypeOf, + getOwnPropertyDescriptor, + // @ts-expect-error TS does not yet have `isNoTrapping` on ObjectConstructor + isNoTrapping, +} = Object; const { ownKeys } = Reflect; const { toStringTag } = Symbol; @@ -88,7 +93,7 @@ const checkPromiseOwnKeys = (pr, check) => { if ( typeof val === 'object' && val !== null && - isFrozen(val) && + isNoTrapping(val) && getPrototypeOf(val) === Object.prototype ) { const subKeys = ownKeys(val); @@ -132,7 +137,7 @@ const checkPromiseOwnKeys = (pr, check) => { */ const checkSafePromise = (pr, check) => { return ( - (isFrozen(pr) || CX(check)`${pr} - Must be frozen`) && + (isNoTrapping(pr) || CX(check)`${pr} - Must be frozen`) && (isPromise(pr) || CX(check)`${pr} - Must be a promise`) && (getPrototypeOf(pr) === Promise.prototype || CX(check)`${pr} - Must inherit from Promise.prototype: ${q( diff --git a/packages/pass-style/test/passStyleOf.test.js b/packages/pass-style/test/passStyleOf.test.js index d09cd55260..b351511488 100644 --- a/packages/pass-style/test/passStyleOf.test.js +++ b/packages/pass-style/test/passStyleOf.test.js @@ -13,7 +13,7 @@ const harden = /** @type {import('ses').Harden & { isFake?: boolean }} */ ( global.harden ); -const { getPrototypeOf, defineProperty } = Object; +const { getPrototypeOf, defineProperty, suppressTrapping } = Object; const { ownKeys } = Reflect; test('passStyleOf basic success cases', t => { @@ -200,7 +200,7 @@ test('passStyleOf testing remotables', t => { const tagRecord2 = makeTagishRecord('Alleged: tagRecord not hardened'); /** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */ - const farObj2 = Object.freeze({ + const farObj2 = suppressTrapping({ __proto__: tagRecord2, }); if (harden.isFake) { @@ -212,11 +212,11 @@ test('passStyleOf testing remotables', t => { }); } - const tagRecord3 = Object.freeze( + const tagRecord3 = suppressTrapping( makeTagishRecord('Alleged: both manually frozen'), ); /** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */ - const farObj3 = Object.freeze({ + const farObj3 = suppressTrapping({ __proto__: tagRecord3, }); t.is(passStyleOf(farObj3), 'remotable'); @@ -387,7 +387,7 @@ test('remotables - safety from the gibson042 attack', t => { }, ); - const makeInput = () => Object.freeze({ __proto__: mercurialProto }); + const makeInput = () => suppressTrapping({ __proto__: mercurialProto }); const input1 = makeInput(); const input2 = makeInput(); diff --git a/packages/ses/package.json b/packages/ses/package.json index e552bda739..40b3e16f04 100644 --- a/packages/ses/package.json +++ b/packages/ses/package.json @@ -85,7 +85,8 @@ "postpack": "git clean -f '*.d.ts*' '*.tsbuildinfo'" }, "dependencies": { - "@endo/env-options": "workspace:^" + "@endo/env-options": "workspace:^", + "@endo/no-trapping-shim": "^0.1.0" }, "devDependencies": { "@endo/compartment-mapper": "workspace:^", diff --git a/packages/ses/src/commons.js b/packages/ses/src/commons.js index 5925c17eae..aeca5426a9 100644 --- a/packages/ses/src/commons.js +++ b/packages/ses/src/commons.js @@ -14,6 +14,8 @@ /* global globalThis */ /* eslint-disable no-restricted-globals */ +import '@endo/no-trapping-shim/shim.js'; + // We cannot use globalThis as the local name since it would capture the // lexical name. const universalThis = globalThis; @@ -75,6 +77,11 @@ export const { setPrototypeOf, values, fromEntries, + // https://github.com/endojs/endo/pull/2673 + // @ts-expect-error TS does not yet have this on ObjectConstructor. + isNoTrapping, + // @ts-expect-error TS does not yet have this on ObjectConstructor. + suppressTrapping, } = Object; export const { @@ -125,6 +132,11 @@ export const { ownKeys, preventExtensions: reflectPreventExtensions, set: reflectSet, + // https://github.com/endojs/endo/pull/2673 + // @ts-expect-error TS does not yet have this on typeof Reflect. + isNoTrapping: reflectIsNoTrapping, + // @ts-expect-error TS does not yet have this on typeof Reflect. + suppressTrapping: reflectSuppressTrapping, } = Reflect; export const { isArray, prototype: arrayPrototype } = Array; @@ -271,9 +283,9 @@ export const getConstructorOf = fn => /** * immutableObject - * An immutable (frozen) empty object that is safe to share. + * An immutable (frozen, no-trapping) empty object that is safe to share. */ -export const immutableObject = freeze(create(null)); +export const immutableObject = suppressTrapping(create(null)); /** * isObject tests whether a value is an object. diff --git a/packages/ses/src/make-hardener.js b/packages/ses/src/make-hardener.js index d377fd8793..8c3c7c3115 100644 --- a/packages/ses/src/make-hardener.js +++ b/packages/ses/src/make-hardener.js @@ -30,7 +30,6 @@ import { apply, arrayForEach, defineProperty, - freeze, getOwnPropertyDescriptor, getOwnPropertyDescriptors, getPrototypeOf, @@ -49,6 +48,8 @@ import { FERAL_STACK_GETTER, FERAL_STACK_SETTER, isError, + isFrozen, + suppressTrapping, } from './commons.js'; import { assert } from './error/assert.js'; @@ -182,8 +183,17 @@ export const makeHardener = () => { // Also throws if the object is an ArrayBuffer or any TypedArray. if (isTypedArray(obj)) { freezeTypedArray(obj); + if (isFrozen(obj)) { + // After `freezeTypedArray`, the typed array might actually be + // frozen if + // - it has no indexed properties + // - it is backed by an Immutable ArrayBuffer as proposed. + // In either case, this makes it a candidate to be made + // non-trapping. + suppressTrapping(obj); + } } else { - freeze(obj); + suppressTrapping(obj); } // we rely upon certain commitments of Object.freeze and proxies here diff --git a/packages/ses/src/permits.js b/packages/ses/src/permits.js index 283d861c34..6c79bd435c 100644 --- a/packages/ses/src/permits.js +++ b/packages/ses/src/permits.js @@ -488,6 +488,9 @@ export const permitted = { groupBy: fn, // Seen on QuickJS __getClass: false, + // https://github.com/endojs/endo/pull/2673 + isNoTrapping: fn, + suppressTrapping: fn, }, '%ObjectPrototype%': { @@ -1624,12 +1627,17 @@ export const permitted = { set: fn, setPrototypeOf: fn, '@@toStringTag': 'string', + // https://github.com/endojs/endo/pull/2673 + isNoTrapping: fn, + suppressTrapping: fn, }, Proxy: { // Properties of the Proxy Constructor '[[Proto]]': '%FunctionPrototype%', revocable: fn, + // https://github.com/endojs/endo/pull/2673 + prototype: 'undefined', }, // Appendix B diff --git a/packages/ses/src/sloppy-globals-scope-terminator.js b/packages/ses/src/sloppy-globals-scope-terminator.js index 01e3860856..7f78e4d66b 100644 --- a/packages/ses/src/sloppy-globals-scope-terminator.js +++ b/packages/ses/src/sloppy-globals-scope-terminator.js @@ -3,7 +3,6 @@ import { create, freeze, getOwnPropertyDescriptors, - immutableObject, reflectSet, } from './commons.js'; import { @@ -11,6 +10,8 @@ import { alwaysThrowHandler, } from './strict-scope-terminator.js'; +const onlyFrozenObject = freeze(create(null)); + /* * createSloppyGlobalsScopeTerminator() * strictScopeTerminatorHandler manages a scopeTerminator Proxy which serves as @@ -45,7 +46,7 @@ export const createSloppyGlobalsScopeTerminator = globalObject => { ); const sloppyGlobalsScopeTerminator = new Proxy( - immutableObject, + onlyFrozenObject, sloppyGlobalsScopeTerminatorHandler, ); diff --git a/packages/ses/src/strict-scope-terminator.js b/packages/ses/src/strict-scope-terminator.js index 7257afccd1..1060ad4fc9 100644 --- a/packages/ses/src/strict-scope-terminator.js +++ b/packages/ses/src/strict-scope-terminator.js @@ -7,12 +7,13 @@ import { freeze, getOwnPropertyDescriptors, globalThis, - immutableObject, } from './commons.js'; import { assert } from './error/assert.js'; const { Fail, quote: q } = assert; +const onlyFrozenObject = freeze(create(null)); + /** * alwaysThrowHandler * This is an object that throws if any property is called. It's used as @@ -21,7 +22,7 @@ const { Fail, quote: q } = assert; * create one and share it between all Proxy handlers. */ export const alwaysThrowHandler = new Proxy( - immutableObject, + onlyFrozenObject, freeze({ get(_shadow, prop) { Fail`Please report unexpected scope handler trap: ${q(String(prop))}`; @@ -88,6 +89,6 @@ export const strictScopeTerminatorHandler = freeze( ); export const strictScopeTerminator = new Proxy( - immutableObject, + onlyFrozenObject, strictScopeTerminatorHandler, ); diff --git a/yarn.lock b/yarn.lock index 9a92741957..014928f2cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -702,7 +702,7 @@ __metadata: languageName: unknown linkType: soft -"@endo/no-trapping-shim@workspace:packages/no-trapping-shim": +"@endo/no-trapping-shim@npm:^0.1.0, @endo/no-trapping-shim@workspace:packages/no-trapping-shim": version: 0.0.0-use.local resolution: "@endo/no-trapping-shim@workspace:packages/no-trapping-shim" dependencies: @@ -8960,6 +8960,7 @@ __metadata: "@endo/compartment-mapper": "workspace:^" "@endo/env-options": "workspace:^" "@endo/module-source": "workspace:^" + "@endo/no-trapping-shim": "npm:^0.1.0" "@endo/test262-runner": "workspace:^" ava: "npm:^6.1.3" babel-eslint: "npm:^10.1.0"