From 7ecc1845c4f364660e66a42c5745d6d7225b76b6 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 26 Oct 2020 13:05:48 -0700 Subject: [PATCH 1/3] fix: add stubs for GC tools (no-op on Node v12) The upcoming GC functionality will require `WeakRef` and `FinalizationRegistry`. Node.js v14 provides these as globals, but v12 does not (there might be a command-line flag to enable it, but I think it's marked as experimental). Rather than require all users upgrade to v14, we elect to disable GC when running on v12. This adds a local `weakref.js` module which attempts to pull `WeakRef` and `FinalizationRegistry` from the global, and exports either the real constructors or no-op stubs. refs #1872 refs #1925 --- packages/SwingSet/src/weakref.js | 38 +++++++++++++++++++++ packages/SwingSet/test/test-fake-weakref.js | 23 +++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 packages/SwingSet/src/weakref.js create mode 100644 packages/SwingSet/test/test-fake-weakref.js diff --git a/packages/SwingSet/src/weakref.js b/packages/SwingSet/src/weakref.js new file mode 100644 index 00000000000..205220378fd --- /dev/null +++ b/packages/SwingSet/src/weakref.js @@ -0,0 +1,38 @@ +/* global globalThis */ + +/* + * We retain a measure of compatibility with Node.js v12, which does not + * expose WeakRef or FinalizationRegistry (there is a --flag for it, but it's + * * not clear how stable it is). When running on a platform without these * + * tools, vats cannot do GC, and the tools they get will be no-ops. WeakRef + * instances will hold a strong reference, and the FinalizationRegistry will + * never invoke the callbacks. + * + * Modules should do: + * + * import { WeakRef, FinalizationRegistry } from '.../weakref'; + * + */ + +function FakeWeakRef(obj) { + const wr = Object.create({ + deref: () => obj, + }); + delete wr.constructor; + return wr; +} + +function FakeFinalizationRegistry(_callback) { + const fr = Object.create({ + register: (_obj, _handle) => undefined, + unregister: _handle => undefined, + }); + delete fr.constructor; + return fr; +} + +const WR = globalThis.WeakRef || FakeWeakRef; +const FR = globalThis.FinalizationRegistry || FakeFinalizationRegistry; + +export const WeakRef = WR; +export const FinalizationRegistry = FR; diff --git a/packages/SwingSet/test/test-fake-weakref.js b/packages/SwingSet/test/test-fake-weakref.js new file mode 100644 index 00000000000..75fc5025fe3 --- /dev/null +++ b/packages/SwingSet/test/test-fake-weakref.js @@ -0,0 +1,23 @@ +import '@agoric/install-ses'; +import test from 'ava'; +import { WeakRef, FinalizationRegistry } from '../src/weakref'; + +// We don't test that WeakRefs actually work, we only make sure we can +// interact with them without crashing. This exercises the fake no-op WeakRef +// and FinalizationRegistry that our `src/weakref.js` creates on Node.js v12. +// On v14 we get real constructors. + +test('weakref is callable', async t => { + const obj = {}; + const wr = new WeakRef(obj); + t.is(obj, wr.deref()); + + const callback = () => 0; + const fr = new FinalizationRegistry(callback); + fr.register(obj); + + const obj2 = {}; + const handle = {}; + fr.register(obj2, handle); + fr.unregister(handle); +}); From 3569dd8147fceed0dbc749e89fc680e732f0444c Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 26 Oct 2020 13:08:12 -0700 Subject: [PATCH 2/3] chore: provide GC tools (WeakRef/FinalizationRegistry) to makeLiveSlots These two authorities are not part of SES, so they must be pulled from the globals of the Start Compartment and ferried through the kernel to the vat manager factory that calls makeLiveSlots. This gives the outer layer of the vat (liveslots) access to nondeterminism. We rely upon liveslots to not share this power with the user-level ocap-style "vat code". Liveslots must never allow user-level code to observe behavior that depends upon GC activity, because that activity is not part of the specified input to the vat. refs #1872 --- packages/SwingSet/src/controller.js | 3 +++ packages/SwingSet/src/kernel/kernel.js | 4 ++++ .../SwingSet/src/kernel/vatManager/factory.js | 2 ++ .../src/kernel/vatManager/localVatManager.js | 3 ++- .../src/kernel/vatManager/nodeWorkerSupervisor.js | 4 +++- .../src/kernel/vatManager/subprocessSupervisor.js | 4 +++- packages/SwingSet/test/test-kernel.js | 3 +++ packages/SwingSet/test/test-liveslots.js | 10 +++++++++- packages/SwingSet/test/test-marshal.js | 15 +++++++++------ packages/SwingSet/test/test-vpid-kernel.js | 5 +++-- packages/SwingSet/test/test-vpid-liveslots.js | 10 +++++++++- 11 files changed, 50 insertions(+), 13 deletions(-) diff --git a/packages/SwingSet/src/controller.js b/packages/SwingSet/src/controller.js index f14eea28caf..653061c19ee 100644 --- a/packages/SwingSet/src/controller.js +++ b/packages/SwingSet/src/controller.js @@ -18,6 +18,7 @@ import { makeMeteringTransformer } from '@agoric/transform-metering'; import { makeTransform } from '@agoric/transform-eventual-send'; import { locateWorkerBin } from '@agoric/xs-vat-worker'; +import { WeakRef, FinalizationRegistry } from './weakref'; import { startSubprocessWorker } from './spawnSubprocessWorker'; import { waitUntilQuiescent } from './waitUntilQuiescent'; import { insistStorageAPI } from './storageAPI'; @@ -183,6 +184,8 @@ export async function makeSwingsetController( startSubprocessWorkerNode, startSubprocessWorkerXS, writeSlogObject, + WeakRef, + FinalizationRegistry, }; const kernelOptions = { verbose }; diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index 536280dc4f4..97e427382bd 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -99,6 +99,8 @@ export default function buildKernel( startSubprocessWorkerNode, startSubprocessWorkerXS, writeSlogObject, + WeakRef, + FinalizationRegistry, } = kernelEndowments; deviceEndowments = { ...deviceEndowments }; // copy so we can modify const { verbose } = kernelOptions; @@ -529,6 +531,7 @@ export default function buildKernel( } } + const gcTools = harden({ WeakRef, FinalizationRegistry }); const vatManagerFactory = makeVatManagerFactory({ allVatPowers, kernelKeeper, @@ -540,6 +543,7 @@ export default function buildKernel( makeNodeWorker, startSubprocessWorkerNode, startSubprocessWorkerXS, + gcTools, }); function buildVatSyscallHandler(vatID, translators) { diff --git a/packages/SwingSet/src/kernel/vatManager/factory.js b/packages/SwingSet/src/kernel/vatManager/factory.js index 8e6bbdf7714..fa4a207e9fb 100644 --- a/packages/SwingSet/src/kernel/vatManager/factory.js +++ b/packages/SwingSet/src/kernel/vatManager/factory.js @@ -14,6 +14,7 @@ export function makeVatManagerFactory({ makeNodeWorker, startSubprocessWorkerNode, startSubprocessWorkerXS, + gcTools, }) { const localFactory = makeLocalVatManagerFactory({ allVatPowers, @@ -22,6 +23,7 @@ export function makeVatManagerFactory({ meterManager, transformMetering, waitUntilQuiescent, + gcTools, }); const nodeWorkerFactory = makeNodeWorkerVatManagerFactory({ diff --git a/packages/SwingSet/src/kernel/vatManager/localVatManager.js b/packages/SwingSet/src/kernel/vatManager/localVatManager.js index dd53bfe7726..8434470bf0a 100644 --- a/packages/SwingSet/src/kernel/vatManager/localVatManager.js +++ b/packages/SwingSet/src/kernel/vatManager/localVatManager.js @@ -13,6 +13,7 @@ export function makeLocalVatManagerFactory(tools) { meterManager, transformMetering, waitUntilQuiescent, + gcTools, } = tools; const { makeGetMeter, refillAllMeters, stopGlobalMeter } = meterManager; @@ -107,7 +108,7 @@ export function makeLocalVatManagerFactory(tools) { // we might or might not use this, depending upon whether the vat exports // 'buildRootObject' or a default 'setup' function - const ls = makeLiveSlots(syscall, vatID, vatPowers, vatParameters); + const ls = makeLiveSlots(syscall, vatID, vatPowers, vatParameters, gcTools); let meterRecord = null; if (metered) { diff --git a/packages/SwingSet/src/kernel/vatManager/nodeWorkerSupervisor.js b/packages/SwingSet/src/kernel/vatManager/nodeWorkerSupervisor.js index 9561e88410c..d4503b6a19f 100644 --- a/packages/SwingSet/src/kernel/vatManager/nodeWorkerSupervisor.js +++ b/packages/SwingSet/src/kernel/vatManager/nodeWorkerSupervisor.js @@ -7,6 +7,7 @@ import anylogger from 'anylogger'; import { assert } from '@agoric/assert'; import { importBundle } from '@agoric/import-bundle'; import { Remotable, getInterfaceOf, makeMarshal } from '@agoric/marshal'; +import { WeakRef, FinalizationRegistry } from '../../weakref'; import { waitUntilQuiescent } from '../../waitUntilQuiescent'; import { makeLiveSlots } from '../liveSlots'; @@ -113,7 +114,8 @@ parentPort.on('message', ([type, ...margs]) => { makeMarshal, testLog, }; - const ls = makeLiveSlots(syscall, vatID, vatPowers, vatParameters); + const gcTools = harden({ WeakRef, FinalizationRegistry }); + const ls = makeLiveSlots(syscall, vatID, vatPowers, vatParameters, gcTools); const endowments = { ...ls.vatGlobals, diff --git a/packages/SwingSet/src/kernel/vatManager/subprocessSupervisor.js b/packages/SwingSet/src/kernel/vatManager/subprocessSupervisor.js index 40621ec5e2f..e7d788719fd 100644 --- a/packages/SwingSet/src/kernel/vatManager/subprocessSupervisor.js +++ b/packages/SwingSet/src/kernel/vatManager/subprocessSupervisor.js @@ -7,6 +7,7 @@ import fs from 'fs'; import { assert } from '@agoric/assert'; import { importBundle } from '@agoric/import-bundle'; import { Remotable, getInterfaceOf, makeMarshal } from '@agoric/marshal'; +import { WeakRef, FinalizationRegistry } from '../../weakref'; import { arrayEncoderStream, arrayDecoderStream } from '../../worker-protocol'; import { netstringEncoderStream, @@ -133,7 +134,8 @@ fromParent.on('data', ([type, ...margs]) => { makeMarshal, testLog, }; - const ls = makeLiveSlots(syscall, vatID, vatPowers, vatParameters); + const gcTools = harden({ WeakRef, FinalizationRegistry }); + const ls = makeLiveSlots(syscall, vatID, vatPowers, vatParameters, gcTools); const endowments = { ...ls.vatGlobals, diff --git a/packages/SwingSet/test/test-kernel.js b/packages/SwingSet/test/test-kernel.js index dfc7322df25..2bec7a73110 100644 --- a/packages/SwingSet/test/test-kernel.js +++ b/packages/SwingSet/test/test-kernel.js @@ -2,6 +2,7 @@ import '@agoric/install-ses'; import test from 'ava'; import anylogger from 'anylogger'; import { initSwingStore } from '@agoric/swing-store-simple'; +import { WeakRef, FinalizationRegistry } from '../src/weakref'; import { waitUntilQuiescent } from '../src/waitUntilQuiescent'; import buildKernel from '../src/kernel/index'; @@ -49,6 +50,8 @@ function makeEndowments() { hostStorage: initSwingStore().storage, runEndOfCrank: () => {}, makeConsole, + WeakRef, + FinalizationRegistry, }; } diff --git a/packages/SwingSet/test/test-liveslots.js b/packages/SwingSet/test/test-liveslots.js index 71c62c7ba7c..1eee852a10d 100644 --- a/packages/SwingSet/test/test-liveslots.js +++ b/packages/SwingSet/test/test-liveslots.js @@ -1,6 +1,7 @@ import '@agoric/install-ses'; import test from 'ava'; import { E } from '@agoric/eventual-send'; +import { WeakRef, FinalizationRegistry } from '../src/weakref'; import { waitUntilQuiescent } from '../src/waitUntilQuiescent'; import { makeLiveSlots } from '../src/kernel/liveSlots'; @@ -37,7 +38,14 @@ function buildSyscall() { } function makeDispatch(syscall, build) { - const { setBuildRootObject, dispatch } = makeLiveSlots(syscall, 'vatA'); + const gcTools = harden({ WeakRef, FinalizationRegistry }); + const { setBuildRootObject, dispatch } = makeLiveSlots( + syscall, + 'vatA', + {}, + {}, + gcTools, + ); setBuildRootObject(build); return dispatch; } diff --git a/packages/SwingSet/test/test-marshal.js b/packages/SwingSet/test/test-marshal.js index d6dd9d58b93..4a99dc292a0 100644 --- a/packages/SwingSet/test/test-marshal.js +++ b/packages/SwingSet/test/test-marshal.js @@ -2,10 +2,13 @@ import '@agoric/install-ses'; import test from 'ava'; import { makePromiseKit } from '@agoric/promise-kit'; +import { WeakRef, FinalizationRegistry } from '../src/weakref'; import { makeMarshaller } from '../src/kernel/liveSlots'; import { buildVatController } from '../src/index'; +const gcTools = harden({ WeakRef, FinalizationRegistry }); + async function prep() { const config = {}; const controller = await buildVatController(config); @@ -13,7 +16,7 @@ async function prep() { } test('serialize exports', t => { - const { m } = makeMarshaller(); + const { m } = makeMarshaller(undefined, gcTools); const ser = val => m.serialize(val); const o1 = harden({}); const o2 = harden({ @@ -38,7 +41,7 @@ test('serialize exports', t => { test('deserialize imports', async t => { await prep(); - const { m } = makeMarshaller(); + const { m } = makeMarshaller(undefined, gcTools); const a = m.unserialize({ body: '{"@qclass":"slot","index":0}', slots: ['o-1'], @@ -63,7 +66,7 @@ test('deserialize imports', async t => { }); test('deserialize exports', t => { - const { m } = makeMarshaller(); + const { m } = makeMarshaller(undefined, gcTools); const o1 = harden({}); m.serialize(o1); // allocates slot=1 const a = m.unserialize({ @@ -75,7 +78,7 @@ test('deserialize exports', t => { test('serialize imports', async t => { await prep(); - const { m } = makeMarshaller(); + const { m } = makeMarshaller(undefined, gcTools); const a = m.unserialize({ body: '{"@qclass":"slot","index":0}', slots: ['o-1'], @@ -94,7 +97,7 @@ test('serialize promise', async t => { }, }; - const { m } = makeMarshaller(syscall); + const { m } = makeMarshaller(syscall, gcTools); const { promise, resolve } = makePromiseKit(); t.deepEqual(m.serialize(promise), { body: '{"@qclass":"slot","index":0}', @@ -130,7 +133,7 @@ test('unserialize promise', async t => { }, }; - const { m } = makeMarshaller(syscall); + const { m } = makeMarshaller(syscall, gcTools); const p = m.unserialize({ body: '{"@qclass":"slot","index":0}', slots: ['p-1'], diff --git a/packages/SwingSet/test/test-vpid-kernel.js b/packages/SwingSet/test/test-vpid-kernel.js index 0a47846d56a..fac2c5b0640 100644 --- a/packages/SwingSet/test/test-vpid-kernel.js +++ b/packages/SwingSet/test/test-vpid-kernel.js @@ -1,9 +1,8 @@ -// eslint-disable-next-line no-redeclare - import '@agoric/install-ses'; import test from 'ava'; import anylogger from 'anylogger'; import { initSwingStore } from '@agoric/swing-store-simple'; +import { WeakRef, FinalizationRegistry } from '../src/weakref'; import { waitUntilQuiescent } from '../src/waitUntilQuiescent'; import buildKernel from '../src/kernel/index'; @@ -36,6 +35,8 @@ function makeEndowments() { hostStorage: initSwingStore().storage, runEndOfCrank: () => {}, makeConsole, + WeakRef, + FinalizationRegistry, }; } diff --git a/packages/SwingSet/test/test-vpid-liveslots.js b/packages/SwingSet/test/test-vpid-liveslots.js index e3758088e10..4133a75995b 100644 --- a/packages/SwingSet/test/test-vpid-liveslots.js +++ b/packages/SwingSet/test/test-vpid-liveslots.js @@ -6,6 +6,7 @@ import test from 'ava'; import { E } from '@agoric/eventual-send'; import { makePromiseKit } from '@agoric/promise-kit'; +import { WeakRef, FinalizationRegistry } from '../src/weakref'; import { makeLiveSlots } from '../src/kernel/liveSlots'; const RETIRE_VPIDS = true; @@ -190,7 +191,14 @@ function resolutionOf(vpid, mode, targets) { } function makeDispatch(syscall, build) { - const { setBuildRootObject, dispatch } = makeLiveSlots(syscall, 'vatA'); + const gcTools = harden({ WeakRef, FinalizationRegistry }); + const { setBuildRootObject, dispatch } = makeLiveSlots( + syscall, + 'vatA', + {}, + {}, + gcTools, + ); setBuildRootObject(build); return dispatch; } From 3949dfbc6284e40f69f7ceff21ed9a414dcdcbd4 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Mon, 26 Oct 2020 23:33:54 -0700 Subject: [PATCH 3/3] fix: WeakRef taming follows taming pattern (#1931) * fix: WeakRef taming follows taming pattern * fix: error message * fix: minor --- packages/SwingSet/src/weakref.js | 127 +++++++++++++++++++++++++------ 1 file changed, 105 insertions(+), 22 deletions(-) diff --git a/packages/SwingSet/src/weakref.js b/packages/SwingSet/src/weakref.js index 205220378fd..ec5a4638cf8 100644 --- a/packages/SwingSet/src/weakref.js +++ b/packages/SwingSet/src/weakref.js @@ -1,5 +1,9 @@ /* global globalThis */ +import { assert, details as d } from '@agoric/assert'; + +const { defineProperties } = Object; + /* * We retain a measure of compatibility with Node.js v12, which does not * expose WeakRef or FinalizationRegistry (there is a --flag for it, but it's @@ -14,25 +18,104 @@ * */ -function FakeWeakRef(obj) { - const wr = Object.create({ - deref: () => obj, - }); - delete wr.constructor; - return wr; -} - -function FakeFinalizationRegistry(_callback) { - const fr = Object.create({ - register: (_obj, _handle) => undefined, - unregister: _handle => undefined, - }); - delete fr.constructor; - return fr; -} - -const WR = globalThis.WeakRef || FakeWeakRef; -const FR = globalThis.FinalizationRegistry || FakeFinalizationRegistry; - -export const WeakRef = WR; -export const FinalizationRegistry = FR; +// TODO We need to migrate this into a ses-level tame-weakref.js, to happen +// as part of `lockdown`. In anticipation, this uses some of the patterns +// followed by the other tamings there. + +// Emulate the internal [[WeakRefTarget]] slot. Despite the term "Weak" in the +// "WeakMap" used in the emulation, this map holds the target strongly. The only +// weakness here is that the weakref,target pair can go away together if the +// weakref is not reachable. +const weakRefTarget = new WeakMap(); + +const FakeWeakRef = function WeakRef(target) { + assert( + new.target !== undefined, + d`WeakRef Constructor requires 'new'`, + TypeError, + ); + assert.equal( + Object(target), + target, + d`WeakRef target must be an object`, + TypeError, + ); + weakRefTarget.set(this, target); +}; + +const InertWeakRef = function WeakRef(_target) { + throw new TypeError('Not available'); +}; + +const FakeWeakRefPrototype = { + deref() { + return weakRefTarget.get(this); + }, + [Symbol.toStringTag]: 'WeakRef', +}; + +defineProperties(FakeWeakRef, { + prototype: { value: FakeWeakRefPrototype }, +}); + +const WeakRef = globalThis.WeakRef || FakeWeakRef; + +// If there is a real WeakRef constructor, we still make it safe before +// exporting it. Unlike https://github.com/tc39/ecma262/issues/2214 +// rather than deleting the `constructor` property, we follow the other +// taming patterns and point it at a throw-only inert one. +defineProperties(WeakRef.prototype, { + constructor: { value: InertWeakRef }, +}); + +harden(WeakRef); + +export { WeakRef }; + +// ///////////////////////////////////////////////////////////////////////////// + +const FakeFinalizationRegistry = function FinalizationRegistry( + cleanupCallback, +) { + assert( + new.target !== undefined, + d`FinalizationRegistry Constructor requires 'new'`, + TypeError, + ); + assert.typeof( + cleanupCallback, + 'function', + d`cleanupCallback must be a function`, + ); + // fall off the end with an empty instance +}; + +const InertFinalizationRegistry = function FinalizationRegistry( + _cleanupCallback, +) { + throw new TypeError('Not available'); +}; + +const FakeFinalizationRegistryPrototype = { + register() {}, + unregister() {}, + [Symbol.toStringTag]: 'FinalizationRegistry', +}; + +defineProperties(FakeFinalizationRegistry, { + prototype: { value: FakeFinalizationRegistryPrototype }, +}); + +const FinalizationRegistry = + globalThis.FinalizationRegistry || FakeFinalizationRegistry; + +// If there is a real FinalizationRegistry constructor, we still make it safe +// before exporting it. Rather than deleting the `constructor` property, we +// follow the other taming patterns and point it at a throw-only inert one. +defineProperties(FinalizationRegistry.prototype, { + constructor: { value: InertFinalizationRegistry }, +}); + +harden(FinalizationRegistry); + +export { FinalizationRegistry };