From 4bd1a8b3c03966f0970c7055609e24929d8ac01c Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 29 Mar 2022 19:02:12 -0700 Subject: [PATCH] feat(swingset): add syscall.abandonExports This allows liveslots to abandon a previously-exported object. The kernel marks the object as orphaned (just as if the exporting vat was terminated), and deletes the exporter's c-list entry. All importing vats continue to have the same access as before, and the refcounts are unchanged. Liveslots will use this during `stopVat()` to revoke all the non-durable objects that it had exported, since these objects won't survive the upgrade. The vat version being stopped may still have a Remotable or a virtual form of the export, so userspace must not be allowed to execute after this syscall is used, otherwise it might try to mention the export again, which would allocate a new mismatched kref, causing confusion and storage leaks. Our naming scheme would normally call this `syscall.dropExports` rather than `syscall.abandonExports`, but I figured this is sufficiently unusual that it deserved a more emphatic name. Vat exports are an obligation, and this syscall allows a vat to shirk that obligation. closes #4951 refs #1848 --- packages/SwingSet/docs/garbage-collection.md | 8 + packages/SwingSet/src/kernel/kernelSyscall.js | 12 ++ .../SwingSet/src/kernel/state/kernelKeeper.js | 18 +- packages/SwingSet/src/kernel/vatTranslator.js | 25 +++ packages/SwingSet/src/lib/message.js | 3 +- .../src/supervisors/supervisor-helper.js | 1 + packages/SwingSet/src/types-external.js | 7 +- packages/SwingSet/test/test-abandon-export.js | 184 ++++++++++++++++++ packages/SwingSet/test/util.js | 5 + 9 files changed, 256 insertions(+), 7 deletions(-) create mode 100644 packages/SwingSet/test/test-abandon-export.js diff --git a/packages/SwingSet/docs/garbage-collection.md b/packages/SwingSet/docs/garbage-collection.md index 95b4bca0b26..b4bf3dcb2a9 100644 --- a/packages/SwingSet/docs/garbage-collection.md +++ b/packages/SwingSet/docs/garbage-collection.md @@ -358,6 +358,14 @@ At this point, the kref is only referenced in the queued `retireImport` action a If the last importing vat had previously called `syscall.retireImport`, there will be no subscribers, but the kernel object data will still be present (a general invariant is that the exporting clist entry and the kernel object table entry are either both present or both missing). A `dispatch.retireExport` will be on the queue for the exporting vat, but it has not yet arrived (otherwise it would be illegal for the exporting vat to call `syscall.retireExport`). That `dispatch.retireExport` GC action will be nullified during `processOneGCAction` because its work was already performed by the exporter's `syscall.retireExport`. +## syscall.abandonExport processing + +During vat upgrade, the last delivery made to the old version is a special `dispatch.stopVat()`. This instructs the soon-to-be-deleted worker to destroy anything that will not survive the upgrade. All Remotables will be abandoned here, because they live solely in RAM, and the RAM heap does not survive. All non-durable virtual objects will also be abandoned, since only durable ones survive. We may also drop otherwise-durable objects which were only referenced by abandoned non-durable objects. + +During this phase, the vat performs a `syscall.abandonExports()` with all the abandoned vrefs. The kernel reacts to this in roughly the same way it reacts to the entire vat being terminated (which it is, sort of, at least the heap is being terminated). Each vref is deleted from the exporting vat's c-list, because *that* vat isn't going to be referencing it any more. The kernel object table is updated to clear the `owner` field: while the kernel object retains its identity, it is now orphaned, and any messages sent to it will be rejected (by the kernel) with a "vat terminated" error. + +No other work needs to be done. Any importing vats will continue to hold their reference as before. They can only tell that the object has been abandoned if they try to send it a message. Eventually, if all the importing vats drop their reference, and nothing else in the kernel is holding one, the kernel object entry will be deleted. In this case, no `dispatch.retireExports` is sent to the old exporting vat, since it's already been removed from their c-list. + ## Post-Decref Processing Those three syscalls may cause some krefs to become eligible for release. The "kernelKeeper" tracks these krefs in an ephemeral `Set` named `maybeFreeKrefs`. Every time a decrement causes the reachable count to transition from 1 to 0, or the recognizable count to transition from 1 to 0, the kref is added to this set. diff --git a/packages/SwingSet/src/kernel/kernelSyscall.js b/packages/SwingSet/src/kernel/kernelSyscall.js index c2d48f87cfd..dcf45d822be 100644 --- a/packages/SwingSet/src/kernel/kernelSyscall.js +++ b/packages/SwingSet/src/kernel/kernelSyscall.js @@ -299,6 +299,14 @@ export function makeKernelSyscallHandler(tools) { return OKNULL; } + function abandonExports(vatID, koids) { + assert(Array.isArray(koids), X`abandonExports given non-Array ${koids}`); + for (const koid of koids) { + kernelKeeper.orphanKernelObject(koid, vatID); + } + return OKNULL; + } + // callKernelHook is only available to devices function callKernelHook(deviceID, hookName, args) { @@ -369,6 +377,10 @@ export function makeKernelSyscallHandler(tools) { const [_, ...args] = ksc; return retireExports(...args); } + case 'abandonExports': { + const [_, ...args] = ksc; + return abandonExports(...args); + } case 'callKernelHook': { const [_, ...args] = ksc; return callKernelHook(...args); diff --git a/packages/SwingSet/src/kernel/state/kernelKeeper.js b/packages/SwingSet/src/kernel/state/kernelKeeper.js index 0138d9740aa..ff5ade55dba 100644 --- a/packages/SwingSet/src/kernel/state/kernelKeeper.js +++ b/packages/SwingSet/src/kernel/state/kernelKeeper.js @@ -527,6 +527,13 @@ export default function makeKernelKeeper( return owner; } + function orphanKernelObject(kref, oldVat) { + const ownerKey = `${kref}.owner`; + const ownerVat = kvStore.get(ownerKey); + assert.equal(ownerVat, oldVat, `export ${kref} not owned by old vat`); + kvStore.delete(ownerKey); + } + function deleteKernelObject(koid) { kvStore.delete(`${koid}.owner`); kvStore.delete(`${koid}.refCount`); @@ -743,10 +750,7 @@ export default function makeKernelKeeper( // must also delete the corresponding kernel owner entry for the object, // since the object will no longer be accessible. const kref = kvStore.get(k); - const ownerKey = `${kref}.owner`; - const ownerVat = kvStore.get(ownerKey); - assert.equal(ownerVat, vatID, `export ${kref} not owned by late vat`); - kvStore.delete(ownerKey); + orphanKernelObject(kref, vatID); } // then scan for imported objects, which must be decrefed @@ -1220,6 +1224,11 @@ export default function makeKernelKeeper( // assert.equal(isReachable, false, `${kref} is reachable but not recognizable`); actions.add(`${ownerVatID} retireExport ${kref}`); } + } else if (recognizable === 0) { + // unreachable, unrecognizable, orphaned: delete the + // empty refcount here, since we can't send a GC + // action without an ownerVatID + deleteKernelObject(kref); } } } @@ -1505,6 +1514,7 @@ export default function makeKernelKeeper( ownerOfKernelDevice, kernelObjectExists, getImporters, + orphanKernelObject, deleteKernelObject, pinObject, diff --git a/packages/SwingSet/src/kernel/vatTranslator.js b/packages/SwingSet/src/kernel/vatTranslator.js index 47667145711..299be05a34b 100644 --- a/packages/SwingSet/src/kernel/vatTranslator.js +++ b/packages/SwingSet/src/kernel/vatTranslator.js @@ -436,6 +436,27 @@ function makeTranslateVatSyscallToKernelSyscall(vatID, kernelKeeper) { return harden(['retireExports', krefs]); } + /** + * + * @param { string[] } vrefs + * @returns { import('../types-external.js').KernelSyscallAbandonExports } + */ + function translateAbandonExports(vrefs) { + assert(Array.isArray(vrefs), X`abandonExports() given non-Array ${vrefs}`); + const krefs = vrefs.map(vref => { + const { type, allocatedByVat } = parseVatSlot(vref); + assert.equal(type, 'object'); + assert.equal(allocatedByVat, true); // abandon *exports*, not imports + // kref must already be in the clist + const kref = mapVatSlotToKernelSlot(vref, gcSyscallMapOpts); + vatKeeper.deleteCListEntry(kref, vref); + return kref; + }); + kdebug(`syscall[${vatID}].abandonExports(${krefs.join(' ')})`); + // abandonExports still has work to do + return harden(['abandonExports', vatID, krefs]); + } + /** * * @param { string } target @@ -561,6 +582,10 @@ function makeTranslateVatSyscallToKernelSyscall(vatID, kernelKeeper) { const [_, ...args] = vsc; return translateRetireExports(...args); } + case 'abandonExports': { + const [_, ...args] = vsc; + return translateAbandonExports(...args); + } default: assert.fail(X`unknown vatSyscall type ${vsc[0]}`); } diff --git a/packages/SwingSet/src/lib/message.js b/packages/SwingSet/src/lib/message.js index f6660e026dc..4b82bc54905 100644 --- a/packages/SwingSet/src/lib/message.js +++ b/packages/SwingSet/src/lib/message.js @@ -177,7 +177,8 @@ export function insistVatSyscallObject(vso) { } case 'dropImports': case 'retireImports': - case 'retireExports': { + case 'retireExports': + case 'abandonExports': { const [slots] = rest; assert(Array.isArray(slots)); for (const slot of slots) { diff --git a/packages/SwingSet/src/supervisors/supervisor-helper.js b/packages/SwingSet/src/supervisors/supervisor-helper.js index 191e8c0ad57..00feba2b54d 100644 --- a/packages/SwingSet/src/supervisors/supervisor-helper.js +++ b/packages/SwingSet/src/supervisors/supervisor-helper.js @@ -106,6 +106,7 @@ function makeSupervisorSyscall(syscallToManager, workerCanBlock) { dropImports: vrefs => doSyscall(['dropImports', vrefs]), retireImports: vrefs => doSyscall(['retireImports', vrefs]), retireExports: vrefs => doSyscall(['retireExports', vrefs]), + abandonExports: vrefs => doSyscall(['abandonExports', vrefs]), // These syscalls should be omitted if the worker cannot get a // synchronous return value back from the kernel, such as when the worker diff --git a/packages/SwingSet/src/types-external.js b/packages/SwingSet/src/types-external.js index 15f2beef1f9..0b23d4d0db9 100644 --- a/packages/SwingSet/src/types-external.js +++ b/packages/SwingSet/src/types-external.js @@ -118,11 +118,12 @@ export {}; * @typedef { [tag: 'dropImports', slots: string[] ]} VatSyscallDropImports * @typedef { [tag: 'retireImports', slots: string[] ]} VatSyscallRetireImports * @typedef { [tag: 'retireExports', slots: string[] ]} VatSyscallRetireExports + * @typedef { [tag: 'abandonExports', slots: string[] ]} VatSyscallAbandonExports * * @typedef { VatSyscallSend | VatSyscallCallNow | VatSyscallSubscribe * | VatSyscallResolve | VatSyscallExit | VatSyscallVatstoreGet | VatSyscallVatstoreGetAfter * | VatSyscallVatstoreSet | VatSyscallVatstoreDelete | VatSyscallDropImports - * | VatSyscallRetireImports | VatSyscallRetireExports + * | VatSyscallRetireImports | VatSyscallRetireExports | VatSyscallAbandonExports * } VatSyscallObject * * @typedef { [tag: 'ok', data: SwingSetCapData | string | string[] | undefined[] | null ]} VatSyscallResultOk @@ -156,12 +157,14 @@ export {}; * @typedef { [tag: 'dropImports', krefs: string[] ]} KernelSyscallDropImports * @typedef { [tag: 'retireImports', krefs: string[] ]} KernelSyscallRetireImports * @typedef { [tag: 'retireExports', krefs: string[] ]} KernelSyscallRetireExports + * @typedef { [tag: 'abandonExports', vatID: string, krefs: string[] ]} KernelSyscallAbandonExports * @typedef { [tag: 'callKernelHook', hookName: string, args: SwingSetCapData]} KernelSyscallCallKernelHook * * @typedef { KernelSyscallSend | KernelSyscallInvoke | KernelSyscallSubscribe * | KernelSyscallResolve | KernelSyscallExit | KernelSyscallVatstoreGet | KernelSyscallVatstoreGetAfter * | KernelSyscallVatstoreSet | KernelSyscallVatstoreDelete | KernelSyscallDropImports - * | KernelSyscallRetireImports | KernelSyscallRetireExports | KernelSyscallCallKernelHook + * | KernelSyscallRetireImports | KernelSyscallRetireExports | KernelSyscallAbandonExports + * | KernelSyscallCallKernelHook * } KernelSyscallObject * @typedef { [tag: 'ok', data: SwingSetCapData | string | string[] | undefined[] | null ]} KernelSyscallResultOk * @typedef { [tag: 'error', err: string ] } KernelSyscallResultError diff --git a/packages/SwingSet/test/test-abandon-export.js b/packages/SwingSet/test/test-abandon-export.js new file mode 100644 index 00000000000..3f15330176b --- /dev/null +++ b/packages/SwingSet/test/test-abandon-export.js @@ -0,0 +1,184 @@ +/* eslint-disable import/order */ +import { test } from '../tools/prepare-test-env-ava.js'; + +import { parse } from '@endo/marshal'; +import buildKernel from '../src/kernel/index.js'; +import { initializeKernel } from '../src/controller/initializeKernel.js'; +import { + makeKernelEndowments, + buildDispatch, + capargs, + capargsOneSlot, +} from './util.js'; + +function makeKernel() { + const endowments = makeKernelEndowments(); + const { kvStore } = endowments.hostStorage; + initializeKernel({}, endowments.hostStorage); + const kernel = buildKernel(endowments, {}, {}); + return { kernel, kvStore }; +} + +async function doAbandon(t, reachable) { + // vatA receives an object from vatB, holds it or drops it + // vatB abandons it + // vatA should retain the object + // sending to the abandoned object should get an error + const { kernel, kvStore } = makeKernel(); + await kernel.start(); + + const { log: logA, dispatch: dispatchA } = buildDispatch(); + let syscallA; + function setupA(syscall) { + syscallA = syscall; + return dispatchA; + } + await kernel.createTestVat('vatA', setupA); + const vatA = kernel.vatNameToID('vatA'); + const aliceKref = kernel.getRootObject(vatA); + kernel.pinObject(aliceKref); + + const { log: logB, dispatch: dispatchB } = buildDispatch(); + let syscallB; + function setupB(syscall) { + syscallB = syscall; + return dispatchB; + } + await kernel.createTestVat('vatB', setupB); + const vatB = kernel.vatNameToID('vatB'); + const bobKref = kernel.getRootObject(vatB); + kernel.pinObject(bobKref); + + await kernel.run(); + + async function flushDeliveries() { + // make a dummy delivery to vatA, so the kernel will call + // processRefcounts(), this isn't normally needed but we're + // calling the syscall object directly here + kernel.queueToKref(aliceKref, 'flush', capargs([])); + await kernel.run(); + t.truthy(logA.length >= 1); + const f = logA.shift(); + t.is(f.type, 'deliver'); + t.is(f.method, 'flush'); + } + + // introduce B to A, so it can send 'holdThis' later + kernel.queueToKref(bobKref, 'exportToA', capargsOneSlot(aliceKref), 'none'); + await kernel.run(); + t.is(logB.length, 1); + t.is(logB[0].type, 'deliver'); + t.is(logB[0].method, 'exportToA'); + const aliceForBob = logB[0].args.slots[0]; // probably o-50 + logB.length = 0; + + // tell B to export 'target' to A, so it gets a c-list and refcounts + const targetForBob = 'o+100'; + syscallB.send(aliceForBob, 'holdThis', capargsOneSlot(targetForBob)); + await kernel.run(); + + t.is(logA.length, 1); + t.is(logA[0].type, 'deliver'); + t.is(logA[0].method, 'holdThis'); + const targetForAlice = logA[0].args.slots[0]; + const targetKref = kvStore.get(`${vatA}.c.${targetForAlice}`); + t.regex(targetKref, /^ko\d+$/); + logA.length = 0; + + let targetOwner = kvStore.get(`${targetKref}.owner`); + let targetRefCount = kvStore.get(`${targetKref}.refCount`); + let expectedRefCount = '1,1'; // reachable+recognizable by vatA + t.is(targetOwner, vatB); + t.is(targetRefCount, expectedRefCount); + + // vatA can send a message to the target + const p1ForAlice = 'p+1'; // left unresolved because vatB is lazy + syscallA.send(targetForAlice, 'ping', capargs([]), p1ForAlice); + await flushDeliveries(); + t.is(logB.length, 1); + t.is(logB[0].type, 'deliver'); + t.is(logB[0].method, 'ping'); + logB.length = 0; + + if (!reachable) { + // vatA drops, but does not retire + syscallA.dropImports([targetForAlice]); + await flushDeliveries(); + // vatB gets a dispatch.dropExports + t.is(logB.length, 1); + t.deepEqual(logB[0], { type: 'dropExports', vrefs: [targetForBob] }); + logB.length = 0; + // the object still exists, now only recognizable + targetOwner = kvStore.get(`${targetKref}.owner`); + targetRefCount = kvStore.get(`${targetKref}.refCount`); + t.is(targetOwner, vatB); + expectedRefCount = '0,1'; + t.is(targetRefCount, expectedRefCount); // merely recognizable + } + + // now have vatB abandon the export + syscallB.abandonExports([targetForBob]); + await flushDeliveries(); + + // vatA is not informed (no GC messages) + t.is(logA.length, 0); + // vatB isn't either + t.is(logB.length, 0); + + targetOwner = kvStore.get(`${targetKref}.owner`); + targetRefCount = kvStore.get(`${targetKref}.refCount`); + t.is(targetOwner, undefined); + t.is(targetRefCount, expectedRefCount); // unchanged + + if (reachable) { + // vatA can send a message, but it will reject + const p2ForAlice = 'p+2'; // rejected by kernel + syscallA.send(targetForAlice, 'ping2', capargs([]), p2ForAlice); + syscallA.subscribe(p2ForAlice); + await flushDeliveries(); + + t.is(logB.length, 0); + t.is(logA.length, 1); + t.is(logA[0].type, 'notify'); + t.is(logA[0].resolutions.length, 1); + const [vpid, rejected, data] = logA[0].resolutions[0]; + t.is(vpid, p2ForAlice); + t.is(rejected, true); + t.deepEqual(data.slots, []); + // TODO: the kernel knows !owner but doesn't remember whether it was + // an upgrade or a termination that revoked the object, so the error + // message is a bit misleading + t.deepEqual(parse(data.body), Error('vat terminated')); + logA.length = 0; + } + + if (reachable) { + // now vatA drops the object + syscallA.dropImports([targetForAlice]); + await flushDeliveries(); + // vatB should not get a dispatch.dropImports + t.is(logB.length, 0); + // the object still exists, now only recognizable + targetRefCount = kvStore.get(`${targetKref}.refCount`); + expectedRefCount = '0,1'; + t.is(targetRefCount, expectedRefCount); // merely recognizable + } + + // now vatA retires the object too + syscallA.retireImports([targetForAlice]); + await flushDeliveries(); + // vatB should not get a dispatch.retireImports + t.is(logB.length, 0); + // the object no longer exists + targetRefCount = kvStore.get(`${targetKref}.refCount`); + expectedRefCount = undefined; + t.is(targetRefCount, expectedRefCount); // gone entirely +} + +test('abandon reachable object', async t => { + return doAbandon(t, true); +}); + +test('abandon recognizable object', async t => { + return doAbandon(t, false); +}); diff --git a/packages/SwingSet/test/util.js b/packages/SwingSet/test/util.js index 2ae9c4231ae..54f34e9ee04 100644 --- a/packages/SwingSet/test/util.js +++ b/packages/SwingSet/test/util.js @@ -46,6 +46,8 @@ export function dumpKT(kernel) { export function buildDispatch(onDispatchCallback = undefined) { const log = []; + const GC = ['dropExports', 'retireExports', 'retireImports']; + function dispatch(vatDeliverObject) { const [type, ...vdoargs] = vatDeliverObject; if (type === 'message') { @@ -71,6 +73,9 @@ export function buildDispatch(onDispatchCallback = undefined) { } } else if (type === 'startVat') { // ignore + } else if (GC.includes(type)) { + const [vrefs] = vdoargs; + log.push({ type, vrefs }); } else { throw Error(`unknown vatDeliverObject type ${type}`); }