[Harmony Proxies] Proposal: Property fixing

Mark S. Miller erights at google.com
Thu May 12 19:03:07 PDT 2011


Hi Tom, thanks for putting these lists together. It will really help clarify
why we decided to enforce some invariants and not others. The crucial
distinction is that between
1) "momentarily invariant between two successive steps, i.e., given no
interleaving with other code".
2) "eternally invariant for all time" vs
Let's take the first two examples from each of your lists:

a) proxies can't take on the identity of other objects (i.e. '===' can't be
intercepted)
b) has('foo') returning true while getPropertyDescriptor('foo') returns
undefined

Let's write a test expressing each of the four combinations. In the
following code, assume the bindings of assertTrue,
Object.getPropertyDescriptor, and the test functions defined below are
stable, as varying them to cause the test to fail does not demonstrate a
violation of the invariant being tested.

  function test1a(p1, p2) {
    assertTrue((p1 === p2) === (p1 === p2));
  }

In ES5, I claim that there is no way I can call test1a that will cause it to
fail, even with conforming host objects. This establishes only that === is
stable between consecutive steps, i.e., without any interleaving.

  function test2a(p1, p2, interleave) {
    "use strict";
    var sameBefore = (p1 === p2);
    interleave();
    assertTrue(sameBefore === (p1 === p2));
  }

In ES5, I claim that there is no way I can call test2a that will cause it to
fail, even with conforming host objects. This establishes that the invariant
is robust across interleavings with other code. (The "use strict" above is
only to suppress a loophole that would actually be besides the point:
Otherwise, interleave could modify test2a.arguments[0], causing test2a to
fail for reasons other than violation of the invariant we're testing.)

  function test1b(p1, name) {
    "use strict";
    name = String(name); // possible interleaving here, which is why we need
"use strict" above
    assertTrue((name in p1) === (Object.getPropertyDescriptor(p1, name) !==
undefined));
  }

In ES5 augmented with only Object.getPropertyDescriptor, I claim that there
is no way I can call test1b that will cause it to fail, even with conforming
host objects. This establishes invariant #b in the absence of interleaving.

  function test2b(p1, name, interleave) {
    "use strict";
    name = String(name);
    var hasBefore = (name in p1);
    interleave();
    assertTrue(hasBefore === (Object.getPropertyDescriptor(p1, name) !==
undefined));
  }

In ES5 augmented with only Object.getPropertyDescriptor, I can cause this
test to fail as follows:

  var x = {foo: 8};
  test2b(x, 'foo', function(){ delete x.foo; });

So #a is eternally invariant while #b is only momentarily invariant.

The most significant weakening that proxies introduce, relative to our
ability to reason in ES5, is introducing more potential interleaving points.
In ES5, ('foo' in p1) could not cause code elsewhere to run, well, unless p1
is a host object. Since proxies introduce so many new interleaving points,
it should not be surprising that proxies can break momentary invariants --
they could anyway by the same means that we broke test2b. The important
question is: Which eternal invariants are we willing to lose? Let's go
through your list again and see which elements of which list break which
kind of invariant:


On Thu, May 12, 2011 at 5:40 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

> I thought it might be productive to try and list the relevant "invariants"
> that proxies may or may not violate. Here is an initial list, which I do not
> claim is complete. If people think it's useful I can also record them on the
> wiki.
>
> invariants that are enforced:
>
> - proxies can't take on the identity of other objects (i.e. '===' can't be
> intercepted)
>

Eternal.


> - immutable [[Prototype]]
>

Eternal for frozen objects. Not guaranteed eternal for non-frozen objects,
but interleavings can only violate this invariant by using non-standard
non-portable operations. Where these operations are available, this is a
momentary invariant. Elsewhere it is eternal.


> - unconfigurable instanceof (Proxy.create(handler, f.prototype) implies
> proxy instanceof f, function proxies are instanceof Function)
>

In ES5, (p1 instanceof F) is eternally stable only once p1's [[Prototype]]
chain is immutable and F.prototype is frozen. If F.prototype is not frozen,
then (p1 instanceof F) is not stable even when p1 is a proxy.

ES5 does specify that Function.prototype is frozen, so if p1 is a function
and p1.[[Prototype]] cannot be modified (for whatever reason), then (p1
instanceof Function) is eternally stable.



> - unconfigurable typeof (typeof objectproxy === "object", typeof
> functionproxy === "function")
>

typeof is eternally stable. The specified constraints on typeof's value are
therefore eternal invariants.



> - unconfigurable [[Class]] (Object or Function, respectively)
>

[[Class]] is eternally stable. The specified constraints on [[Class]]'s value
are therefore eternal invariants.




> - once preventExtensions, seal, freeze is called on a proxy, its properties
> are fixed, so the invariants associated with these operations are maintained
> (e.g. can't add new properties, can't delete existing properties, …)
>

In both cases, given that these operations return successfully rather than
throw. Eternal.



>
> controversial invariant:
>
> - proxies can't emulate non-configurable properties. If they would, proxies
> could still update attributes of non-configurable properties.
>

The ES5 constraints on updating non configurable properties are eternal
invariants.



>
> invariants that can be broken:
>
> - general inconsistencies between traps
>
>   - e.g. has('foo') returning true while getPropertyDescriptor('foo')
> returns undefined
>

Momentary, as established in the intro.


>   - e.g. has('foo') returning true while getPropertyNames() doesn't contain
> it
>

Momentary.


>   - e.g. get('foo') returning 42 while
> getOwnPropertyDescriptor('foo').value returns 24 (with no assignment
> operations happening in between)
>

The parenthetical acknowledges the momentary nature of the invariant. Since
the whole point of proxies is to interleave code anyway, the handler could
have simply performed said assignment.



> - traps that return values of the wrong type, e.g. getOwnPropertyNames not
> returning an array, getOwnPropertyDescriptor not returning a valid property
> descriptor
>

Eternal. I grant that this breaks the pattern.



> - inheritance: traps are free to ignore the proxy's prototype when it comes
> to property lookup
>

There are some implied eternal invariants that this does break. We should
enumerate them. But the obvious ones are momentary.



> - duplicate property names in the property listing traps (enumerate,
> get{Own}PropertyNames)
>

Eternal


> - the keys/enumerate traps should only return enumerable property names
>

Momentary



> - the keys/getOwnPropertyNames traps should only return "own" property
> names
>

Momentary



> - the result of getOwnPropertyNames should be a proper subset of the result
> of getPropertyNames (same for enumerate/keys)
>

Momentary



> - ... (I did not try to be exhaustive)
>

;)


So I'm rather calm about proxies breaking momentary invariants. We should be
very careful when we consider breaking eternal invariants.



>
> Because the ES5 spec is very implicit about most of these invariants, any
> distinction between which invariants to uphold and which not will be
> necessarily vague. However, I can discern some logic in the current
> distinction:
>
> Most of the enforced properties have to do with classification: instanceof,
> typeof and === are operators used to classify objects, and classifications
> typically come with some implied invariants (I'm aware of the fact that
> instanceof tests can be non-monotonic in JS).
>
> For {freeze|seal|preventExtensions}, one can make the case that defensive
> programming is one of their main use cases. Allowing proxies to gratuitously
> break them feels like taking away a lot of the usefulness of these
> primitives. The use case is not always defensive programming, e.g. frozen
> objects facilitate caching without cache invalidation.
>
>
> W.r.t. non-configurable properties: at this point I am convinced that
> Sean's API is better than the current design of outright rejecting
> non-configurable properties. Surely there will be cases where proxies will
> need to emulate non-configurable properties. Also, the fact that the default
> forwarding handler can't straightforwardly delegate getOwnPropertyDescriptor
> calls to its target (since it has to change the property's configurability)
> is a bad smell.
>
>
> Building on an earlier idea proposed by David ("inheritance-safe proxies"),
> a compromise could be as follows:
>
> - allow proxies to emulate/intercept non-configurable properties without
> checking
>
> - introduce an "ESObject" abstraction such that if h is a user-defined
> proxy handler, ESObject(h) creates a "safe" proxy handler that checks
> conformance of the handler w.r.t. the above ES5 Object semantics. This can
> be useful for catching bugs, or preventing misbehavior, depending on your
> POV.
>
>
> Whether or not ESObject should or could be fully defined in either the
> engine or in Javascript is an orthogonal issue.
>


This still enables the attacker to give to the defender an object that
breaks the old eternal invariants that the defender may have been relying
on.



>
> Cheers,
>
> Tom
>
>
> 2011/5/11 David Bruant <david.bruant at labri.fr>
>
>> Le 11/05/2011 08:41, Allen Wirfs-Brock a écrit :
>>
>>  I think we are dancing around one of the key differences between static
>>> languages and dynamic languages.  Static languages make guarantees about a
>>> set of potentially complex invariants  (for example, subtype conformance).
>>> They can do this because the necessary work to detect violations of those
>>> invariants is performed ahead of time before the program is allowed to
>>> execute.  Dynamic languages do most invariant validation as the program runs
>>> and hence generally restrict themselves to guaranteeing simple invariants
>>> (for example, memory safety) that can be cheaply performed many times as the
>>> program runs.  Dynamic languages generally avoid expense checking of complex
>>> invariants and instead assume that any critical violation of complex
>>> invariants will ultimately manifest  themselves as violations of the simple
>>> invariants that are checked.
>>>
>>> A related difference is that a static language generally rejects programs
>>> when it proves the set of all possible program inputs produces some states
>>> that violate the language's invariants.  The program is rejected, even if
>>> the input states that produce the invariant violations will never occur in
>>> practice.  This is a conservative (or pessimistic) approach -- if a program
>>> might fail, we assume it will fail.  Dynamic languages generally only reject
>>> programs (at runtime) when the actual data values used by the program
>>> violates the language's invariants.  This is a permissive (or optimistic)
>>> approach -- if a program might work, we give it the benefit of the doubt and
>>> let it run up to the point it begins to misbehave.
>>>
>>> The configurability restrictions on Proxies seems to be trying to apply a
>>> static language perspective to the very dynamic ES language.  They are based
>>> upon a complex invariant (what can/cannot be assumed after  observing the
>>> state of a configurable attribute).  Because, there is at best difficult to
>>> guarantee that user written proxy handlers will correctly enforce the
>>> invariants associated with of configurable:false it is forbidden for a proxy
>>> to set configurable to that state.  It is pessimistic, it says that because
>>> somebody might write a buggy proxy setting configurable we won't let them
>>> write any proxy that sets configurable.  An alternative that has been
>>> proposed is to try to dynamically enforce the configurable invariants.  But
>>> that is an example, of moving expensive (and probably highly redundant)
>>>  complex invariants checks into runtime.  While it would catch buggy
>>> programs, but has the potential of imposing a significant runtime
>>> performance penalty on valid programs.
>>>  The normal dynamic language approach to this sort of problem is to be
>>> optimistic about the validity of the program while continuing to guarantee
>>> memory safety, and depending upon conventional testing procedure to detect
>>> more complex error conditions.
>>>
>> I understand the rationale that leads to the difference you describe in
>> static/dynamic languages design. I understand it and I think these are good
>> reasons. However, I can't help asking you some sort of proof. Has some
>> research been done in the area?
>> Are there dynamic languages that tried to enforce invariants at run-time?
>> What lessons did they learn from that experience?
>> Was the cost on valid program big enough to question these checks?
>> Are there examples of dynamic languages interpreter with static analysis
>> that were able to diminish this cost? Diminishing the cost to make the
>> program "as fast in the long term"? (I quote, because I know that "as fast"
>> and "in the long term" are vague notions)
>>
>> David
>>
>>
>> _______________________________________________
>> es-discuss mailing list
>> es-discuss at mozilla.org
>> https://mail.mozilla.org/listinfo/es-discuss
>>
>
>
> _______________________________________________
> es-discuss mailing list
> es-discuss at mozilla.org
> https://mail.mozilla.org/listinfo/es-discuss
>
>


-- 
    Cheers,
    --MarkM
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20110512/bcadd9ba/attachment-0001.html>


More information about the es-discuss mailing list