Agoric / agoric-sdk

monorepo for the Agoric Javascript smart contract platform
Apache License 2.0
326 stars 206 forks source link

GC sensing possible with custom WeakMap/Set #5000

Open mhofman opened 2 years ago

mhofman commented 2 years ago

Describe the bug

The current approach to avoid contract code from sensing if the representative of a Virtual Object was garbage collected and transparently re-created (https://github.com/Agoric/agoric-sdk/issues/2724) is to replace the WeakMap and WeakSet available in the contract's compartment by a vref aware version (https://github.com/Agoric/agoric-sdk/issues/2993).

However nothing prevents user code from rolling out their own WeakMap/WeakSet implementation based on class private fields and a return override, akin to the AltWeakMapSet that was once proposed to work around the WeakMap performance issues of XS (https://github.com/endojs/endo/pull/703).

To Reproduce

Conceptual for now, but I can build a POC if needed

Expected behavior

Contract code unable to sense GC.

Platform Environment

N/A

Additional context

The object being frozen does not prevent private fields from being attached, so that is not a possible avenue for mitigating this.

The only options I can think of right now are:

erights commented 2 years ago

Even though it is an information leak, I labeled it with security-availability rather than security-confidentiality because the threat here is breaking consensus by acting differently on different validators, based only on changes in spontaneous GC schedule. This could be caused, for example, by different memory budgets. Breaking consensus prevents progress, i.e., availability.

Although no state was directly corrupted, I am also labeling it security-integrity, because correct code may operate incorrectly when it uses return override to add private fields to VDOs (virtual/durable objects).

erights commented 2 years ago

The coupling here is interesting. Naively this should not have been possible because

It is only this last step which falls into this trap, where the consequences of liveSlots observing GC inadvertently bled through into observability by the code running under supervision by that liveSlots instance.

I don’t see any way to repair this without either

Fortunately, this is not relevant to MN1. It is relevant to MN3.

mhofman commented 2 years ago

Regarding a possible VM change on the private fields side, I've been trying to think about what that could look like. I've also been reading discussions around similar concerns when attaching private fields to the WindowProxy.

Conceptually there is no difference between attaching a private field to an object, and using that object as a WeakMap key. Any mechanism that prevents private fields attachment should similarly prevent addition to Weak collections. WeakRef / FinalizationRegistry may be sufficiently orthogonal to not participate in the same mechanism, but that seems improbable.

The fact we deny the WeakMap/WeakSet intrinsics to the contract's Compartment does not negate that liveslots needs the ability to add VDO and Presence representatives to a native WeakMap, as well as WeakRef and FinalizationRegistry. Thus any mechanism we come up with must not be absolute. One approach may be to have a temporal aspect to the mechanism: you can add the object to Weak collections or WeakRef/FR until it is "flagged as unweakable".

At first I imagined the "unweakable" mechanism could be a flag to Object.seal, but that wouldn't be compatible with the probable usage in the Web platform for WindowProxy and Location as these objects need to stay extensible. Similarly, these web objects should be usable with weak intrinsics, so throwing on adding to weak collections for example is not an option either for those. While in the web case, these are effectively roots of the object graph and thus their collection never observable, that's not the case for VDO and Presence representatives. I don't see a way to reconcile our requirements with any potential WindowProxy requirements.

It's also unclear whether the stage 1 private declarations proposal would enable adding private fields to objects after their creation without using the return override trick. I opened an issue to clarify: https://github.com/tc39/proposal-private-declarations/issues/12

mhofman commented 2 years ago

Maybe an alternative would be to somehow have the ability to flag objects so they cannot be used in a return override construction?

warner commented 2 years ago

For folks (like me) trying to understand the mechanics around this, I found https://hacks.mozilla.org/2021/06/implementing-private-fields-for-javascript/ to be a useful primer on private fields and the way a constructor can "override" the object being created by returning a value (instead of omitting the return statement as usual).

mhofman commented 2 years ago

Maybe an alternative would be to somehow have the ability to flag objects so they cannot be used in a return override construction?

An rough idea of how to accomplish this would be:

mhofman commented 2 years ago

For folks (like me) trying to understand the mechanics around this, I found https://hacks.mozilla.org/2021/06/implementing-private-fields-for-javascript/ to be a useful primer on private fields and the way a constructor can "override" the object being created by returning a value (instead of omitting the return statement as usual).

Neat! The linked article on the origins of the return override is also enlightening: https://www.mgaudet.ca/technical/2020/7/24/investigating-the-return-behaviour-of-js-constructors

FUDCo commented 2 years ago

There's an important piece of this story that I'm completely failing to grok from the various comments and links folks have given, namely: how does this particular combination of features or misfeatures enable the creation of a weak-keyed collection in user space? Because I'm just not seeing it.

mhofman commented 2 years ago

I think the most descriptive is the actual implementation: https://github.com/endojs/endo/blob/9600194abc53bf28ca877f9b8eeb6ef73c52b214/packages/make-hardener/src/AltWeakMapSet.js

FUDCo commented 2 years ago

That is whacky. How is this not just considered a spec bug?

mhofman commented 2 years ago

Because unfortunately return override was added on purpose for backwards compatibility in the ES6 days. And its implications on private fields must have been considered since there are spec algorithms that explicitly check for privates being already present when installing them, which can only happen in the return override case.

What this means is you can create WeakMap like semantics out of regular class and private fields syntax. Aka the spec made WeakMap semantics undeniable. I would love to plug the return override hole, but I don't know how doable that will be.

mhofman commented 2 years ago

If we wanted to tackle this by preventing specifically private fields addition, I think the following approach would work:

const facet = Object.create(
  Object.prototype,
  boundBehaviorPropertyDescriptors,
  {
    privateSealed: true,
  },
);

The advantage of this approach is that it doesn't create the ability for code to prevent extensions through the return override trick on 3rd party objects, only the creator can decide that.

It's not clear if a similar concept could apply to classes. It feels like the conceptual equivalent would be a final class modifier that prevents derived classes, which is arguably more far reaching than simply sealing the privates.

An options bag might also allow private declarations to be set on a regular object at construction without requiring the object literal syntax (as long as there is some kind of reification allowing expression usage) :

private #x as x;
const obj = Object.create(null, {}, { privateEntries: [x, 'privateValue'] });
assert(obj.#x === 'privateValue');

And finally it would allow an "integrity level caching" creation option as well.

mhofman commented 2 years ago

This came up on the html side with people stamping the window proxy object: https://github.com/whatwg/html/issues/7952