diff --git a/packages/swingset-liveslots/test/test-liveslots-mock-gc.js b/packages/swingset-liveslots/test/test-liveslots-mock-gc.js index 9df0d4fc9a01..55f9a0d94408 100644 --- a/packages/swingset-liveslots/test/test-liveslots-mock-gc.js +++ b/packages/swingset-liveslots/test/test-liveslots-mock-gc.js @@ -3,6 +3,7 @@ import '@endo/init/debug.js'; import { Far } from '@endo/marshal'; import { makeLiveSlots } from '../src/liveslots.js'; +import { parseVatSlot } from '../src/parseVatSlots.js'; import { kslot, kser } from './kmarshal.js'; import { buildSyscall } from './liveslots-helpers.js'; import { makeMessage, makeStartVat, makeBringOutYourDead } from './util.js'; @@ -147,3 +148,251 @@ test('dropImports', async t => { t.deepEqual(possiblyDeadSet, new Set()); t.is(FR.countCallbacks(), 0); }); + +const doublefreetest = test.macro(async (t, mode) => { + // A and B are virtual objects. RAM holds Representatives for + // each. A holds a virtual (.state) reference to B. Both A and B + // Representatives are finalized in the same crank. A's baseref + // sorts lexicographically earlier than B. + // + // Previously, both A and B's baserefs appear in deadSet together, + // and the first loop through scanForDeadObjects processes A first, + // which gets deleted. While deleting it, we drop the virtual ref to + // B, *which adds B back to possiblyDeadSet*. Then we process B and + // delete B. Then scanForDeadObjects does a second loop, which sees + // B in possiblyDeadSet, sees no slotToVal for it (not reintroduced + // since finalization), promotes it to deadSet, then deletes it a + // second time. When B is a collection, this used to be silently + // ignored because allCollectionObjIDs was consulted, inhibiting the + // duplicate deletion. With that removed, B is deleted twice, which + // fails. + + const { syscall, fakestore } = buildSyscall(); + const gcTools = makeMockGC(); + + const initData = () => ({ value: 0 }); + const behavior = { set: ({ state }, value) => (state.value = value) }; + const things = {}; + const thingNames = [ + 'object1', + 'object2', + 'collection3', + 'collection4', + 'object5', + 'object6', + ]; + let fromThing; + let toThing; + // eslint-disable-next-line no-unused-vars + let fromName; + // eslint-disable-next-line no-unused-vars + let toName; + + function buildRootObject(vatPowers) { + const { VatData } = vatPowers; + const { defineKind, makeScalarBigMapStore } = VatData; + + const { firstType, lastType, order } = mode; + + // We need 2*2*2 combinations of: + // * firstType: A is virtual [object, collection] + // * lastType: B is virtual [object, collection] + // * order (first->last/last->first): A.vref < B.vref , A.vref > B.vref + + // KindIDs share a numberspace with nextObjectID, for which o+0 is + // used for the root object. KindID=1 is used for KindHandles, + // then collection types claim 2-9 (2 is scalarMapStore, 6 is + // scalarDurableMapStore, 9 is scalarDurableWeakSetStore). These + // claims happen early, before buildRootObject runs. + + // Instances of the collection then get vrefs of o+vNN/MM or + // o+dNN/MM, where 'v' and 'd' indicate virtual/durability (the +v + // vs +d lets the kernel delete merely-virtual data without + // needing to ask liveslots which vrefs are virtual and which are + // durable), NN is the type, and MM is the next collectionID (a + // space which starts at 1, and increments for every collection + // created, regardless of type). MM=1 is claimed by baggage, which + // gets o+d6/1, because type=6 is scalarDurableMapStore. MM=2/3/4 + // are claimed by the watched-promise tables. + + // The vrefs of virtual objects are o+vNN/PP, where NN is + // allocated from the nextObjectID space, which typically starts + // at 10 (since 2-9 were claimed for collection types), and PP is + // a separate counter for each kind (starting at 1). Durable + // objects get o+dNN/PP . + + // So the first userspace-created scalarMapStore will get o+v2/MM, + // a scalarDurableMapStore will get o+d6/MM, the first + // userspace-created virtual kind's first instance will get + // o+v10/1, and a subsequent durable kind's instance will get + // o+d11/1. + + // To get vrefs that have a specific lexicographic ordering, and + // are also suitable for establishing virtual-data refcounts in + // the right directions, we must abuse the ordering rules (which + // would not be possible if we used numerical ordering instead of + // lexicographic). We create a virtual kind first, which gets + // KindID=10, and two instances 'object1' (o+v10/1) and 'object2' + // (o+v10/2). Then we make two scalarMapStores, 'collection3' + // (o+v2/5) and 'collection4' (o+v2/6). Then we create a dozen + // throwaway Kinds, enough to reach KindID=22, and make two + // instances of the last one, 'object5' (o+v22/1) and 'object6' + // (o+v22/2). The total set of vrefs is thus sorted: + // + // * o+v10/1 object1 + // * o+v10/2 object2 + // * o+v2/5 collection3 + // * o+v2/6 collection4 + // * o+v22/1 object5 + // * o+v22/2 object6 + // + // and we can use A->B with object1->collection3 or + // collection3->object5 to get the desired reference-edge + // orientations + // + // This is, of course, highly dependent upon the IDs assigned by + // liveslots to scalarMapStore, and the number of allocations + // (which controls our starting point of "10"). The test code + // compares all the vrefs against each other to ensure we're + // getting the lexicographic ordering that we expect. + + // The specific failing case was: o+d11/1 -> o+d6/8, which + // corresponds to our object1->collection3 case. + + // kind10 instances will be o+v10/MM + const makeKind10 = defineKind('kind10', initData, behavior); + things.object1 = makeKind10(); // o+v10/1 + things.object2 = makeKind10(); // o+v10/2 + + things.collection3 = makeScalarBigMapStore('collection3'); // o+v2/5 + things.collection4 = makeScalarBigMapStore('collection4'); // o+v2/6 + + // consume KindIDs 11 to 21 + for (let i = 11; i < 22; i += 1) { + defineKind(`kind${i}`, initData, behavior); + } + + // kind22 instances will be o+v22/MM + const makeKind22 = VatData.defineKind('kind22', initData, behavior); + things.object5 = makeKind22(); // o+v22/1 + things.object6 = makeKind22(); // o+v22/2 + + // all six Representatives have a RAM pillar now, until we use + // mockGC to drop them + + // things.object1.set(things.collection3); // vdata ref A -> B + // return Far('root', {}); + + let firstName; + let lastName; + for (const name of thingNames) { + if (!firstName) { + // discard everything until we find a match for the first name + if (name.startsWith(firstType)) { + firstName = name; + } + continue; + } + if (!lastName) { + // then do the same for the last name + if (name.startsWith(lastType)) { + lastName = name; + } + continue; + } + } + + const firstThing = things[firstName]; + const lastThing = things[lastName]; + let fromType; + switch (order) { + case 'first->last': + [fromThing, toThing] = [firstThing, lastThing]; + [fromName, toName] = [firstName, lastName]; + fromType = firstType; + break; + case 'last->first': + [fromThing, toThing] = [lastThing, firstThing]; + [fromName, toName] = [lastName, firstName]; + fromType = lastType; + break; + default: + throw Error(`unknown order ${order}`); + } + if (fromType === 'object') { + fromThing.set(toThing); + } else { + fromThing.init('key', toThing); + } + + return Far('root', {}); + } + + const makeNS = () => ({ buildRootObject }); + const ls = makeLiveSlots(syscall, 'vatA', {}, {}, gcTools, undefined, makeNS); + const { dispatch, testHooks } = ls; + const { valToSlot } = testHooks; + + await dispatch(makeStartVat(kser())); + + // for (const key of Array.from(fakestore.keys()).sort()) { + // console.log(key.padEnd(25, ' '), '->', fakestore.get(key)); + // } + // console.log(); + + const vrefs = {}; + const compares = []; + for (const [name, compare] of Object.entries(things)) { + const vref = valToSlot.get(compare); + // console.log(name, compare, vref); + vrefs[name] = vref; + compares.push(vref.padEnd(10, ' ') + name); + } + + // Make sure the allocated object IDs sort as we need them to. If + // this fails, maybe liveslots is allocating so many built-in + // collections/types that scalarDurableMapStore no longer has an ID + // that sorts between our early Kinds and our later Kinds. + const sortedCompares = [...compares].sort(); + t.deepEqual(compares, sortedCompares); + // for (const s of sortedCompares) { console.log(s); } + + // console.log(`${fromName} -> ${toName}`); + + // now pretend all RAM pillars are dropped + for (const thing of Object.values(things)) { + gcTools.kill(thing); + } + gcTools.flushAllFRs(); + // the bug caused BOYD (in scanForDeadObjects) to perform a + // double-free of lastThing, causing this to throw + await dispatch(makeBringOutYourDead()); + + for (const [name, vref] of Object.entries(vrefs)) { + // everything should be deleted + if (name.startsWith('object')) { + t.is(fakestore.get(`vom.${vref}`), undefined); + t.is(fakestore.get(`vom.rc.${vref}`), undefined); + t.is(fakestore.get(`vom.es.${vref}`), undefined); + } else { + // all collection metadata should be gone + const collectionID = String(parseVatSlot(vref).subid); + t.is(fakestore.get(`vc.${collectionID}.|schemata`), undefined); + t.is(fakestore.get(`vc.${collectionID}.|label`), undefined); + t.is(fakestore.get(`vc.${collectionID}.|nextOrdinal`), undefined); + t.is(fakestore.get(`vc.${collectionID}.|entryCount`), undefined); + } + } +}); + +for (const firstType of ['object', 'collection']) { + for (const lastType of ['object', 'collection']) { + for (const order of ['first->last', 'last->first']) { + const name = `double-free ${firstType} ${lastType} ${order}`; + const mode = { firstType, lastType, order }; + test(name, doublefreetest, mode); + } + } +} + +// test('double-free', doublefreetest, { firstType: 'object', lastType: 'collection', order: 'first->last' });