-
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
revoke a Remotable #2070
Comments
@erights and I were able to refine this a bit more: To delete the c-list entries, we define a "Taboo" object as one which may not be spoken of. When a Remotable is revoked by its creator, eventually all other vats which held Presences for it will learn about the revocation, and that vref will become taboo for them: it will be an error for any outbound message to cite the vref. Such messages will be rejected by the liveslots layer before any syscall is emitted. Eventually, the kernel will forget about the kref as well. We want to retain the one-to-one relationship between I think we can accomplish this safely by splitting the c-list into its two directions: inbound (kref to vref) and outbound (vref to kref). The exporting vat, which revokes its Remotable, will do a Now, the kref might still be referenced by kernel data structures (run-queue, settled promise data) or by other c-lists. The kernel identifies all other vats which have the kref in their c-lists (the "subscribers") and queues a The subscribing vat's liveslots reacts to Once all Over time, these kernel data structures will hopefully drain and those references will go away. If/when the only remaining kref citations are in inbound c-lists, we now know that no one can ever hear of the object again: vats can't emit it, no pending messages reference it, no resolved promises reference it, therefore nobody will ever be able to receive a message that cites it. In this state, the object is fully unreferenceable, so the kernel object table entry can be safely removed, along with the inbound c-list entries. Those inbound entries are removed with another delivery, perhaps If we implement the #1872 non-deterministic GC scheme, then The refcounting scheme needs to track the inbound and outbound c-lists separately. An outbound entry holds a reference on the kref, but not the inbound. When a vat is terminated, all of its exports become taboo, just as if the vat had explicitly revoked them first. Other ideas that came up:
We don't think we can drop auxilliary data when the object is revoked, however the fact that no vat can speak of the object again makes it more likely that the kernel object table entry can be deleted entirely sooner or later, and we can drop the auxilliary data at that point. Programmers are responsible for revoking objects they create, to avoid accumulating garbage. But we may be able to tolerate some rate of unrevoked objects if it is low enough, so we might not impose this as a hard requirement. We can imagine some sort of testing harness that runs a contract in a loop and counts how many uncollected objects remain after each cycle (an automated version of @FUDCo 's benchmarking tool), and use the results as entry criteria for contracts on the chain, or at least as feedback to the authors (just like we'd use lint checks). Object revocation introduces a slight DoS vector, but I'm not currently too worried about it. Imagine a situation where vat C sends a record to vat A, and vat A uses some pieces of it, then sends the whole record on to vat B. If C includes some unexpected properties in the record (which A doesn't look for), then C could revoke those properties later, and A spontaneously loses the ability to send that full record to B. A is already relying upon C to give it the pieces that it needs: if C revokes one of those objects (or just omits them from the record), A cannot get their job done. So the only incremental vulnerability is that A might be able to do some sort of verification on the pieces that it does care about, and think they're safe, but then see their later message fail when it includes the whole record. A can mitigate this by only sending the pieces that it cares about (i.e. unpack C's record immediately, then drop the original). |
Hrm, one realization I had was that this gives vats a new way to sense whether a reference they hold is a Presence or a local object: they send it a message with a known-Taboo object (perhaps something they've revoked themselves). Local objects will accept it without complaint, but sending it off-vat would trigger an error during serialization. Of course To block this for the message-send case, we'd probably need to serialize the argument graph and examine its slots for Taboo presences, which sounds annoyingly expensive. |
This ability to sense is within our distributed object semantics and is not something to avoid. Doing an eventual send to a local object and doing an eventual send to a remote object are similar enough that for programs that stay away from edge cases programmers get away with treating them as mostly the same. However, when a message crosses a vat boundary it is marshalled. A proxy with proper pass-by-copy behavior is accepted and treated as pass-by-copy. But it can sense the things marshal is doing and infer that it is being marshalled. The vat boundary is semantically significant. We are not trying to hide it from code that wants to see it. |
Which variants of these designs require that objects use the mechanism in order to be shared, and which variants don't? Our platform already has enough learning costs that I think it would be important to choose one where developers didn't have to add these declarations until they have made enough progress to be concerned about performance. Relative to that consideration, I think the cost of adding these declarations and making use of them within Zoe itself isn't a big deal. |
Let's see, we've got one main distinction: revocable or irrevocable. To be revocable, we need to provide a revocation facet at construction time (at least I haven't been able to think of any other way to represent the revocation authority, since we don't have a lexical way to demonstrate that some particular piece of code is "inside" the object). Which means we need a function that returns both the remotely-sharable object and the revocation facet, something like the example above (for non-virtual objects): const props = {
foo(x) { stuff },
bar(y) { stuff },
};
const [obj1, revoke1] = Revocable(props);
const [obj2, revoke2] = Revocable(props); For virtual objects, we can make all of them revocable, since we need a construction function anyways (the return value from For irrevocable objects, we can continue to use object literals. But it kind of raises the question: when should we not use revocation? @erights pointed out that we don't have to require everything to be tightly revoked; we might be able to accomodate some overhead (uncollected objects) as long as it isn't too much. But I'm inclined to think that marking objects as revocable, and actually revoking them when they're no longer useful, should be part of the best-practices. |
Dean had a counter-proposal, which I think is roughly: stop making the guarantee that Presences are unique, at least not once their upstream Remotable has been revoked. I'm still trying to figure out the details. |
Ok, so @dtribble 's counter-proposal is:
Then, if/when the Remotable is revoked:
This requires us to define a pass-by-copy "dead object record", which contains merely the error capdata with which any messages should be rejected. The idea is that any message that references the now-revoked Presence will be delivered successfully (they are not "taboo"), however the Presence arrives as a dead object instead of a new Presence. I'm not yet sure how to represent these. I really don't want the kernel to parse or modify the One additional feature Dean suggested was that many objects have contagious failures, and the order of successful operations is important (think of a writable file object: if the third We're also thinking that a short-term priority should be to change the way Remotables are declared:
A useful experiment to run would be to instrument our tree to sense when/where we depend upon Presence identity: patch Map/WeakMap to notice when a Presence is used as a key. Ideally we'd also sense when someone does |
From Slack: The short summary of the problem is that we need to record boolean flags; one flag in the case of non-faceted objects and an array of flags in the case of a faceted object. This felt very analogous to what we do with the export status, except that it pertains to the state of the object itself rather than to its relationship with the rest of the world. Currently the state of the object is kept in the vatstore under the key Option 1: store under a new key in the vatstore, say Option 2: change the spec of the Option 3: store the revocation status in the existing state object under a pseudo-property name, and censor this from the visible state that’s presented to user code after reading. Pro: still only one read, and finesses the backwards compatibility issue. Con: embedding metadata in the data is icky and fraught with peril, though possibly we could use a property name containing a character that would not be allowed in an actual state variable, to preclude the possibility of collision, or alternatively do some kind of Hilbert hotel trick. Option 4: give up on the idea of this revocation thing for virtual exos. Pro: super easy. Con: it would make Mark sad. Personally, I’d go with option 3, but we’re curious as to your take on the matter. |
(also from slack, in a thread that's talking about adding revocability into virtual/durable objects, rather than ephemeral Remotables, which it what this ticket is about, but it's sufficiently related to warrant capturing some ideas) Yeah, I don't like the extra storage/key costs of 1, or the extra RAM/storage costs of 2, or the compatibility/security problems of 3. |
What is the Problem Being Solved?
We've tossed around the idea that the creator of an object could be allowed to "revoke" it, meaning that (at least) all future messages sent to this object would throw an exception (the "revocation error") rather than triggering any behavior.
The notional benefit is that the revocation error could be pushed all the way out to the clients which hold a Presence for the object. Those vats could then handle message sends to the now-revoked Presence locally, without even talking to the kernel. Their liveslots layer would remember that the Presence had been revoked, and it wouldn't need to make a syscall to process a message send.
The hope would be that this allows the original Remotable to be dropped, and that somehow this would reduce memory use in some vats.
Description of the Design
The kernel object table would be enhanced: the "owner" field (which holds a VatID) would alternately hold the revocation error object (capdata). This would probably subsume the error we create when sending a message to a dead vat: instead of checking whether the owner vat-id is still alive, we'd just replace the owner field with a "vat is dead" error object in all remaining exports of the terminated vat.
We cannot delete the kernel object table entry merely because an object is revoked: these objects still have identity, and one vat might send an otherwise-dead object to some other, who might compare it against something, and this comparison must continue to work as it did when the objects were alive. This may limit the savings we might achieve.
On the Presence side, we'd probably replace the "handler" of the associated HandledPromise with one that always throws.
On the Remotable side, we currently only anticipate revoking virtual objects, because the explicit creation point is also the correct place for us to return the revocation facet. Normal (non-virtual) objects are usually created with a plain JavaScript object literal (usually in the form
harden({ props... })
), which means there's nothing special to distinguish the creator's access from any other client's access, and we certainly don't want to enable arbitrary clients to revoke an object they didn't create, even if that client is in the same vat as the creator (they hold a Remotable rather than a Presence).But one direction worth exploring might be to require
Remotable(obj)
on everything that is allowed to cross the wire. If we did that, then we could maybe pass a second argument in, which could be given the revocation facet (Remotable(obj, f => revocationFacet = f)
, just like theexecutor
innew Promise(executor)
), or maybe we could pass a closely-held handle object in. A less palatable option might be to haveRemotable()
return something different than its argument:{ r, revocationFacet } = Remotable(obj)
, wherer
is what you send over the wire, andobj
would throw an error if sent over the wire.Alternately, we could define an explicit
Revocable
creator function, and declare that normal Remotables (whether created byRemotable()
or just plain hardened objects) are irrevocable, and the only way to make a revocable Remotable is withRevocable
. In this case, the most straightforward signature would beconst [ obj, revoke ] = Revocable({ properties.. })
, to make it clear that the object literal containing the properties of the new object is not itself the callable object. Also, it should be clear that in:that having access to either
props
orobj1
orrevoke1
or evenobj2
gives you no ability to accessrevoke2
.Security Considerations
As mentioned above, we must be careful with the revocation authority: only the original creator of the object should get access (of course they can delegate it to whomever they want). Specifically, merely having access to the object must not automatically grant revocation authority.
Open Questions
Is this useful? Would it actually save any space?
Would the #2069 auxilliary data be dropped when the object is revoked? That would probably be surprising, but we might consider the potential data savings to be worth it.
The text was updated successfully, but these errors were encountered: