Skip to content

Commit

Permalink
feat(swingset): add syscall.abandonExports
Browse files Browse the repository at this point in the history
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
  • Loading branch information
warner authored and mergify-bot committed Mar 31, 2022
1 parent 8356c2d commit 4bd1a8b
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 7 deletions.
8 changes: 8 additions & 0 deletions packages/SwingSet/docs/garbage-collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions packages/SwingSet/src/kernel/kernelSyscall.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
18 changes: 14 additions & 4 deletions packages/SwingSet/src/kernel/state/kernelKeeper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -1505,6 +1514,7 @@ export default function makeKernelKeeper(
ownerOfKernelDevice,
kernelObjectExists,
getImporters,
orphanKernelObject,
deleteKernelObject,
pinObject,

Expand Down
25 changes: 25 additions & 0 deletions packages/SwingSet/src/kernel/vatTranslator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]}`);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/SwingSet/src/lib/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions packages/SwingSet/src/supervisors/supervisor-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions packages/SwingSet/src/types-external.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
184 changes: 184 additions & 0 deletions packages/SwingSet/test/test-abandon-export.js
Original file line number Diff line number Diff line change
@@ -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);
});
5 changes: 5 additions & 0 deletions packages/SwingSet/test/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -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}`);
}
Expand Down

0 comments on commit 4bd1a8b

Please sign in to comment.