From 86d38bab9cf825ad00e595b0b7800855e8f07a15 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Tue, 6 Aug 2024 20:11:03 -0700 Subject: [PATCH] feat(ses): ArrayBuffer.prototype.transferToImmutable --- packages/ses/NEWS.md | 9 +- packages/ses/package.json | 3 +- packages/ses/src/commons.js | 1 + packages/ses/src/get-anonymous-intrinsics.js | 11 +++ packages/ses/src/lockdown.js | 1 + packages/ses/src/permits.js | 25 +++++ .../ses/test/immutable-array-buffer.test.js | 91 +++++++++++++++++++ yarn.lock | 3 +- 8 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 packages/ses/test/immutable-array-buffer.test.js diff --git a/packages/ses/NEWS.md b/packages/ses/NEWS.md index f343e409b5..6a3ab6f309 100644 --- a/packages/ses/NEWS.md +++ b/packages/ses/NEWS.md @@ -1,5 +1,13 @@ User-visible changes in `ses`: +# Next release + +- Adds `ArrayBuffer.p.immutable` and `ArrayBuffer.p.transferToImmutable` as a shim for a future proposal. It makes an ArrayBuffer-like object whose contents cannot be mutated. However, due to limitations of the shim + - Unlike `ArrayBuffer` and `SharedArrayBuffer` this shim's ArrayBuffer-like object cannot be transfered or cloned between JS threads. + - Unlike `ArrayBuffer` and `SharedArrayBuffer`, this shim's ArrayBuffer-like object cannot be used as the backing store of TypeArrays or DataViews. + - The shim depends on the platform providing either `structuredClone` or `Array.prototype.transfer`. Node <= 16 provides neither, causing the shim to fail to initialize, and therefore SES to fail to initialize on such platforms. + - Even after the upcoming `transferToImmutable` proposal is implemented by the platform, the current code will still replace it with the shim implementation, in accord with shim best practices. See https://github.com/endojs/endo/pull/2311#discussion_r1632607527 . It will require a later manual step to delete the shim, after manual analysis of the compat implications. + # v1.6.0 (2024-07-30) - *NOTICE*: This version introduces multiple features to converge upon a @@ -60,7 +68,6 @@ User-visible changes in `ses`: in TypeScript), the stacktrace line-numbers point back into the original source, as they do on Node without SES. - # v1.5.0 (2024-05-06) - Adds `importNowHook` to the `Compartment` options. The compartment will invoke the hook whenever it encounters a missing dependency while running `compartmentInstance.importNow(specifier)`, which cannot use an asynchronous `importHook`. diff --git a/packages/ses/package.json b/packages/ses/package.json index 4cd3bfbdd3..31879a4e10 100644 --- a/packages/ses/package.json +++ b/packages/ses/package.json @@ -76,7 +76,8 @@ "test:platform-compatibility": "node test/package/test.cjs" }, "dependencies": { - "@endo/env-options": "^1.1.5" + "@endo/env-options": "^1.1.5", + "@endo/immutable-arraybuffer": "^0.1.0" }, "devDependencies": { "@endo/compartment-mapper": "^1.2.1", diff --git a/packages/ses/src/commons.js b/packages/ses/src/commons.js index 5028a7f572..19b00c80c1 100644 --- a/packages/ses/src/commons.js +++ b/packages/ses/src/commons.js @@ -19,6 +19,7 @@ export { universalThis as globalThis }; export const { Array, + ArrayBuffer, Date, FinalizationRegistry, Float32Array, diff --git a/packages/ses/src/get-anonymous-intrinsics.js b/packages/ses/src/get-anonymous-intrinsics.js index cf402c3339..bfa00c8b92 100644 --- a/packages/ses/src/get-anonymous-intrinsics.js +++ b/packages/ses/src/get-anonymous-intrinsics.js @@ -14,6 +14,7 @@ import { matchAllSymbol, regexpPrototype, globalThis, + ArrayBuffer, } from './commons.js'; import { InertCompartment } from './compartment.js'; @@ -157,5 +158,15 @@ export const getAnonymousIntrinsics = () => { ); } + const ab = new ArrayBuffer(0); + // @ts-expect-error TODO How do I add transferToImmutable to ArrayBuffer type? + // eslint-disable-next-line @endo/no-polymorphic-call + const iab = ab.transferToImmutable(); + const iabProto = getPrototypeOf(iab); + if (iabProto !== ArrayBuffer.prototype) { + // In a native implementation, these will be the same prototype + intrinsics['%ImmutableArrayBufferPrototype%'] = iabProto; + } + return intrinsics; }; diff --git a/packages/ses/src/lockdown.js b/packages/ses/src/lockdown.js index 3a2e9c4921..63e6782926 100644 --- a/packages/ses/src/lockdown.js +++ b/packages/ses/src/lockdown.js @@ -15,6 +15,7 @@ // @ts-check import { getEnvironmentOption as getenv } from '@endo/env-options'; +import '@endo/immutable-arraybuffer/shim.js'; import { FERAL_FUNCTION, FERAL_EVAL, diff --git a/packages/ses/src/permits.js b/packages/ses/src/permits.js index 585d3c0c3f..298cbacb7a 100644 --- a/packages/ses/src/permits.js +++ b/packages/ses/src/permits.js @@ -1283,6 +1283,31 @@ export const permitted = { // https://github.com/tc39/proposal-arraybuffer-transfer transferToFixedLength: fn, detached: getter, + // https://github.com/endojs/endo/pull/2309#issuecomment-2155513240 + // to be proposed + transferToImmutable: fn, + immutable: getter, + }, + + // If this exists, it is purely an artifact of how we currently shim + // `transferToImmutable`. As natively implemented, there would be no + // such extra prototype. + '%ImmutableArrayBufferPrototype%': { + '[[Proto]]': '%ArrayBufferPrototype%', + byteLength: getter, + slice: fn, + // See https://github.com/tc39/proposal-resizablearraybuffer + transfer: fn, + resize: fn, + resizable: getter, + maxByteLength: getter, + // https://github.com/tc39/proposal-arraybuffer-transfer + transferToFixedLength: fn, + detached: getter, + // https://github.com/endojs/endo/pull/2309#issuecomment-2155513240 + // to be proposed + transferToImmutable: fn, + immutable: getter, }, // SharedArrayBuffer Objects diff --git a/packages/ses/test/immutable-array-buffer.test.js b/packages/ses/test/immutable-array-buffer.test.js new file mode 100644 index 0000000000..d4d19ba4e5 --- /dev/null +++ b/packages/ses/test/immutable-array-buffer.test.js @@ -0,0 +1,91 @@ +import test from 'ava'; +import '../index.js'; + +const { isFrozen, getPrototypeOf } = Object; + +lockdown(); + +test('ses Immutable ArrayBuffer shim installed and hardened', t => { + const ab1 = new ArrayBuffer(0); + const iab = ab1.transferToImmutable(); + const iabProto = getPrototypeOf(iab); + t.true(isFrozen(iabProto)); + t.true(isFrozen(iabProto.slice)); +}); + +test('ses Immutable ArrayBuffer shim ops', t => { + // Absent on Node <= 18 + const canResize = 'maxByteLength' in ArrayBuffer.prototype; + + const ab1 = new ArrayBuffer(2, { maxByteLength: 7 }); + const ta1 = new Uint8Array(ab1); + ta1[0] = 3; + ta1[1] = 4; + const iab = ab1.transferToImmutable(); + t.true(iab instanceof ArrayBuffer); + ta1[1] = 5; + const ab2 = iab.slice(0); + const ta2 = new Uint8Array(ab2); + t.is(ta1[1], undefined); + t.is(ta2[1], 4); + ta2[1] = 6; + + const ab3 = iab.slice(0); + t.true(ab3 instanceof ArrayBuffer); + + const ta3 = new Uint8Array(ab3); + t.is(ta1[1], undefined); + t.is(ta2[1], 6); + t.is(ta3[1], 4); + + t.is(ab1.byteLength, 0); + t.is(iab.byteLength, 2); + t.is(ab2.byteLength, 2); + + t.is(iab.maxByteLength, 2); + if (canResize) { + t.is(ab1.maxByteLength, 0); + t.is(ab2.maxByteLength, 2); + } + + if ('detached' in ab1) { + t.true(ab1.detached); + t.false(ab2.detached); + t.false(ab3.detached); + } + t.false(iab.detached); + t.false(iab.resizable); +}); + +// This could have been written as a test.failing as compared to +// the immutable ArrayBuffer we'll propose. However, I'd rather test what +// the shim purposely does instead. +test('ses Immutable ArrayBuffer shim limitations', t => { + const ab1 = new ArrayBuffer(2); + const dv1 = new DataView(ab1); + t.is(dv1.buffer, ab1); + t.is(dv1.byteLength, 2); + const ta1 = new Uint8Array(ab1); + ta1[0] = 3; + ta1[1] = 4; + t.is(ta1.byteLength, 2); + + t.throws(() => new DataView({}), { instanceOf: TypeError }); + // Unfortutanely, calling a TypeArray constructor with an object that + // is not a TypeArray, ArrayBuffer, or Iterable just creates a useless + // empty TypedArray, rather than throwing. + const ta2 = new Uint8Array({}); + t.is(ta2.byteLength, 0); + + const iab = ab1.transferToImmutable(); + t.throws(() => new DataView(iab), { + instanceOf: TypeError, + }); + // Unfortunately, unlike the immutable ArrayBuffer to be proposed, + // calling a TypedArray constructor with the shim implementation of + // an immutable ArrayBuffer as argument treats it as an unrecognized object, + // rather than throwing an error. + t.is(iab.byteLength, 2); + const ta3 = new Uint8Array(iab); + t.is(ta3.byteLength, 0); +}); diff --git a/yarn.lock b/yarn.lock index 74bd617dd5..c351da12a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -494,7 +494,7 @@ __metadata: languageName: unknown linkType: soft -"@endo/immutable-arraybuffer@workspace:packages/immutable-arraybuffer": +"@endo/immutable-arraybuffer@npm:^0.1.0, @endo/immutable-arraybuffer@workspace:packages/immutable-arraybuffer": version: 0.0.0-use.local resolution: "@endo/immutable-arraybuffer@workspace:packages/immutable-arraybuffer" dependencies: @@ -10554,6 +10554,7 @@ __metadata: dependencies: "@endo/compartment-mapper": "npm:^1.2.1" "@endo/env-options": "npm:^1.1.5" + "@endo/immutable-arraybuffer": "npm:^0.1.0" "@endo/module-source": "npm:^1.0.1" "@endo/test262-runner": "npm:^0.1.39" ava: "npm:^6.1.3"