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

Alex Vincent ajvincent at gmail.com
Sat Sep 3 05:37:48 UTC 2016


<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:
http://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.)
-- 
"The first step in confirming there is a bug in someone else's work is
confirming there are no bugs in your own."
-- Alexander J. Vincent, June 30, 2001
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20160902/652e2647/attachment.html>


More information about the es-discuss mailing list