-
Notifications
You must be signed in to change notification settings - Fork 212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
kindConstructor vs GC sensitivity vs non-metered deserialization #3462
Comments
I hate to suggest it, but maybe the answer is to abandon the "objects as closures" pattern and use A more drastic approach that might work would be to create Representatives with state (serializable properties) but without behavior. Or maybe The key thing is to not involve userspace in the creation (or re-use) of a Representative, and make that exclusively the job of liveslots (just like it does for Presences). |
I like that idea but I recommend that representatives be opaque/encapsulated elsewhere than in invocations to that handler function/object. This basically splits up the three aspects of usual objects into distinct parts. |
For reference, here's how Payments were defined until @katelynsills recently reverted them (#3357) to be regular Remotables: export const makePaymentMaker = (allegedName, brand) => {
const paymentVOFactory = () => {
return {
init: () => {},
self: Far(`${allegedName} payment`, {
getAllegedBrand: () => brand,
}),
};
};
const makePayment = makeKind(paymentVOFactory);
return makePayment;
}; |
In today's kernel meeting, we decided the core problem is the kind constructor's ability to mutate state when called. The kind constructor is shaped like this: function makeThingInstance(state) { // called at least once for each Representative
// <- opportunity for mutation!
function init(args) { // called once per virtual object
}
const self = Far('interfacename', {
behaviorMethod() { // called once per rep.foo() invocation
// closes over state or external things
},
});
return { init, self };
}
const thingMaker = makeKind(makeThingInstance); The problem is that "opportunity" area, outside of We thought of two approaches. The first is to give up on the "objects as closures" pattern by recasting the kind constructor in a more declarative fashion. We can imagine moving the behavior into a single object, which gets its uniqueness from arguments or an implicit const balances = new WeakMap();
function initThing(state, name) {
state.name = name;
}
const thingInterfaceName = 'thing';
const thingBehavior = {
getName(self, state) { return state.name; },
rename(self, state, newName) { state.name = newName; },
getBalance(self, state) { return balances.get(self); },
}
const thingMaker = makeKind(initThing, thingInterfaceName, thingBehavior); We could make it one step closer to traditional JS objects by using an implicit const thingBehavior = {
getName(state) { return state.name; },
rename(state, newName) { state.name = newName; },
getBalance(state) { return balances.get(this); },
} We must probably keep const names = makeVirtualWeakStore();
const balances = makeVirtualWeakStore();
function initThing(self, name) {
names.set(self, name);
}
const thingBehavior = {
getName() { return names.get(this); },
rename(newName) { names.set(this, newName); },
getBalance() { return balances.get(this); },
} The second approach is to keep using the objects-as-closures pattern, but enforce source-code restrictions on the kind constructor. @erights thinks it wouldn't be hard to make an AST-based parser/scanner to reject kind constructors that deviate from a tightly-limited pattern. The constructor would look exactly like our original, but const thingMaker = makeKind(`${makeThingInstance}`, { balances }); and the first thing To simplify the scanner, we'd require kind constructors to be written in Jessie, which we know we can parse without the full power of something like Babel (which we really don't want to drag into the vat). We concluded that we can put off fixing this for a while, because it won't be an issue until we reach adversarial contract code. We'll continue to write our kind constructors in the existing style, and document that as the preferred approach, and then later we can make it mandatory by changing the |
Thanks for writing this down. Regarding the first approach, a few things we should clarify:
|
|
Thanks for explaining, I hadn't managed to thread it through to the point where during serialization, the representative would be "unwrapped" for the virtual object id.
My cursory reading of |
Hm, I think I remember @FUDCo and I discussing that. I think our path out was that |
@warner is it possible to make the API changes for MN-1, and hold the deep impl of it to MN-1.1? |
I think this is a non-issue now that So metering is a function of when deserialization happens, and that's sensitive to GC, but we don't meter deserialization, so even if userspace gains a way to sense metering, they shouldn't be able to sense the GC that provokes/inhibits it. |
Yes, this was my original motivation for suggesting a behavior object approach! |
Closing per @warner's comment above. |
What is the Problem Being Solved?
In #3458 we draw up a plan to isolate any GC-sensitive code paths within liveslots in an "unmetered box", to prevent variations in GC timing getting amplified into variance of metering results. @erights raised an important question today: can the userspace "kind constructor", used to regenerate new Representative
Objects
for virtual objects, be used to violate determinism?The kind constructor might be invoked any time we deserialize a virtual-object vref. We specifically allow the Representative to be GCed when the JS heap no longer needs it, but retain the ability to make a new one when needed (thus we say the vref identifies an abstract "virtual" object rather than a single concrete Remotable). To make
===
work (#1968) it is important to have at most one Representative for the virtual object at a time. If determinism were not a consideration, we would simply use a Map of WeakRefs, with a FinalizationRegistry to remove lost Representatives (and this is exactly what liveslots does).But we have two additional concerns:
We decided long ago to withhold
WeakRef
andFinalizationRegistry
from userspace code, so they can't be used to probe the status of arbitrary objects (and thus learn when GC or finalization happens). The involvement of userspace code in the kind constructor makes the task more difficult. @FUDCo and I identified several requirements to support the first concern:vatStore
(get/set/delete) syscalls must not depend upon the GC status.To meet these requirements, we made two big changes. First we made the virtual objects perform their vatStore reads at property getter time rather than when the Representative is created. The sequence of property reads (i.e. getter invocation) is deterministic, but the act of creating a Representative might not be. This hurts performance, because we'd really prefer to read the data just once and leave it in RAM, but every
syscall.vatStoreRead
goes into the transcript, and the transcripts must be consistent across validators. (And unlikesyscall.dropImports
, we can't strip them from the transcript, because they have critical return values, and the transcript is the only place to record that information for use during replay).The second was to change the deserialization code to always call the kind constructor, even if there's already a Representative, so that userspace cannot simply count the number of constructor invocations and thus sense the GC state. If the Representative already existed, we discard the newly-constructed one. The act of deseralization is deterministic: it occurs at top-of-crank (on the arguments of a
dispatch.deliver
or the resolution data of adispatch.notify
), and mid-crank when a virtual object property lookup or virtual weak store value read causes virtualized data to be deserialized. So the number of kind constructor invocations is deterministic. This also hurts performance, because obviously we'd rather only call the kind constructor when really necessary, but we concluded the extra calls were necessary to prevent GC-based nondeterminism from leaking into userspace.@FUDCo and I spent a bunch of time studying that pathway and decided that userspace could not tell whether their earlier Representative has been collected or not. They don't have WeakRef, and the
WeakMap
/WeakSet
they get is vref-aware, so the only tool they can use is===
against a previously-held object. If they're able to ask that question, the answer will always be false (i.e. the one they just created is different than the one they'd held onto from before), meaning their new one will be discarded. The only time the new Representative is used is if the previous one had been collected, and for that to happen, userspace must not be able to ask the===
question, so they can't sense the answer.I believe we also studied the
state
object closed over by the kind constructors "behavior" function and were satisfied that it couldn't help either (I think maybe each call gets a brand newstate
object).The concern that @erights highlighted is that our new desire to maintain deterministic metering results while still tolerating GC timing variations might be threatened by the inclusion of userspace code in the liveslots deserialization pathway. Our plan (#3458) requires us to exclude the GC-sensitive code pathways within liveslots from metering, by disabling metering just before and reenabling it just after. Basically any code that touches a WeakRef or might call a finalizer must be unmetered.
But the userspace kind constructor gets called during deserialization of virtual-object -bearing capdata. And in particular the object created by that constructor will be used or discarded depending upon the GC state, and userspace isn't obligated to produce identically-behaving objects each time.
I think the basic attack would look like:
The first few times the kind constructor is called, it produces a Representative whose
.bar()
method runs quickly. After that,bar()
causes an infinite loop (which will cause a metering fault, although a more subtle attack would delay discovery by making a less drastic difference in meter consumption). The timing of GC determines which of these instances gets used later, and the invocation of.bar()
by other userspace code must clearly be subject to metering.In writing this up, I'm realizing that maybe this ability to change behavior on a construction-by-construction basis already violates our belief that userspace cannot distinguish between the "real" and "fake" constructor invocations, and the potential metering variation is just adding one fire to the existing pile of fire.
Userspace must not be able to sense GC timing at all: even if GC happens at the same time across all validators, knowing when it happens would reveal information about other objects within the same vat, which would (noisily) violate confidentiality within the ocap model. Liveslots is allowed to sense GC (and must, to support distributed GC), but we think we want to rely upon liveslots to not leak this GC timing into the metering results and thus validator consensus.
We need to study this more carefully. The fallback is to make GC timing deterministic, so that there is no variation to influence metering results and consensus. But that wouldn't be sufficient to protect the intra-vat confidentiality property.
Security Considerations
Violations of intra-vat inter-object confidentiality.
Consensus failures provoked by local variations in GC timing, leaking into metering results, leaking into "how many cranks go into each block"
runPolicy
decisions (#3460).The text was updated successfully, but these errors were encountered: