Skip to content

Commit

Permalink
prohibit changes to durable-Kind stateShape during upgrade
Browse files Browse the repository at this point in the history
When a vat upgrade provides a new definition for a pre-existing
durable Kind, it can supply a `stateShape` option that might not be
compatible with one established by the previous incarnation.

The long-term issue is how to manage "schema upgrades", as the
permissible/desired shape of the virtual-object `state` data changes
over versions. For now, our main concern is that userspace doesn't
create a situation where reading a `state` property causes an error,
because the new `stateShape` rejects values that were recorded by an
earlier version.

The short-term fix is to insist that each new incarnation defines the
Kind with exactly the same `stateShape` as its predecessor. This check
is performed by comparing the serialized/marshalled capdata of
`stateShape` against that of the earlier version, which is convenient
but overly strict (e.g. the properties must be defined in the same
order).

If violated, the new-version `buildRootObject` will throw an error as
it calls `defineDurableKind`.

refs #7337
  • Loading branch information
warner committed Apr 13, 2023
1 parent 84ac88f commit e47f456
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 9 deletions.
3 changes: 2 additions & 1 deletion packages/swingset-liveslots/src/liveslots.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function build(
syscall,
forVatID,
vatPowers,
liveSlotsOptions,
liveSlotsOptions = {},
gcTools,
console,
buildVatNamespace,
Expand Down Expand Up @@ -664,6 +664,7 @@ function build(
m.serialize,
unmeteredUnserialize,
assertAcceptableSyscallCapdataSize,
liveSlotsOptions,
);

const collectionManager = makeCollectionManager(
Expand Down
1 change: 1 addition & 0 deletions packages/swingset-liveslots/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
* @typedef {{
* enableDisavow?: boolean,
* relaxDurabilityRules?: boolean,
* allowStateShapeChanges?: boolean,
* }} LiveSlotsOptions
*
* @typedef {import('@endo/marshal').CapData<string>} SwingSetCapData
Expand Down
34 changes: 27 additions & 7 deletions packages/swingset-liveslots/src/virtualObjectManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,20 @@ function insistDurableCapdata(vrm, what, capdata, valueFor) {
});
}

function insistSameCapData(oldCD, newCD) {
if (oldCD.body !== newCD.body) {
Fail`durable Kind stateShape mismatch (body)`;
}
if (oldCD.slots.length !== newCD.slots.length) {
Fail`durable Kind stateShape mismatch (slots.length)`;
}
oldCD.slots.forEach((oldVref, idx) => {
if (newCD.slots[idx] !== oldVref) {
Fail`durable Kind stateShape mismatch (slot[${idx}])`;
}
});
}

/**
* Create a new virtual object manager. There is one of these for each vat.
*
Expand All @@ -281,6 +295,7 @@ function insistDurableCapdata(vrm, what, capdata, valueFor) {
* @param {import('@endo/marshal').Unserialize<string>} unserialize Unserializer for this vat
* @param {*} assertAcceptableSyscallCapdataSize Function to check for oversized
* syscall params
* @param {import('./types').LiveSlotsOptions} liveSlotsOptions
*
* @returns {object} a new virtual object manager.
*
Expand Down Expand Up @@ -321,7 +336,10 @@ export function makeVirtualObjectManager(
serialize,
unserialize,
assertAcceptableSyscallCapdataSize,
liveSlotsOptions = {},
) {
const { allowStateShapeChanges = false } = liveSlotsOptions;

// array of Caches that need to be flushed at end-of-crank, two per Kind
// (dataCache, contextCache)
const allCaches = [];
Expand Down Expand Up @@ -704,22 +722,24 @@ export function makeVirtualObjectManager(
// compatibility
checkAndUpdateFacetiousness(tag, durableKindDescriptor, facetNames);

const stateShapeCapData = serialize(stateShape);
const newShapeCD = serialize(stateShape);

// Durable kinds can only hold durable objects in their state,
// so if the stateShape were to require a non-durable object,
// nothing could ever match. So we require the shape have only
// durable objects
insistDurableCapdata(vrm, 'stateShape', stateShapeCapData, false);
insistDurableCapdata(vrm, 'stateShape', newShapeCD, false);

// compare against slots of previous definition, incref/decref
let oldStateShapeSlots = [];
if (durableKindDescriptor.stateShapeCapData) {
oldStateShapeSlots = durableKindDescriptor.stateShapeCapData.slots;
const oldShapeCD = durableKindDescriptor.stateShapeCapData;

const oldStateShapeSlots = oldShapeCD ? oldShapeCD.slots : [];
if (oldShapeCD && !allowStateShapeChanges) {
insistSameCapData(oldShapeCD, newShapeCD);
}
const newStateShapeSlots = stateShapeCapData.slots;
const newStateShapeSlots = newShapeCD.slots;
vrm.updateReferenceCounts(oldStateShapeSlots, newStateShapeSlots);
durableKindDescriptor.stateShapeCapData = stateShapeCapData; // replace
durableKindDescriptor.stateShapeCapData = newShapeCD; // replace

saveDurableKindDescriptor(durableKindDescriptor);
}
Expand Down
101 changes: 100 additions & 1 deletion packages/swingset-liveslots/test/virtual-objects/test-state-shape.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,20 @@ test('durable stateShape refcounts', async t => {
return Far('root', {});
}

// to test refcount increment/decrement, we need to override the
// usual rule that the new version must exactly match the original
// stateShape
const options = { allowStateShapeChanges: true };
const makeNS2 = () => ({ buildRootObject: build2 });
const ls2 = makeLiveSlots(sc2, 'vatA', {}, {}, gcTools, undefined, makeNS2);
const ls2 = makeLiveSlots(
sc2,
'vatA',
{},
options,
gcTools,
undefined,
makeNS2,
);

const standard2Vref = 'o-2';
const vp = { standard2: kslot(standard2Vref) };
Expand All @@ -197,3 +209,90 @@ test('durable stateShape refcounts', async t => {
t.falsy(ls2.testHooks.getReachableRefCount(standard1Vref));
t.is(ls2.testHooks.getReachableRefCount(standard2Vref), 1);
});

test('durable stateShape must match', async t => {
const kvStore = new Map();
const { syscall: sc1 } = buildSyscall({ kvStore });
const gcTools = makeMockGC();

function build1(vatPowers, _vp, baggage) {
const { VatData } = vatPowers;
const { makeKindHandle, defineDurableKind } = VatData;

return Far('root', {
create: (obj1, obj2) => {
const kh = makeKindHandle('shaped');
baggage.init('kh', kh);
const stateShape = { x: obj1, y: obj2 };
defineDurableKind(kh, init, behavior, { stateShape });
},
});
}

const makeNS1 = () => ({ buildRootObject: build1 });
const ls1 = makeLiveSlots(sc1, 'vatA', {}, {}, gcTools, undefined, makeNS1);
const startVat1 = makeStartVat(kser());
await ls1.dispatch(startVat1);
const rootA = 'o+0';

const vref1 = 'o-1';
const vref2 = 'o-2';
await ls1.dispatch(
makeMessage(rootA, 'create', [kslot(vref1), kslot(vref2)]),
);

// the first version's state is { x: vref1, y: vref2 }

// ------

// Simulate upgrade by starting from the non-empty kvStore.
const clonedStore = new Map(kvStore);
const { syscall: sc2 } = buildSyscall({ kvStore: clonedStore });

function build2(vatPowers, vatParameters, baggage) {
const { VatData } = vatPowers;
const { defineDurableKind } = VatData;
const { obj1, obj2 } = vatParameters;
const kh = baggage.get('kh');
// several shapes that are not compatible
const shape1 = { x: obj1, y: M.any() };
const shape2 = { x: obj1 };
const shape3 = { x: obj1, y: obj2, z: M.string() };
const shape4 = { x: M.or(obj1, M.string()), y: obj2 };
const shape5 = { x: obj2, y: obj1 }; // wrong slots
const trial = shape => {
t.throws(
() => defineDurableKind(kh, init, behavior, { stateShape: shape }),
{ message: /durable Kind stateShape mismatch/ },
);
};
trial(shape1);
trial(shape2);
trial(shape3);
trial(shape4);
trial(shape5);
const stateShape = { x: obj1, y: obj2 }; // the correct shape
defineDurableKind(kh, init, behavior, { stateShape });
t.pass();

return Far('root', {});
}

// we do *not* override allowStateShapeChanges
// const options = { allowStateShapeChanges: true };
const options = undefined;
const makeNS2 = () => ({ buildRootObject: build2 });
const ls2 = makeLiveSlots(
sc2,
'vatA',
{},
options,
gcTools,
undefined,
makeNS2,
);

const vp = { obj1: kslot(vref1), obj2: kslot(vref2) };
const startVat2 = makeStartVat(kser(vp));
await ls2.dispatch(startVat2);
});

0 comments on commit e47f456

Please sign in to comment.