Proxies and preventExtensions: how to avoid inconsistency in a membrane?
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:
- 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.
- 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.
- 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
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.
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
}
};
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.
<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:
Revoking the broken proxy has even more side effects:
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.
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:
(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.)