From 2bfcbc6b2d3b2b66ceb41c6ce71e166f6fbe8cf2 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Fri, 4 Mar 2022 18:16:55 -0800 Subject: [PATCH] feat(swingset): allow object refs in create/upgrade/terminate vat This allows `E(vatAdminService).createVat(bundleID, { vatParameters })` to include object references in `vatParameters`, which are then delivered through the `dispatch.startVat()` delivery and made available to `buildRootObject(vatPowers, vatParameters)`. From within a vat, `vatPowers.exitVat(completion)` and `.exitVatWithFailure(reason)` can include object references, and they will be delivered to the parent caller's `done` promise. From outside the vat, `E(adminNode).terminateWithFailure(reason)` can take object refs in `reason` and they will be delivered to the parent caller's `done` promise. `E(adminNode).upgrade(bundleID, vatParameters)` can take object refs in `vatParameters` just like `createVat` (although `upgrade` itself is still non-functional). The kernel will maintain a refcount on each object passed through this mechanism, to keep it from being collected while in transit. This is implemented with the new "kernel device hooks" feature, which allows a device to call into the kernel and have its drefs translated into krefs. This will help with ZCF/contract vat upgrade, to pass the new contract bundlecap into the new ZCF vat via vatParameters. closes #4588 closes #4381 refs #1848 --- .../src/devices/vat-admin/device-vat-admin.js | 59 ++--- packages/SwingSet/src/kernel/kernel.js | 65 +---- .../SwingSet/src/kernel/vat-admin-hooks.js | 114 +++++++++ packages/SwingSet/src/kernel/vatTranslator.js | 12 +- packages/SwingSet/test/vat-admin/bootstrap.js | 28 ++- .../SwingSet/test/vat-admin/new-vat-13.js | 6 +- .../SwingSet/test/vat-admin/new-vat-44.js | 6 +- .../test/vat-admin/new-vat-refcount.js | 8 + .../test/vat-admin/test-create-vat.js | 227 +++++++++++++++++- .../test/vat-admin/vat-export-held.js | 7 + 10 files changed, 416 insertions(+), 116 deletions(-) create mode 100644 packages/SwingSet/src/kernel/vat-admin-hooks.js create mode 100644 packages/SwingSet/test/vat-admin/new-vat-refcount.js create mode 100644 packages/SwingSet/test/vat-admin/vat-export-held.js diff --git a/packages/SwingSet/src/devices/vat-admin/device-vat-admin.js b/packages/SwingSet/src/devices/vat-admin/device-vat-admin.js index 86a6fd22f5e7..e81d3517ba78 100644 --- a/packages/SwingSet/src/devices/vat-admin/device-vat-admin.js +++ b/packages/SwingSet/src/devices/vat-admin/device-vat-admin.js @@ -93,9 +93,6 @@ bundleID before submitting to the kernel), or (temporarily) a full bundle. export function buildDevice(tools, endowments) { const { hasBundle, getBundle, getNamedBundleID } = endowments; - const { pushCreateVatBundleEvent, pushCreateVatIDEvent } = endowments; - const { pushUpgradeVatEvent } = endowments; - const { terminate } = endowments; const { meterCreate, meterAddRemaining, meterSetThreshold, meterGet } = endowments; @@ -194,65 +191,43 @@ export function buildDevice(tools, endowments) { // D(devices.vatAdmin).createByBundle(bundle, options) -> vatID if (method === 'createByBundle') { - // see #4588 - assert.equal(argsCapdata.slots.length, 0, 'cannot handle refs'); - const args = unserialize(argsCapdata); - const [bundle, options] = args; - // options cannot hold caps yet, but I expect #4486 will remove - // this case before #4381 needs them here - const vatID = pushCreateVatBundleEvent(bundle, options); + const res = syscall.callKernelHook('createByBundle', argsCapdata); + const vatID = JSON.parse(res.body); + assert.typeof(vatID, 'string', `createByBundle gave non-VatID`); return returnFromInvoke(vatID); } // D(devices.vatAdmin).createByBundleID(bundleID, options) -> vatID if (method === 'createByBundleID') { - // see #4588 - assert.equal(argsCapdata.slots.length, 0, 'cannot handle refs'); - const args = unserialize(argsCapdata); - const [bundleID, options] = args; - assert.typeof(bundleID, 'string', `createByBundleID() bundleID`); - // TODO: options cannot hold caps yet, #4381 will want them in - // vatParameters, follow the pattern in terminateWithFailure - const vatID = pushCreateVatIDEvent(bundleID, options); + const res = syscall.callKernelHook('createByID', argsCapdata); + const vatID = JSON.parse(res.body); + assert.typeof(vatID, 'string', `createByID gave non-VatID`); return returnFromInvoke(vatID); } - // D(devices.vatAdmin).upgradeVat(bundleID, vatParameters) > upgradeID + // D(devices.vatAdmin).upgradeVat(bundleID, vatParameters) -> upgradeID if (method === 'upgradeVat') { - // see #4381, we really need to accept slots in vatParameters - // so we can pass contract bundlecaps to upgraded ZCF - const { body, slots } = argsCapdata; - // this has all the same problems described in terminateWithFailure - assert.equal(slots.length, 0, 'upgradeVat() cannot handle refs yet'); - const args = JSON.parse(body); + const args = JSON.parse(argsCapdata.body); assert(Array.isArray(args), 'upgradeVat() args array'); assert.equal(args.length, 2, `upgradeVat() args length`); - const [bundleID, vatParameters] = args; + const [bundleID, _vatParameters] = args; assert.typeof(bundleID, 'string', `upgradeVat() bundleID`); - const vpCapdata = { body: JSON.stringify(vatParameters), slots }; - const upgradeID = pushUpgradeVatEvent(bundleID, vpCapdata); + + const res = syscall.callKernelHook('upgrade', argsCapdata); + const upgradeID = JSON.parse(res.body); + assert.typeof(upgradeID, 'string', 'upgradeVat gave non-upgradeID'); return returnFromInvoke(upgradeID); } // D(devices.vatAdmin).terminateWithFailure(vatID, reason) if (method === 'terminateWithFailure') { - // 'args' is capdata([vatID, reason]), and comes from vatAdmin (but - // 'reason' comes from user code). We want to extract vatID and get - // capdata(reason) to send into terminate(). Don't use the - // deviceTools serialize(), it isn't a complete marshal and we just - // want to passthrough anyways. - const { body, slots } = argsCapdata; - // TODO: these slots are drefs, and the kernel terminate() - // endowment needs krefs, so we forbid them entirely for now - // see #4588 - assert.equal(slots.length, 0, 'cannot handle refs in terminate'); - const args = JSON.parse(body); + const args = JSON.parse(argsCapdata.body); assert(Array.isArray(args), 'terminateWithFailure() args array'); assert.equal(args.length, 2, `terminateWithFailure() args length`); - const [vatID, reason] = args; + const [vatID, _reason] = args; assert.typeof(vatID, 'string', `terminateWithFailure() vatID`); - const reasonCapdata = { body: JSON.stringify(reason), slots }; - terminate(vatID, reasonCapdata); + + syscall.callKernelHook('terminate', argsCapdata); return returnFromInvoke(undefined); } diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index 3faacaf3bfe4..56dc1a316bc3 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -23,6 +23,7 @@ import { processNextGCAction } from './gc-actions.js'; import { makeVatLoader } from './vat-loader/vat-loader.js'; import { makeDeviceTranslators } from './deviceTranslator.js'; import { notifyTermination } from './notifyTermination.js'; +import { makeVatAdminHooks } from './vat-admin-hooks.js'; function abbreviateReplacer(_, arg) { if (typeof arg === 'bigint') { @@ -548,6 +549,11 @@ export default function buildKernel( const kd = harden(['startVat', vatParameters]); // eslint-disable-next-line no-use-before-define const vd = vatWarehouse.kernelDeliveryToVatDelivery(vatID, kd); + // decref slots now that create-vat is off run-queue + for (const kref of vatParameters.slots) { + kernelKeeper.decrementRefCount(kref, 'create-vat-event'); + } + // TODO: can we provide a computron count to the run policy? const status = await deliverAndLogToVat(vatID, kd, vd); return { ...status, discardFailedDelivery: true }; @@ -1266,58 +1272,6 @@ export default function buildKernel( hasBundle: kernelKeeper.hasBundle, getBundle: kernelKeeper.getBundle, getNamedBundleID: kernelKeeper.getNamedBundleID, - pushCreateVatBundleEvent(bundle, dynamicOptions) { - // TODO: translate dynamicOptions.vatParameters.slots from dref to kref - const source = { bundle }; - const { vatParameters: rawVP, ...rest } = dynamicOptions; - const vatParameters = { body: stringify(harden(rawVP)), slots: [] }; - insistCapData(vatParameters); - dynamicOptions = rest; - const vatID = kernelKeeper.allocateUnusedVatID(); - const event = { - type: 'create-vat', - vatID, - source, - vatParameters, - dynamicOptions, - }; - kernelKeeper.addToAcceptanceQueue(harden(event)); - // the device gets the new vatID immediately, and will be notified - // later when it is created and a root object is available - return vatID; - }, - pushCreateVatIDEvent(bundleID, dynamicOptions) { - assert(kernelKeeper.hasBundle(bundleID), bundleID); - // TODO: translate dynamicOptions.vatParameters.slots from dref to kref - const source = { bundleID }; - const { vatParameters: rawVP, ...rest } = dynamicOptions; - const vatParameters = { body: stringify(harden(rawVP)), slots: [] }; - insistCapData(vatParameters); - dynamicOptions = rest; - const vatID = kernelKeeper.allocateUnusedVatID(); - const event = { - type: 'create-vat', - vatID, - source, - vatParameters, - dynamicOptions, - }; - kernelKeeper.addToAcceptanceQueue(harden(event)); - // the device gets the new vatID immediately, and will be notified - // later when it is created and a root object is available - return vatID; - }, - pushUpgradeVatEvent(bundleID, rawVP) { - const vatParameters = { body: stringify(harden(rawVP)), slots: [] }; - insistCapData(vatParameters); - const upgradeID = kernelKeeper.allocateUpgradeID(); - // TODO: translate vatParameters.slots from dref to kref - // TODO: incref both bundleID and slots in vatParameters - const ev = { type: 'upgrade-vat', upgradeID, bundleID, vatParameters }; - kernelKeeper.addToAcceptanceQueue(harden(ev)); - return upgradeID; - }, - terminate: (vatID, reason) => terminateVat(vatID, true, reason), meterCreate: (remaining, threshold) => kernelKeeper.allocateMeter(remaining, threshold), meterAddRemaining: (meterID, delta) => @@ -1386,6 +1340,13 @@ export default function buildKernel( } } + // attach vat-admin device hooks + const vatAdminDeviceID = kernelKeeper.getDeviceIDForName('vatAdmin'); + if (vatAdminDeviceID) { + const hooks = makeVatAdminHooks({ kernelKeeper, terminateVat }); + deviceHooks.set(vatAdminDeviceID, hooks); + } + kernelKeeper.loadStats(); } diff --git a/packages/SwingSet/src/kernel/vat-admin-hooks.js b/packages/SwingSet/src/kernel/vat-admin-hooks.js new file mode 100644 index 000000000000..fd62fb0a2812 --- /dev/null +++ b/packages/SwingSet/src/kernel/vat-admin-hooks.js @@ -0,0 +1,114 @@ +import { assert } from '@agoric/assert'; +import { stringify, parse } from '@endo/marshal'; +import { insistVatID } from '../lib/id.js'; + +export function makeVatAdminHooks(tools) { + const { kernelKeeper, terminateVat } = tools; + return { + createByBundle(argsCapData) { + // first, split off vatParameters + const argsJSON = JSON.parse(argsCapData.body); + const [bundle, { vatParameters: vpJSON, ...dynamicOptionsJSON }] = + argsJSON; + // assemble the vatParameters capdata + const vatParameters = { + body: JSON.stringify(vpJSON), + slots: argsCapData.slots, + }; + // then re-parse the rest with marshal + const dynamicOptions = parse(JSON.stringify(dynamicOptionsJSON)); + // incref slots while create-vat is on run-queue + for (const kref of vatParameters.slots) { + kernelKeeper.incrementRefCount(kref, 'create-vat-event'); + } + const source = { bundle }; + const vatID = kernelKeeper.allocateUnusedVatID(); + const event = { + type: 'create-vat', + vatID, + source, + vatParameters, + dynamicOptions, + }; + kernelKeeper.addToAcceptanceQueue(harden(event)); + // the device gets the new vatID immediately, and will be notified + // later when it is created and a root object is available + const vatIDCapData = { body: JSON.stringify(vatID), slots: [] }; + return harden(vatIDCapData); + }, + + createByID(argsCapData) { + // argsCapData is marshal([bundleID, options]), and options is { + // vatParameters, ...rest }, and 'rest' is JSON-serializable (no + // slots or bigints or undefined). We get the intermediate marshal + // representation (with @qclass nodes), carve off vatParameters, + // then reassemble the rest. All slots will be associated with + // vatParameters. + + // first, split off vatParameters + const argsJSON = JSON.parse(argsCapData.body); + const [bundleID, { vatParameters: vpJSON, ...dynamicOptionsJSON }] = + argsJSON; + assert(kernelKeeper.hasBundle(bundleID), bundleID); + // assemble the vatParameters capdata + const vatParameters = { + body: JSON.stringify(vpJSON), + slots: argsCapData.slots, + }; + // then re-parse the rest with marshal + const dynamicOptions = parse(JSON.stringify(dynamicOptionsJSON)); + // incref slots while create-vat is on run-queue + for (const kref of vatParameters.slots) { + kernelKeeper.incrementRefCount(kref, 'create-vat-event'); + } + const source = { bundleID }; + const vatID = kernelKeeper.allocateUnusedVatID(); + const event = { + type: 'create-vat', + vatID, + source, + vatParameters, + dynamicOptions, + }; + kernelKeeper.addToAcceptanceQueue(harden(event)); + // the device gets the new vatID immediately, and will be notified + // later when it is created and a root object is available + const vatIDCapData = { body: JSON.stringify(vatID), slots: [] }; + return harden(vatIDCapData); + }, + + upgrade(argsCapData) { + // marshal([bundleID, vatParameters]) -> upgradeID + const argsJSON = JSON.parse(argsCapData.body); + const [bundleID, vpJSON] = argsJSON; + assert.typeof(bundleID, 'string'); + const vpCD = { body: JSON.stringify(vpJSON), slots: argsCapData.slots }; + for (const kref of vpCD.slots) { + kernelKeeper.incrementRefCount(kref, 'upgrade-vat-event'); + } + const upgradeID = kernelKeeper.allocateUpgradeID(); + const ev = { + type: 'upgrade-vat', + upgradeID, + bundleID, + vatParameters: vpCD, + }; + kernelKeeper.addToAcceptanceQueue(harden(ev)); + const upgradeIDCD = { body: JSON.stringify(upgradeID), slots: [] }; + return harden(upgradeIDCD); + }, + + terminate(argsCD) { + // marshal([vatID, reason]) -> null + const argsJSON = JSON.parse(argsCD.body); + const [vatID, reasonJSON] = argsJSON; + insistVatID(vatID); + const reasonCD = { ...argsCD, body: JSON.stringify(reasonJSON) }; + // we don't need to incrementRefCount because if terminateVat sends + // 'reason' to vat-admin, it uses notifyTermination / queueToKref / + // doSend, and doSend() does its own incref + terminateVat(vatID, true, reasonCD); + return harden({ body: stringify(undefined), slots: [] }); + }, + }; +} diff --git a/packages/SwingSet/src/kernel/vatTranslator.js b/packages/SwingSet/src/kernel/vatTranslator.js index bbec336c6173..52740c17d39e 100644 --- a/packages/SwingSet/src/kernel/vatTranslator.js +++ b/packages/SwingSet/src/kernel/vatTranslator.js @@ -154,15 +154,11 @@ function makeTranslateKernelDeliveryToVatDelivery(vatID, kernelKeeper) { * @returns { VatDeliveryStartVat } */ function translateStartVat(kernelVP) { + const slots = kernelVP.slots.map(slot => mapKernelSlotToVatSlot(slot)); + const vatVP = { ...kernelVP, slots }; /** @type { VatDeliveryStartVat } */ - return harden(['startVat', kernelVP]); // TODO until capdata - // const vatVP = { - // ...kernelVP, - // slots: kernelVP.slots.map(slot => mapKernelSlotToVatSlot(slot)), - // }; - // /** @type { VatDeliveryStartVat } */ - // const startVatMessageVatDelivery = harden(['startVat', vatVP]); - // return startVatMessageVatDelivery; + const startVatMessageVatDelivery = harden(['startVat', vatVP]); + return startVatMessageVatDelivery; } function translateBringOutYourDead() { diff --git a/packages/SwingSet/test/vat-admin/bootstrap.js b/packages/SwingSet/test/vat-admin/bootstrap.js index 8b22aa06b124..08099396ff66 100644 --- a/packages/SwingSet/test/vat-admin/bootstrap.js +++ b/packages/SwingSet/test/vat-admin/bootstrap.js @@ -3,45 +3,51 @@ import { Far } from '@endo/marshal'; export function buildRootObject() { let admin; + let held; + + const adder = Far('adder', { add1: x => x + 1 }); + const options = { vatParameters: { adder } }; return Far('root', { async bootstrap(vats, devices) { admin = await E(vats.vatAdmin).createVatAdminService(devices.vatAdmin); + held = await E(vats['export-held']).createHeld(); }, async byBundle(bundle) { - const { root } = await E(admin).createVat(bundle); + const { root } = await E(admin).createVat(bundle, options); const n = await E(root).getANumber(); return n; }, async byName(bundleName) { - const { root } = await E(admin).createVatByName(bundleName); + const { root } = await E(admin).createVatByName(bundleName, options); const n = await E(root).getANumber(); return n; }, async byNamedBundleCap(name) { const bcap = await E(admin).getNamedBundleCap(name); - const { root } = await E(admin).createVat(bcap); + const { root } = await E(admin).createVat(bcap, options); const n = await E(root).getANumber(); return n; }, async byID(id) { const bcap = await E(admin).getBundleCap(id); - const { root } = await E(admin).createVat(bcap); + const { root } = await E(admin).createVat(bcap, options); const n = await E(root).getANumber(); return n; }, async counters(bundleName) { - const { root } = await E(admin).createVatByName(bundleName); + const { root } = await E(admin).createVatByName(bundleName, options); const c = E(root).createRcvr(1); const log = []; log.push(await E(c).increment(3)); log.push(await E(c).increment(5)); log.push(await E(c).ticker()); + log.push(await E(c).add2(6)); return log; }, @@ -52,5 +58,17 @@ export function buildRootObject() { async nonBundleCap() { return E(admin).createVat(Far('non-bundlecap', {})); // should reject }, + + getHeld() { + return held; + }, + + refcount(id) { + // bootstrap retains 'held' the whole time, contributing one refcount + return E(admin) + .getBundleCap(id) + .then(bcap => E(admin).createVat(bcap, { vatParameters: { held } })) + .then(() => 0); + }, }); } diff --git a/packages/SwingSet/test/vat-admin/new-vat-13.js b/packages/SwingSet/test/vat-admin/new-vat-13.js index 701ccc2142c1..45a60dbc84c2 100644 --- a/packages/SwingSet/test/vat-admin/new-vat-13.js +++ b/packages/SwingSet/test/vat-admin/new-vat-13.js @@ -1,7 +1,8 @@ import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; -export function buildRootObject(_vatPowers) { +export function buildRootObject(_vatPowers, vatParameters) { + const { adder } = vatParameters; function rcvrMaker(seed) { let count = 0; let sum = seed; @@ -11,6 +12,9 @@ export function buildRootObject(_vatPowers) { count += 1; return sum; }, + add2(val) { + return E(adder).add1(val + 1); + }, ticker() { return count; }, diff --git a/packages/SwingSet/test/vat-admin/new-vat-44.js b/packages/SwingSet/test/vat-admin/new-vat-44.js index 879344e9083b..efade3d362cd 100644 --- a/packages/SwingSet/test/vat-admin/new-vat-44.js +++ b/packages/SwingSet/test/vat-admin/new-vat-44.js @@ -1,9 +1,11 @@ +import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; -export function buildRootObject(_vatPowers) { +export function buildRootObject(_vatPowers, vatParameters) { + const { adder } = vatParameters; return Far('root', { getANumber() { - return 44; + return E(adder).add1(43); }, }); } diff --git a/packages/SwingSet/test/vat-admin/new-vat-refcount.js b/packages/SwingSet/test/vat-admin/new-vat-refcount.js new file mode 100644 index 000000000000..f0c3466dfb2e --- /dev/null +++ b/packages/SwingSet/test/vat-admin/new-vat-refcount.js @@ -0,0 +1,8 @@ +import { Far } from '@endo/marshal'; + +export function buildRootObject(_vatPowers, vatParameters) { + const { held } = vatParameters; + return Far('root', { + foo: () => held, // hold until root goes away + }); +} diff --git a/packages/SwingSet/test/vat-admin/test-create-vat.js b/packages/SwingSet/test/vat-admin/test-create-vat.js index 35d790efdbea..930cae09fd83 100644 --- a/packages/SwingSet/test/vat-admin/test-create-vat.js +++ b/packages/SwingSet/test/vat-admin/test-create-vat.js @@ -3,12 +3,14 @@ import { test } from '../../tools/prepare-test-env-ava.js'; // eslint-disable-next-line import/order import bundleSource from '@endo/bundle-source'; import { parse } from '@endo/marshal'; +import { provideHostStorage } from '../../src/controller/hostStorage.js'; import { buildKernelBundles, - buildVatController, + initializeSwingset, + makeSwingsetController, loadBasedir, } from '../../src/index.js'; -import { capargs } from '../util.js'; +import { capargs, capSlot } from '../util.js'; function nonBundleFunction(_E) { return {}; @@ -28,30 +30,54 @@ test.before(async t => { const brokenRootVatBundle = await bundleSource( new URL('broken-root-vat.js', import.meta.url).pathname, ); + const vatRefcountBundle = await bundleSource( + new URL('new-vat-refcount.js', import.meta.url).pathname, + ); const nonBundle = `${nonBundleFunction}`; const bundles = { vat13Bundle, vat44Bundle, brokenModuleVatBundle, brokenRootVatBundle, + vatRefcountBundle, nonBundle, }; t.context.data = { kernelBundles, bundles }; }); -async function doTestSetup(t) { +async function doTestSetup(t, enableSlog = false) { const { bundles, kernelBundles } = t.context.data; const config = await loadBasedir(new URL('./', import.meta.url).pathname); + config.defaultManagerType = 'xs-worker'; config.bundles = { new13: { bundle: bundles.vat13Bundle }, brokenModule: { bundle: bundles.brokenModuleVatBundle }, brokenRoot: { bundle: bundles.brokenRootVatBundle }, }; - const c = await buildVatController(config, [], { kernelBundles }); + const hostStorage = provideHostStorage(); + await initializeSwingset(config, [], hostStorage, { kernelBundles }); + let doSlog = false; + function slogSender(_, s) { + if (!doSlog) return; + const o = JSON.parse(s); + delete o.time; + delete o.replay; + delete o.crankNum; + delete o.deliveryNum; + if (['crank-start', 'deliver', 'syscall'].includes(o.type)) { + console.log(JSON.stringify(o)); + } + } + const c = await makeSwingsetController(hostStorage, {}, { slogSender }); const id44 = await c.validateAndInstallBundle(bundles.vat44Bundle); + const idRC = await c.validateAndInstallBundle(bundles.vatRefcountBundle); c.pinVatRoot('bootstrap'); await c.run(); - return { c, id44, vat13Bundle: bundles.vat13Bundle }; + if (enableSlog) { + // for debugging, set enableSlog=true to start tracing after setup + doSlog = true; + } + return { c, id44, idRC, vat13Bundle: bundles.vat13Bundle, hostStorage }; } test('createVatByBundle', async t => { @@ -94,7 +120,7 @@ test('counter test', async t => { const { c } = await doTestSetup(t); const kpid = c.queueToVatRoot('bootstrap', 'counters', capargs(['new13'])); await c.run(); - t.deepEqual(JSON.parse(c.kpResolution(kpid).body), [4, 9, 2]); + t.deepEqual(JSON.parse(c.kpResolution(kpid).body), [4, 9, 2, 8]); }); async function brokenVatTest(t, bundleName) { @@ -131,3 +157,192 @@ test('error creating vat from non-bundle', async t => { Error('Vat Creation Error: createVat() requires a bundlecap'), ); }); + +function findRefs(kvStore, koid) { + const refs = []; + const nextVatID = Number(kvStore.get('vat.nextID')); + for (let vn = 1; vn < nextVatID; vn += 1) { + const vatID = `v${vn}`; + const r = kvStore.get(`${vatID}.c.${koid}`); + if (r) { + refs.push(`${vatID}: ${r}`); + } + } + const nextDeviceID = Number(kvStore.get('device.nextID')); + for (let dn = 1; dn < nextDeviceID; dn += 1) { + const deviceID = `d${dn}`; + const r = kvStore.get(`${deviceID}.c.${koid}`); + if (r) { + refs.push(`${deviceID}: ${r}`); + } + } + const refcountString = kvStore.get(`${koid}.refCount`) || '0,0'; + const refcount = refcountString.split(',').map(Number); + return { refs, refcount }; +} + +test('createVat holds refcount', async t => { + const printSlog = false; // set true to debug this test + const { c, idRC, hostStorage } = await doTestSetup(t, printSlog); + const { kvStore } = hostStorage; + + // The bootstrap vat starts by fetching 'held' from vat-export-held, during + // doTestSetup(), and retains it throughout the entire test. When we send + // it refcount(), it will send VatAdminService.getBundleCap(), wait for the + // response, then send VatAdminService.createVat(held). VAS will tell + // device-vat-admin to push a create-vat event (including 'held') on the + // run-queue. Some time later, the create-vat event reaches the front, and + // the new vat is created, receiving 'held' in vatParametesr. + + // We want to check the refcounts during this sequence, to confirm that the + // create-vat event holds a reference. Otherwise, 'held' might get GCed + // after VAS has pushed the event but before the kernel has created the new + // vat. (Currently, this is accidentally prevented by the fact that + // deviceKeeper.mapKernelSlotToDeviceSlot does an incrementRefCount but + // nothing ever decrements it: devices hold eternal refcounts on their + // c-list entries, and nothing ever removes a device c-list entry. But some + // day when we fix that, we'll rely upon the create-vat event's refcount to + // keep these things alive. + + // We happen to know that 'held' will be allocated ko27, but we use + // `getHeld()` to obtain the real value in case e.g. a new built-in vat is + // added and some other koid is allocated. To remind us to review this test + // if/when things like that change, this test also asserts ko27, but that + // can be updated in a single place. + + // 'held' is exported by v1, which shows up in the c-lists but doesn't + // count towards the refcount + let expectedRefcount = 0; + let expectedCLists = 1; // v1-export-held + + // bootstrap() imports 'held', adding it to the v2-bootstrap c-list and + // incrementing the refcount. + expectedRefcount += 1; // v2-bootstrap + expectedCLists += 1; // v2-bootstrap + + // calling getHeld doesn't immediately increment the refcount + const kpid1 = c.queueToVatRoot('bootstrap', 'getHeld', capargs([])); + await c.run(); + const h1 = c.kpResolution(kpid1); + t.deepEqual(JSON.parse(h1.body), capSlot(0, 'held')); + const held = h1.slots[0]; + t.is(held, 'ko27'); // gleaned from the logs, unstable, update as needed + + // but `kpResolution()` does an incref on the results, making the refcount + // now 2,2: imported by v2-bootstrap and pinned by kpResolution. + expectedRefcount += 1; // kpResolution pin + const { refcount, refs } = findRefs(kvStore, held); + t.deepEqual(refcount, [expectedRefcount, expectedRefcount]); + t.is(refs.length, expectedCLists); + + // console.log(`---`); + + async function stepUntil(predicate) { + for (;;) { + // eslint-disable-next-line no-await-in-loop + const more = await c.step(); + // const { refcount, refs } = findRefs(kvStore, held); + // const rc = kvStore.get(`${held}.refCount`); + // console.log(`rc(${held}): ${refcount} ${refs.join(' , ')}`); + if (!more || predicate()) { + return; + } + } + } + + // now start refcount() and step until we see the `send(createVat)` on + // the run-queue + const kpid = c.queueToVatRoot('bootstrap', 'refcount', capargs([idRC])); + function seeDeliverCreateVat() { + // console.log('rq:', JSON.stringify(c.dump().runQueue)); + return c + .dump() + .runQueue.filter(q => q.type === 'send' && q.msg.method === 'createVat') + .length; + } + await stepUntil(seeDeliverCreateVat); + + // now we should see 3,3: v2-bootstrap, the kpResolution pin, and the + // send(createVat) arguments. Two of these are c-lists. + expectedRefcount += 1; // send(createVat) arguments + const r1 = findRefs(kvStore, held); + t.deepEqual(r1.refcount, [expectedRefcount, expectedRefcount]); + t.is(r1.refs.length, expectedCLists); + // console.log(`---`); + + // allow that to be delivered to vat-admin and step until we see the + // 'create-vat' event on the run-queue, which means vat-admin has just + // finished executing the createVat() message. We stop stepping before + // delivering bringOutYourDead to vat-admin, so it should still be holding + // the arguments. + function seeCreateVat() { + // console.log('rq:', JSON.stringify(c.dump().runQueue)); + return c.dump().runQueue.filter(q => q.type === 'create-vat').length; + } + await stepUntil(seeCreateVat); + // console.log(`---`); + + // We should see 5,5: v2-bootstrap, the kpResolution pin, vat-vat-admin, + // device-vat-admin, and the create-vat run-queue event. Three of these are + // c-lists. + expectedRefcount += 1; // vat-vat-admin c-list + expectedCLists += 1; // vat-vat-admin c-list + expectedRefcount += 1; // device-vat-admin c-list + expectedCLists += 1; // device-vat-admin c-list + + const r2 = findRefs(kvStore, held); + // console.log(`r2:`, JSON.stringify(r2)); + t.deepEqual(r2.refcount, [expectedRefcount, expectedRefcount]); + t.is(r2.refs.length, expectedCLists); + + // Allow the vat-admin bringOutYourDead to be delivered, which *ought* to + // allow it to drop its reference to 'held'. NOTE: for some reason, + // `createVat()` does not drop that reference right away. I *think* it + // holds onto them until the result promise resolves, which doesn't happen + // until `newVatCallback()` is delivered. So this -=1 is commented out + // until we figure out how to fix that.. maybe a HandledPromise thing. + + // expectedRefcount -= 1; // vat-vat-admin retires + // expectedCLists -= 1; // vat-vat-admin retires + + // In addition, device-vat-admin does not yet participate in GC, and holds + // its references forever. So this -=1 remains commented out until we + // implement that functionality. + + // expectedRefcount -= 1; // device-vat-admin retires + // expectedCLists -= 1; // device-vat-admin retires + + t.deepEqual(c.dump().reapQueue, ['v3']); + await c.step(); + t.deepEqual(c.dump().reapQueue, []); + // console.log(`---`); + + // At this point we expected to see 5,5: v2-bootstrap, kpResolution pin, + // vat-vat-admin (because of the non-dropping bug), device-vat-admin + // (because of unimplemented GC), and the create-vat run-queue event. Two + // are c-lists. + + const r3 = findRefs(kvStore, held); + // console.log(`r3:`, JSON.stringify(r3)); + t.deepEqual(r3.refcount, [expectedRefcount, expectedRefcount]); + t.is(r3.refs.length, expectedCLists); + + // Allow create-vat to be processed, removing the create-vat reference and + // adding a reference from the new vat's c-list + await c.step(); + expectedRefcount -= 1; // remove send(createVat) argument + expectedRefcount += 1; // new-vat c-list + expectedCLists += 1; // new-vat c-list + // console.log(`---`); + + // v2-bootstrap, kpResolution pin, device-vat-admin, new-vat + const r4 = findRefs(kvStore, held); + // console.log(`r4:`, JSON.stringify(r4)); + t.deepEqual(r4.refcount, [expectedRefcount, expectedRefcount]); + t.is(r4.refs.length, expectedCLists); + + // now let everything finish + // await c.run(); + await stepUntil(() => false); + t.deepEqual(JSON.parse(c.kpResolution(kpid).body), 0); +}); diff --git a/packages/SwingSet/test/vat-admin/vat-export-held.js b/packages/SwingSet/test/vat-admin/vat-export-held.js new file mode 100644 index 000000000000..dde54163e9dd --- /dev/null +++ b/packages/SwingSet/test/vat-admin/vat-export-held.js @@ -0,0 +1,7 @@ +import { Far } from '@endo/marshal'; + +export function buildRootObject() { + return Far('root', { + createHeld: () => Far('held', {}), + }); +}