Proxies and preventExtensions: how to avoid inconsistency in a membrane?

# Alex Vincent (8 years ago)

<tldr>An ES7 invariant is forcing me to consider some truly nasty hacks for

my Membrane implementation... and I'm only getting started with practical use-cases. So I want the ECMAScript language contributors to consider the same, and maybe offer something I haven't thought of. :-)</tldr>

Consider the following code:

var x = { property1: true}; var extra = { isExtra: true }; var extraDesc = { value: extra, writable: true, enumerable: true, configurable: true }; var h = { ownKeys: function(target) { return Reflect.ownKeys(target).concat("extra"); },

getOwnPropertyDescriptor: function(target, propName) { if (propName === "extra") return extraDesc; return Reflect.getOwnPropertyDescriptor(target, propName); } };

var p = new Proxy(x, h); Reflect.ownKeys(p); /* ["property1", "extra"] */

Object.preventExtensions(x); // this works fine

Reflect.ownKeys(p); // throws an error

The ES7 spec requires this behavior: www.ecma-international.org/ecma-262/7.0/#sec-proxy-object-internal-methods-and-internal-slots-ownpropertykeys (see the very last invariant under the note following the algorithm)

A couple weeks ago, I wrote to this mailing list to announce a membrane implementation under development. One main reason for a membrane is that you can hand untrusted code a set of proxies from one "object graph" and protect the actual implementation from reaching the untrusted zone. So the first use case I came up with is "allow untrusted code to define properties that never make it to the protected objects." The reason why is to save clutter on the implementation (particularly for debugging), while giving the untrusted code more freedom. (I have many, many more use cases. This was just the simplest.)

If the protected objects are not extensible from the beginning, by rule, neither can any proxy be that uses the protected objects as proxy targets. That's irritating, but not a genuine problem. The real problem is when the target starts out extensible... and then, through no action of the untrusted code or proxies such code is given, the proxy target's extensibility ends.

Suddenly, the proxy for that target is broken:

  • ownKeys and getOwnPropertyDescriptor used to report additional local properties but are now forbidden from doing so
  • the extra properties came from untrusted code, so now the untrusted code can't get those properties back: instead errors are thrown

Revoking the broken proxy has even more side effects:

  • All of the proxy's properties are inaccessible - and trying to access them throws errors
  • Other proxies which refer to that proxy by property name have no way of detecting the proxy is revoked

One alternative I came up with is equally undesirable: generating an artificial target for each proxy that, through a WeakMap, refer to the desired targets.

  • A proxy can't be called as a function (or a constructor) unless the target is a function. (More precisely, it can't be called unless the target has a "[[Callable]]" attribute, so a Function() instance might work.)
  • An extensible proxy that refers to a non-extensible object must have an actual target that is extensible, and (probably via a WeakMap) allows the handler to get the real target.
  • For m object graphs to n lockable objects, you would need (m * n) non-primitive values that have to be defined so that the proxies can't be locked out of having their own properties that don't propagate to the original objects...

In fact, the way my membrane is written now, it is vulnerable to Object.preventExtensions() pulling the rug out from under it in this manner. (I have a property I expose in a similar manner to the example above, for debugging and testing purposes only. It will eventually go away.)

So the options I can see, under the rules, are either a whole bunch of carefully-chosen artificial targets or documentation that says "you can't provide local properties and disable the underlying objects' extensibility." I could go for a third, hybrid option which is opt-in-to-extensible-targets, again with documentation.

I'm writing to ask the following:

  1. Are there other options for the given problem that I am not seeing?
  2. Could the existing ProxyHandler part of the spec be extended, so that in the future I could override that invariant (and others, yet undetermined) explicitly?
  3. Could there be a Proxy.hasBeenRevoked(proxy) method?

(I'm aware I could use some internal tracking mechanism to detect when a reference from one Proxy to another, previously-defined one must be broken... but that defeats the purpose of a Membrane providing proxies for use in the first place.)

# Tom Van Cutsem (8 years ago)

The problems you are seeing are inherent to what you're trying to achieve. You're trying to build an abstraction (virtual extension properties that should continue to work even on non-extensible objects) that are directly in conflict with ES's invariants (e.g. if the object is truly non-extensible, it shouldn't be able to report additional properties, virtual or not (because the client can't tell the difference between an extension property and a normal property, and we're trying to protect clients from unexpected proxy behavior).

For the specific case of extension properties, the better alternative is to simply let the untrusted code use WeakMaps to attach properties to objects it doesn't control. Sadly the syntax is not as sweet as obj[prop], but this approach does completely sidestep the invariant issues (it's perfectly fine to associate frozen objects with additional state via a WeakMap).

Essentially, if you want to implement a membrane that works in the face of ES invariants, you need to use the "shadow target" technique where the membrane proxy is not directly targeting the "real" target object but instead a "dummy" target object that it can use to be more flexible in what it can answer. It seems that is more or less the solution you stumbled upon. See < tvcutsem/harmony-reflect/blob/master/examples/generic_membrane.js>

for a membrane implementation that works this way.

I'm writing to ask the following:

  1. Are there other options for the given problem that I am not seeing?

To my knowledge, the shadow target technique is the only way to build

proxies that need to be able to do more than their real target allows.

  1. Could the existing ProxyHandler part of the spec be extended, so that in the future I could override that invariant (and others, yet undetermined) explicitly?

What do you mean by "override that invariant"? By definition, an invariant

can't be overridden. Otherwise it's no longer an invariant.

  1. Could there be a Proxy.hasBeenRevoked(proxy) method?

I don't see why you would need this. Note that revokable proxies can't be

just revoked by any other object: on creation, the code that creates the proxy (presumably the membrane) is the only code that has access to the revoke function. Thus, it is always possible for the creator of the proxy to wrap the proxy's revoke function with additional actions that would need to occur before or after revocation, and share only the wrapped revoke function with the outside world.

, Tom

# Alex Vincent (8 years ago)

On Sun, Sep 4, 2016 at 1:14 PM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

Essentially, if you want to implement a membrane that works in the face of ES invariants, you need to use the "shadow target" technique where the membrane proxy is not directly targeting the "real" target object but instead a "dummy" target object that it can use to be more flexible in what it can answer. It seems that is more or less the solution you stumbled upon. See <tvcutsem/harmony-reflect/blob/master examples/generic_membrane.js> for a membrane implementation that works this way.

Ah, nuts. I suspected that might be the case. Well, it's ugly but it is doable within the rules, and I already have an appropriate code point to make that work out. (Nice that the first argument of each trap is the proxy target.)

Regarding .hasBeenRevoked, yes, I get that too. The revoke function from Proxy.revocable() handles only the proxy itself, no administration of metadata around that. Oh, well, it was worth a shot.

# Oriol Bugzilla (8 years ago)

Could there be a Proxy.hasBeenRevoked(proxy) method?

The Proxy constructor only accepts targets and handlers when they are objects and are not revoked proxies. I wonder what's the point of not allowing revoked proxies but allow revocable proxies which may be revoked later.

Anyways, this allows you to check if a value is a revoked proxy: you only need to ensure that it's an object but makes Proxy throw.

I think something like this should work:

Proxy.hasBeenRevoked = function(value) {
  if(Object(value) !== value) {
    return false; // Primitive value
  }
  try {
    new Proxy(value, value);
    return false; // Either non-proxy object or non-revoked proxy
  } catch(err) {
    return true; // Revoked proxy
  }
};
# Isiah Meadows (8 years ago)

Is a proxy a valid proxy target? If that's the case, then the indirection should allow you to do what you're doing. The outer proxy could freeze and let the inner proxy know it shouldn't extend anything anymore, without actually freezing it. The inner proxy can handle the rest of the actual logic in the membrane.