From b2d073d93443862fffb1fecf28c491141f9222f2 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 19 May 2021 00:23:11 -0700 Subject: [PATCH] feat(SwingSet): add "reachable" flag to clist entries The kernel-to-vat clist entry for `kref` is stored in the kernelDB under a key of `${vatID}.c.${kref}` . This changes the value at that key from `${vref}` to `${flag} ${vref}`, where `flag` is either `R` (for "reachable and recognizable") or `_` (for "recognizable but not reachable). The "neither reachable nor recognizable" state simply deletes the clist entry altogether. This adds vatKeeper methods to set and clear the flag on a given kref. These will be used by the GC syscall handlers for dropImport and friends. closes #3108 --- .../SwingSet/src/kernel/state/kernelKeeper.js | 4 +- .../SwingSet/src/kernel/state/vatKeeper.js | 97 ++++++++++++++++--- packages/SwingSet/test/test-clist.js | 89 +++++++++++++++++ 3 files changed, 173 insertions(+), 17 deletions(-) create mode 100644 packages/SwingSet/test/test-clist.js diff --git a/packages/SwingSet/src/kernel/state/kernelKeeper.js b/packages/SwingSet/src/kernel/state/kernelKeeper.js index 5364155028bd..8f90dcaa412b 100644 --- a/packages/SwingSet/src/kernel/state/kernelKeeper.js +++ b/packages/SwingSet/src/kernel/state/kernelKeeper.js @@ -42,7 +42,9 @@ const enableKernelPromiseGC = true; // v$NN.o.nextID = $NN // v$NN.p.nextID = $NN // v$NN.d.nextID = $NN -// v$NN.c.$kernelSlot = $vatSlot = o+$NN/o-$NN/p+$NN/p-$NN/d+$NN/d-$NN +// v$NN.c.$kernelSlot = '$R $vatSlot' +// $R is 'R' when reachable, '_' when merely recognizable +// $vatSlot is one of: o+$NN/o-$NN/p+$NN/p-$NN/d+$NN/d-$NN // v$NN.c.$vatSlot = $kernelSlot = ko$NN/kp$NN/kd$NN // v$NN.t.$NN = JSON(transcript entry) // v$NN.t.nextID = $NN diff --git a/packages/SwingSet/src/kernel/state/vatKeeper.js b/packages/SwingSet/src/kernel/state/vatKeeper.js index 22f4538f433f..ad6461e968df 100644 --- a/packages/SwingSet/src/kernel/state/vatKeeper.js +++ b/packages/SwingSet/src/kernel/state/vatKeeper.js @@ -78,23 +78,61 @@ export function makeVatKeeper( return harden({ source, options }); } + function parseReachableAndVatSlot(value) { + assert.typeof(value, 'string', X`non-string value: ${value}`); + const flag = value.slice(0, 1); + assert.equal(value.slice(1, 2), ' '); + const vatSlot = value.slice(2); + let reachable; + if (flag === 'R') { + reachable = true; + } else if (flag === '_') { + reachable = false; + } else { + assert(`flag (${flag}) must be 'R' or '_'`); + } + return { reachable, vatSlot }; + } + + function buildReachableAndVatSlot(reachable, vatSlot) { + return `${reachable ? 'R' : '_'} ${vatSlot}`; + } + + function getReachableAndVatSlot(kernelSlot) { + const kernelKey = `${vatID}.c.${kernelSlot}`; + return parseReachableAndVatSlot(storage.get(kernelKey)); + } + + function setReachableFlag(kernelSlot) { + const kernelKey = `${vatID}.c.${kernelSlot}`; + const { vatSlot } = parseReachableAndVatSlot(storage.get(kernelKey)); + storage.set(kernelKey, buildReachableAndVatSlot(true, vatSlot)); + } + + function clearReachableFlag(kernelSlot) { + const kernelKey = `${vatID}.c.${kernelSlot}`; + const { vatSlot } = parseReachableAndVatSlot(storage.get(kernelKey)); + storage.set(kernelKey, buildReachableAndVatSlot(false, vatSlot)); + } + /** - * Provide the kernel slot corresponding to a given vat slot, creating the - * kernel slot if it doesn't already exist. + * Provide the kernel slot corresponding to a given vat slot, allocating a + * new one (for exports only) if it doesn't already exist. If we're allowed + * to allocate, we also ensure the 'reachable' flag is set on it (whether + * we allocated a new one or used an existing one). If we're not allowed to + * allocate, we insist that the reachable flag was already set. * * @param {string} vatSlot The vat slot of interest - * + * @param {bool} setReachable Set the 'reachable' flag on vat exports. * @returns {string} the kernel slot that vatSlot maps to - * * @throws {Error} if vatSlot is not a kind of thing that can be exported by vats - * or is otherwise invalid. + * or is otherwise invalid. */ - function mapVatSlotToKernelSlot(vatSlot) { + function mapVatSlotToKernelSlot(vatSlot, setReachable = true) { assert.typeof(vatSlot, 'string', X`non-string vatSlot: ${vatSlot}`); + const { type, allocatedByVat } = parseVatSlot(vatSlot); const vatKey = `${vatID}.c.${vatSlot}`; if (!storage.has(vatKey)) { - const { type, allocatedByVat } = parseVatSlot(vatSlot); - if (allocatedByVat) { let kernelSlot; if (type === 'object') { @@ -109,7 +147,10 @@ export function makeVatKeeper( incrementRefCount(kernelSlot, `${vatID}|vk|clist`); const kernelKey = `${vatID}.c.${kernelSlot}`; incStat('clistEntries'); - storage.set(kernelKey, vatSlot); + // we add the key as "unreachable", and then rely on + // setReachableFlag() at the end to both mark it reachable and to + // update any necessary refcounts consistently + storage.set(kernelKey, buildReachableAndVatSlot(false, vatSlot)); storage.set(vatKey, kernelSlot); kernelSlog && kernelSlog.changeCList( @@ -126,8 +167,19 @@ export function makeVatKeeper( assert.fail(X`unknown vatSlot ${q(vatSlot)}`); } } + const kernelSlot = storage.get(vatKey); - return storage.get(vatKey); + if (setReachable) { + const { reachable } = getReachableAndVatSlot(kernelSlot); + if (allocatedByVat) { + // exports are marked as reachable, if they weren't already + setReachableFlag(kernelSlot); + } else { + // imports must be reachable + assert(reachable, X`vat tried to access unreachable import`); + } + } + return kernelSlot; } /** @@ -135,13 +187,12 @@ export function makeVatKeeper( * creating the vat slot if it doesn't already exist. * * @param {string} kernelSlot The kernel slot of interest - * + * @param {bool} setReachable Set the 'reachable' flag on vat imports. * @returns {string} the vat slot kernelSlot maps to - * * @throws {Error} if kernelSlot is not a kind of thing that can be imported by vats - * or is otherwise invalid. + * or is otherwise invalid. */ - function mapKernelSlotToVatSlot(kernelSlot) { + function mapKernelSlotToVatSlot(kernelSlot, setReachable = true) { assert.typeof(kernelSlot, 'string', 'non-string kernelSlot'); const kernelKey = `${vatID}.c.${kernelSlot}`; if (!storage.has(kernelKey)) { @@ -166,7 +217,7 @@ export function makeVatKeeper( const vatKey = `${vatID}.c.${vatSlot}`; incStat('clistEntries'); storage.set(vatKey, kernelSlot); - storage.set(kernelKey, vatSlot); + storage.set(kernelKey, buildReachableAndVatSlot(false, vatSlot)); kernelSlog && kernelSlog.changeCList( vatID, @@ -178,7 +229,19 @@ export function makeVatKeeper( kdebug(`Add mapping k->v ${kernelKey}<=>${vatKey}`); } - return storage.get(kernelKey); + const { reachable, vatSlot } = getReachableAndVatSlot(kernelSlot); + const { allocatedByVat } = parseVatSlot(vatSlot); + if (setReachable) { + if (!allocatedByVat) { + // imports are marked as reachable, if they weren't already + setReachableFlag(kernelSlot); + } else { + // if the kernel is sending non-reachable exports back into + // exporting vat, that's a kernel bug + assert(reachable, X`kernel sent unreachable export`); + } + } + return vatSlot; } /** @@ -293,6 +356,8 @@ export function makeVatKeeper( getSourceAndOptions, mapVatSlotToKernelSlot, mapKernelSlotToVatSlot, + setReachableFlag, + clearReachableFlag, hasCListEntry, deleteCListEntry, deleteCListEntriesForKernelSlots, diff --git a/packages/SwingSet/test/test-clist.js b/packages/SwingSet/test/test-clist.js new file mode 100644 index 000000000000..d9133486cc95 --- /dev/null +++ b/packages/SwingSet/test/test-clist.js @@ -0,0 +1,89 @@ +import { test } from '../tools/prepare-test-env-ava'; + +// eslint-disable-next-line import/order +import { initSwingStore } from '@agoric/swing-store-simple'; +import { makeDummySlogger } from '../src/kernel/slogger'; +import makeKernelKeeper from '../src/kernel/state/kernelKeeper'; +import { wrapStorage } from '../src/kernel/state/storageWrapper'; + +test(`clist reachability`, async t => { + const slog = makeDummySlogger({}); + const { enhancedCrankBuffer: s } = wrapStorage(initSwingStore().storage); + + const kk = makeKernelKeeper(s, slog); + kk.createStartingKernelState('local'); + const vatID = kk.allocateUnusedVatID(); + const vk = kk.allocateVatKeeper(vatID); + + t.is(vk.mapKernelSlotToVatSlot('ko1'), 'o-50'); + t.is(vk.mapKernelSlotToVatSlot('ko2'), 'o-51'); + t.is(vk.mapKernelSlotToVatSlot('ko1'), 'o-50'); + + t.is(vk.mapVatSlotToKernelSlot('o+1'), 'ko20'); + t.is(vk.mapVatSlotToKernelSlot('o+2'), 'ko21'); + t.is(vk.mapVatSlotToKernelSlot('o+1'), 'ko20'); + + t.is(s.get(`${vatID}.c.o+1`), `ko20`); + t.is(s.get(`${vatID}.c.ko20`), 'R o+1'); + + // newly allocated imports are marked as reachable + t.is(s.get(`${vatID}.c.ko1`), 'R o-50'); + // now pretend that the vat drops its o-50 import: the syscall.dropImport + // will cause the kernel to clear the reachability flag + vk.clearReachableFlag('ko1'); + t.is(s.get(`${vatID}.c.ko1`), '_ o-50'); + // while dropped, the vat may not access the import + t.throws(() => vk.mapVatSlotToKernelSlot('o-50'), { + message: /vat tried to access unreachable import/, + }); + + // now the kernel sends a new message that references ko1, causing a + // re-import + vk.setReachableFlag('ko1'); + t.is(s.get(`${vatID}.c.ko1`), 'R o-50'); + // re-import without intervening dropImport is idempotent + vk.setReachableFlag('ko1'); + t.is(s.get(`${vatID}.c.ko1`), 'R o-50'); + + // test the same thing for exports + t.is(s.get(`${vatID}.c.ko20`), 'R o+1'); + // kernel tells vat that kernel is dropping the export, clearing the + // reachability flag + vk.clearReachableFlag('ko20'); + t.is(s.get(`${vatID}.c.ko20`), '_ o+1'); + // while dropped, access by the kernel indicates a bug + t.throws(() => vk.mapKernelSlotToVatSlot('ko20'), { + message: /kernel sent unreachable export/, + }); + + // vat re-exports o+1 + vk.setReachableFlag('ko20'); + t.is(s.get(`${vatID}.c.ko20`), 'R o+1'); + // re-export without intervening dropExport is idempotent + vk.setReachableFlag('ko20'); + t.is(s.get(`${vatID}.c.ko20`), 'R o+1'); + + // test setReachable=false, used by handlers for GC operations like + // syscall.dropImport to talk about an import without claiming it's + // reachable or causing it to become reachable + + t.is(vk.mapKernelSlotToVatSlot('ko3'), 'o-52'); + vk.clearReachableFlag('ko3'); + t.is(s.get(`${vatID}.c.ko3`), '_ o-52'); + t.throws(() => vk.mapVatSlotToKernelSlot('o-52'), { + message: /vat tried to access unreachable import/, + }); + t.is(vk.mapVatSlotToKernelSlot('o-52', false), 'ko3'); + t.is(vk.mapKernelSlotToVatSlot('ko3', false), 'o-52'); + t.is(s.get(`${vatID}.c.ko3`), '_ o-52'); + + t.is(vk.mapVatSlotToKernelSlot('o+3'), 'ko22'); + vk.clearReachableFlag('ko22'); + t.is(s.get(`${vatID}.c.ko22`), '_ o+3'); + t.throws(() => vk.mapKernelSlotToVatSlot('ko22'), { + message: /kernel sent unreachable export/, + }); + t.is(vk.mapKernelSlotToVatSlot('ko22', false), 'o+3'); + t.is(vk.mapVatSlotToKernelSlot('o+3', false), 'ko22'); + t.is(s.get(`${vatID}.c.ko22`), '_ o+3'); +});