From bd421ff1aac2c7dfcc2fd1d035acbc778ab9c4ad Mon Sep 17 00:00:00 2001 From: Chip Morningstar Date: Thu, 29 Apr 2021 18:25:29 -0700 Subject: [PATCH] feat: wrap WeakMap and WeakSet to hide virtual object non-determinism --- packages/SwingSet/src/kernel/liveSlots.js | 36 +++++- .../src/kernel/vatManager/manager-local.js | 2 +- .../src/kernel/virtualObjectManager.js | 121 +++++++++++++++++- .../test-virtualObjectManager.js | 73 +++++++++++ .../tools/fakeVirtualObjectManager.js | 4 + 5 files changed, 225 insertions(+), 11 deletions(-) diff --git a/packages/SwingSet/src/kernel/liveSlots.js b/packages/SwingSet/src/kernel/liveSlots.js index 43c30fc5f6a..ee40f55053c 100644 --- a/packages/SwingSet/src/kernel/liveSlots.js +++ b/packages/SwingSet/src/kernel/liveSlots.js @@ -27,7 +27,7 @@ const DEFAULT_VIRTUAL_OBJECT_CACHE_SIZE = 3; // XXX ridiculously small value to * @param {*} vatParameters * @param {*} gcTools { WeakRef, FinalizationRegistry, waitUntilQuiescent } * @param {Console} console - * @returns {*} { vatGlobals, dispatch, setBuildRootObject } + * @returns {*} { vatGlobals, inescapableGlobalLexicals, dispatch, setBuildRootObject } * * setBuildRootObject should be called, once, with a function that will * create a root object for the new vat The caller provided buildRootObject @@ -342,6 +342,8 @@ function build( makeVirtualObjectRepresentative, makeWeakStore, makeKind, + RepairedWeakMap, + RepairedWeakSet, } = makeVirtualObjectManager( syscall, allocateExportID, @@ -801,6 +803,11 @@ function build( makeKind, }); + const inescapableGlobalLexicals = harden({ + WeakMap: RepairedWeakMap, + WeakSet: RepairedWeakSet, + }); + function setBuildRootObject(buildRootObject) { assert(!didRoot); didRoot = true; @@ -892,7 +899,14 @@ function build( harden(dispatch); // we return 'deadSet' for unit tests - return harden({ vatGlobals, setBuildRootObject, dispatch, m, deadSet }); + return harden({ + vatGlobals, + inescapableGlobalLexicals, + setBuildRootObject, + dispatch, + m, + deadSet, + }); } /** @@ -907,7 +921,7 @@ function build( * @param {boolean} enableDisavow * @param {*} gcTools { WeakRef, FinalizationRegistry, waitUntilQuiescent } * @param {Console} [liveSlotsConsole] - * @returns {*} { vatGlobals, dispatch, setBuildRootObject } + * @returns {*} { vatGlobals, inescapableGlobalLexicals, dispatch, setBuildRootObject } * * setBuildRootObject should be called, once, with a function that will * create a root object for the new vat The caller provided buildRootObject @@ -955,8 +969,20 @@ export function makeLiveSlots( gcTools, liveSlotsConsole, ); - const { vatGlobals, dispatch, setBuildRootObject, deadSet } = r; // omit 'm' - return harden({ vatGlobals, dispatch, setBuildRootObject, deadSet }); + const { + vatGlobals, + inescapableGlobalLexicals, + dispatch, + setBuildRootObject, + deadSet, + } = r; // omit 'm' + return harden({ + vatGlobals, + inescapableGlobalLexicals, + dispatch, + setBuildRootObject, + deadSet, + }); } // for tests diff --git a/packages/SwingSet/src/kernel/vatManager/manager-local.js b/packages/SwingSet/src/kernel/vatManager/manager-local.js index 87fe51ab54d..2ae808ec743 100644 --- a/packages/SwingSet/src/kernel/vatManager/manager-local.js +++ b/packages/SwingSet/src/kernel/vatManager/manager-local.js @@ -129,7 +129,7 @@ export function makeLocalVatManagerFactory(tools) { assert, }); const inescapableTransforms = []; - const inescapableGlobalLexicals = {}; + const inescapableGlobalLexicals = { ...ls.inescapableGlobalLexicals }; if (metered) { const getMeter = meterRecord.getMeter; inescapableTransforms.push(src => transformMetering(src, getMeter)); diff --git a/packages/SwingSet/src/kernel/virtualObjectManager.js b/packages/SwingSet/src/kernel/virtualObjectManager.js index 978129cadd9..e5e2b737744 100644 --- a/packages/SwingSet/src/kernel/virtualObjectManager.js +++ b/packages/SwingSet/src/kernel/virtualObjectManager.js @@ -250,16 +250,13 @@ export function makeVirtualObjectManager( function virtualObjectKey(key) { const vobjID = valToSlot.get(key); - if (!vobjID) { - return undefined; - } else { + if (vobjID) { const { type, virtual } = parseVatSlot(vobjID); if (type === 'object' && virtual) { return `ws${storeID}.${vobjID}`; - } else { - return undefined; } } + return undefined; } return harden({ @@ -318,6 +315,118 @@ export function makeVirtualObjectManager( }); } + function vrefKey(value) { + const vobjID = valToSlot.get(value); + if (vobjID) { + const { type, virtual } = parseVatSlot(vobjID); + if (type === 'object' && virtual) { + return vobjID; + } + } + return undefined; + } + + /* eslint max-classes-per-file: ["error", 2] */ + + const actualWeakMaps = new WeakMap(); + const virtualObjectMaps = new WeakMap(); + + class RepairedWeakMap { + constructor() { + actualWeakMaps.set(this, new WeakMap()); + virtualObjectMaps.set(this, new Map()); + } + + has(key) { + const vkey = vrefKey(key); + if (vkey) { + return virtualObjectMaps.get(this).has(vkey); + } else { + return actualWeakMaps.get(this).has(key); + } + } + + get(key) { + const vkey = vrefKey(key); + if (vkey) { + return virtualObjectMaps.get(this).get(vkey); + } else { + return actualWeakMaps.get(this).get(key); + } + } + + set(key, value) { + const vkey = vrefKey(key); + if (vkey) { + virtualObjectMaps.get(this).set(vkey, value); + } else { + actualWeakMaps.get(this).set(key, value); + } + return this; + } + + delete(key) { + const vkey = vrefKey(key); + if (vkey) { + return virtualObjectMaps.get(this).delete(vkey); + } else { + return actualWeakMaps.get(this).delete(key); + } + } + } + + Object.defineProperty(RepairedWeakMap, Symbol.toStringTag, { + value: 'WeakMap', + writable: false, + enumerable: false, + configurable: true, + }); + + const actualWeakSets = new WeakMap(); + const virtualObjectSets = new WeakMap(); + + class RepairedWeakSet { + constructor() { + actualWeakSets.set(this, new WeakSet()); + virtualObjectSets.set(this, new Set()); + } + + has(value) { + const vkey = vrefKey(value); + if (vkey) { + return virtualObjectSets.get(this).has(vkey); + } else { + return actualWeakSets.get(this).has(value); + } + } + + add(value) { + const vkey = vrefKey(value); + if (vkey) { + virtualObjectSets.get(this).add(vkey); + } else { + actualWeakSets.get(this).add(value); + } + return this; + } + + delete(value) { + const vkey = vrefKey(value); + if (vkey) { + return virtualObjectSets.get(this).delete(vkey); + } else { + return actualWeakSets.get(this).delete(value); + } + } + } + + Object.defineProperty(RepairedWeakSet, Symbol.toStringTag, { + value: 'WeakSet', + writable: false, + enumerable: false, + configurable: true, + }); + /** * Define a new kind of virtual object. * @@ -495,6 +604,8 @@ export function makeVirtualObjectManager( return harden({ makeWeakStore, makeKind, + RepairedWeakMap, + RepairedWeakSet, flushCache: cache.flush, makeVirtualObjectRepresentative, }); diff --git a/packages/SwingSet/test/virtualObjects/test-virtualObjectManager.js b/packages/SwingSet/test/virtualObjects/test-virtualObjectManager.js index 69bf80b6282..0bc35e6904c 100644 --- a/packages/SwingSet/test/virtualObjects/test-virtualObjectManager.js +++ b/packages/SwingSet/test/virtualObjects/test-virtualObjectManager.js @@ -241,3 +241,76 @@ test('weak store operations', t => { ws1.set(nv2, 'non-virtual object #2 revised'); t.is(ws1.get(nv2), 'non-virtual object #2 revised'); }); + +test('virtualized weak collection operations', t => { + // TODO: don't yet have a way to test the weakness of the virtualized weak + // collections + + const { + RepairedWeakMap, + RepairedWeakSet, + makeKind, + } = makeFakeVirtualObjectManager(3); + + const thingMaker = makeKind(makeThingInstance); + const zotMaker = makeKind(makeZotInstance); + + const thing1 = thingMaker('t1'); + const thing2 = thingMaker('t2'); + + const zot1 = zotMaker(1, 'z1'); + const zot2 = zotMaker(2, 'z2'); + const zot3 = zotMaker(3, 'z3'); + const zot4 = zotMaker(4, 'z4'); + + const wm1 = new RepairedWeakMap(); + const wm2 = new RepairedWeakMap(); + const nv1 = {}; + const nv2 = { a: 47 }; + wm1.set(zot1, 'zot #1'); + wm2.set(zot2, 'zot #2'); + wm1.set(zot3, 'zot #3'); + wm2.set(zot4, 'zot #4'); + wm1.set(thing1, 'thing #1'); + wm2.set(thing2, 'thing #2'); + wm1.set(nv1, 'non-virtual object #1'); + wm1.set(nv2, 'non-virtual object #2'); + t.is(wm1.get(zot1), 'zot #1'); + t.is(wm1.has(zot1), true); + t.is(wm2.has(zot1), false); + wm1.set(zot3, 'zot #3 revised'); + wm2.delete(zot4); + t.is(wm1.get(nv1), 'non-virtual object #1'); + t.is(wm1.get(nv2), 'non-virtual object #2'); + t.is(wm2.has(zot4), false); + t.is(wm1.get(zot3), 'zot #3 revised'); + wm1.delete(nv1); + t.is(wm1.has(nv1), false); + wm1.set(nv2, 'non-virtual object #2 revised'); + t.is(wm1.get(nv2), 'non-virtual object #2 revised'); + + const ws1 = new RepairedWeakSet(); + const ws2 = new RepairedWeakSet(); + ws1.add(zot1); + ws2.add(zot2); + ws1.add(zot3); + ws2.add(zot4); + ws1.add(thing1); + ws2.add(thing2); + ws1.add(nv1); + ws1.add(nv2); + t.is(ws1.has(zot1), true); + t.is(ws2.has(zot1), false); + ws1.add(zot3); + ws2.delete(zot4); + t.is(ws1.has(nv1), true); + t.is(ws1.has(nv2), true); + t.is(ws2.has(nv1), false); + t.is(ws2.has(nv2), false); + t.is(ws2.has(zot4), false); + t.is(ws1.has(zot3), true); + ws1.delete(nv1); + t.is(ws1.has(nv1), false); + ws1.add(nv2); + t.is(ws1.has(nv2), true); +}); diff --git a/packages/SwingSet/tools/fakeVirtualObjectManager.js b/packages/SwingSet/tools/fakeVirtualObjectManager.js index 181a70ed11a..28acd07b851 100644 --- a/packages/SwingSet/tools/fakeVirtualObjectManager.js +++ b/packages/SwingSet/tools/fakeVirtualObjectManager.js @@ -64,6 +64,8 @@ export function makeFakeVirtualObjectManager(cacheSize = 100) { makeVirtualObjectRepresentative, makeWeakStore, makeKind, + RepairedWeakMap, + RepairedWeakSet, flushCache, } = makeVirtualObjectManager( fakeSyscall, @@ -77,6 +79,8 @@ export function makeFakeVirtualObjectManager(cacheSize = 100) { return { makeKind, makeWeakStore, + RepairedWeakMap, + RepairedWeakSet, flushCache, dumpStore, };