-
Notifications
You must be signed in to change notification settings - Fork 113
Could this be done with "private symbols" instead? #115
Comments
I might have more to say later, but a quick note while I'm thinking of it:
These two statements aren't compatible: |
Read this statement a little more closely. 😉 (Emphasis mine just now) I'm proposing to change that behavior as well, just as a follow-on. I'll edit to clarify. |
As someone who has tried to convert existing codebases that are using WeakMap privacy to use the stage 3 private fields proposal instead, I’d like to share that
I am confused about prototype involvement here for reasons related to what bakkot said. However forwarding through proxies (across the board, even for intrinsic and host APIs) would be a godsend. One concern about the proposed API: |
|
@isiahmeadows that seems reasonable to me. I understand where you’re coming from with inherited properties now. I had imagined this as bypassing ordinary lookup. A minor thing to note is that existing slots work cross-realm. Redefining intrinsic/host slots in terms of private symbols does not prevent this from remaining true, but it is a case where user-created private symbols would differ in capability (as Symbol.for defeats the point of privacy ... and just about everything else symbols offer). |
@bathos I considered a private equivalent for |
@bathos Can you say more about the issues you ran into with the Stage 3 proposal? |
@littledan, I’ll see if I can provide a better summary later if it helps, but for now here’s some related posts on esdiscuss where I described the issues I ran into when attempting conversion: https://esdiscuss.org/topic/proposal-object-members#content-30 the tl;dr is that today’s scope-based hard privacy has served me well when dealing with APIs composed of interconnected objects because I had full control over visibility — the issues with WeakMap chiefly concerned ergonomics rather than functionality. When attempting conversion, some things that were easy with WeakMaps became strange and verbose, and some things weren’t possible at all. Eventually I gave up and returned to WeakMaps. |
@isiahmeadows I like the general direction of this proposal a lot, but the tunnelling-through-proxies behavior has me worried that private symbols can't be used for reliable brands-checks, diminishing the value of "hard private" fields. Consider: // Alice writes:
let brand = Symbol.private()
export function createBrandedObject(v) {
return Object.freeze({ [brand]: true, x: v, m() { return this.x * 2 } });
}
export function useBrandedObject(obj) {
if (obj[brand]) { // brands-check
// assuming obj is a branded object, Alice would never expect this to fail
assert(obj.x * 2 === obj.m())
}
throw new TypeError("illegal argument")
}
// Eve writes:
let target = alice.createBrandedObject(24)
let proxy = new Proxy(target, {
get(t, name, r) {
return (name === "m") ? ( () => 0 ): Reflect.get(t, name, r);
}
})
alice.useBrandedObject(proxy)
// Eve fools Alice into thinking proxy is a branded object
// the assertion fails (42 === 0), violating Alice's assumptions/invariants
// in the general case, an attacker can make use of Alice's confusion attn @erights |
@tvcutsem that’s a good point. As much as I wish Proxies proxied brandedness — it’d sure make them more useful to everybody but Alice — those assurances would be lost. This is also an issue for using standard prototype chain property lookup as proposed above. How would people feel about a more conservative variation where:
Speaking for myself, even with these adjustments, this behavior would still address all problems I ran into with private fields. It would match the existing semantics of slots rather than altering them, I think. |
For the sake of illustrating @tvcutsem’s point less abstractly, I’d like to show an example of how this would matter for an existing intrinsic operation,
If slots were realized as private-symbol-keyed properties that could be inherited and proxied, the invariants here could indeed be violated: const p = Object.create(/foo/);
p.exec({
toString() {
Reflect.setPrototypeOf(p, null);
return '';
}
}) Anywhere a slot-existence check as in step 3 of |
@bathos For a concrete example, that would be restructured as follows:
That would fix it while still permitting the assertion to hold. |
I left that part implicit within the proposal when I specified proposing using those instead of internal slots. In general, mutable state would have to be referenced like that, but that's more of a practical concern than something purely theoretical. |
That makes sense I think — though it likely means making a lot of changes throughout both the ES spec and other specs that define or reference slots, like HTML. I don’t know if that is seen as problematic or not, but it’s a big touch. |
@bathos It would result in a rather big diff due to the existing pervasive use of internal slots, but the change behaviorally would be minimal and it'd affect almost nobody. The main differences is that fewer The WebIDL spec would need updated as well as a few places within the HTML spec, but most other specs would be largely unaffected. I'm not TC39, but I don't believe the change is that problematic. |
One additional thought re: tvcutsem’s demonstration + the private state object solution: A scenario which the private state object doesn’t address is user code that relies on slot-accessing brand checks as a way of testing whether a value is safe for subsequent method invocation. For the category of values for which such checks presently return negative that would now return positive, the invariant that this status will always remain true will be gone; and because this subset of values cannot be distinguished from the existing positive set, such checks would become unreliable in this regard categorically (at least as a technicality). Contrived, but for example: const { apply } = Reflect;
const { exec } = RegExp.prototype;
const { get } = Reflect.getOwnPropertyDescriptor(RegExp.prototype, 'global');
function getPatternExecutor(value) {
try {
if (apply(get, value, []) !== undefined) return s => apply(exec, value, [ s ]);
} catch {}
throw new TypeError(`must be a valid RegExp instance`);
} Presently, if the s => apply function gets returned successfully, there is a perfect guarantee that its Does this matter? It would seem to me that the answer is "probably not", but I’m wary of assuming a change like that is safe too quickly and wanted to provide another example that falls outside of intrinsic/host cases as food for thought. |
@bathos Your scenario above (user code using a brands-check to test whether a value is safe for subsequent method invocation) is a subset of the kinds of implicit assertions/invariants that I wanted to highlight in my (contrived) example. Does it matter? I empathize with your "probably not" answer: developers generally won't want to break existing code and most uses of proxies will be 'well-behaved'. But when reasoning from a security/defensive programming standpoint, we can't assume all developers will be cooperative. All that matters is that this is a potential exploit for an attacker to use, and what makes it dangerous is that private symbols are advertised to introduce "hard private" state, which proxies can't violate directly (they can't "steal" the private state) but can indirectly (by making an object that passes a brands-check behave in arbitrary ways). @erights can probably say more from his experience in building secure sandboxes why/how this matters in practice. Here's a thought to fix this: rather than tunnelling private symbol access through proxies, or letting This in effect makes private symbols even more similar to WeakMaps, because a WeakMap also treats a proxy object as a separate key. Assuming this semantics and revisiting my earlier example, in order for Eve to let the check |
BTW, you don't need anything special for private state for proxies - just
use the handler object itself for state.
…On Mon, Aug 6, 2018, 08:07 Tom Van Cutsem ***@***.***> wrote:
@bathos <https://github.com/bathos> Your scenario above (user code using
a brands-check to test whether a value is safe for subsequent method
invocation) is a subset of the kinds of implicit assertions/invariants that
I wanted to highlight in my (contrived) example.
Does it matter? I empathize with your "probably not" answer: developers
generally won't want to break existing code and most uses of proxies will
be 'well-behaved'. But when reasoning from a security/defensive programming
standpoint, we can't assume all developers will be cooperative. All that
matters is that this is a potential exploit for an attacker to use, and
what makes it dangerous is that private symbols are advertised to introduce
"hard private" state, which proxies can't violate directly (they can't
"steal" the private state) but can indirectly (by making an object that
passes a brands-check behave in arbitrary ways). @erights
<https://github.com/erights> can probably say more from his experience in
building secure sandboxes why/how this matters in practice.
Here's a thought to fix this: rather than tunnelling private symbol access
through proxies, or letting [[Get]] bail for private symbol access on
proxies, as you suggested earlier, we could let proxy objects have their
own private state. In other words, if sym is a private symbol and proxy
is a Proxy object, then proxy[sym] could lookup or update the private
symbol on the proxy object itself.
This in effect makes private symbols even more similar to WeakMaps,
because a WeakMap also treats a proxy object as a separate key.
Assuming this semantics and revisiting my earlier example, in order for
Eve to let the check if (obj[brand]) succeed, she would need to
explicitly 'copy' the brand private symbol from the target to the proxy,
which she can't, because Alice hasn't exposed the brand symbol. However,
in other scenarios where the creator of the proxy object also has access to
the private symbol, it would be perfectly legit for the proxy creator to
explicitly copy the private symbols onto the proxy.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#115 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AERrBKgfuZFoIUl3YO85e0QF0O3piN4-ks5uODF8gaJpZM4VlnKR>
.
|
It has been helpful for me to separate branding from encapsulation. As @tvcutsem points out, "private symbols" would not be appropriate for branding, although they would be perfectly adequate for encapsulation. In all of our attempts to model "private state" we've conflated these two concepts. What would the outcome be like if we separated them? WeakSet would provide the branding mechanism and private symbols would provide encapsulation: // The "branded Point" class
const pointBrand = new WeakSet();
const $x = Symbol.private('x');
const $y = Symbol.private('y');
function brandcheck(obj) {
if (!pointBrand.has(this)) throw new TypeError();
}
class Point {
constructor(x, y) {
this[$x] = x;
this[$y] = y;
pointBrand.add(this);
}
toString() {
brandcheck(this);
return `<${ this[$x] }:${ this[$y] }`;
}
add(other) {
brandcheck(this);
brandcheck(other);
return new Point(this[$x] + other[$x], this[$y] + other[$y]);
}
} Requiring explicit brand checks at the start of method bodies clears up one of the issues I've always had with the current private fields proposal: it's very easy to accidentally run code before accessing a field and throwing a TypeError. We really want the brand check to happen at the beginning of execution. @tvcutsem What are your thoughts? |
@zenparsing Thank you. I have been wondering why everyone keeps mentioning brand checking in the context of private fields and I haven't understood why they would be tied so closely together. I would have expected that it could be very easily handled with a decorator if that's something desired. |
cc @erights does this proposal meet the invariants that you have been maintaining for what property access means? |
Part of the value of the current stage 3 proposal is that you get brand checks "for free". I don't see what value we get out of removing them from the proposal. |
@ljharb You get them for free only if and when the access happens. In practice, esp. when implementing host API stuff or things meant to follow its patterns closely, the check needs to be immediate and unconditional or else the details of the implementation (not the private state) may become observable. (This was one of the reasons I returned to WeakMaps — even in cases where there were no functionality limitations preventing me from using While private symbols don’t remove that boilerplate either*, I think @zenparsing’s distinction is pretty compelling and the example reads nicely.
|
@bathos sure, you'd need a |
At least in the host examples, to obtain consistent messaging, it’d be try {
this.#foo;
} catch {
throw new TypeError('Illegal invocation');
} I don’t think that’s better than the weak collection approach, which can be served by a helper function as in zenparsing’s example. |
@ljharb That wouldn't be very explicit and also not very free. You now have to maintain your private variable name in all of your methods. It would be much better code to have something that explicitly brand checks rather than arbitrary field names littered throughout your codebase. |
Let's step back a bit. My understanding of the history here is that @waldemarhorwat proposed that undeclared private field access throw as part of an effort to give private fields better, more reliable behavior than ordinary properties, given our opportunity in this proposal to make a more restricted mechanism, and as part of a general encouragement of static shape. This decision follows our general invariant that private fields and methods are just like public ones, except that some things "don't work", either with runtime errors or early errors. These semantics could help avoid real bugs! And we've already heard positive feedback in this repository on the change. The ability to do brand checks may be nice, but I think the semantic stands justified on its own without that usage pattern. |
@littledan I’m curious if you got a chance to check out the posts I linked to earlier. The class-declaration-scoping of the private property proposal was the primary thing that prevented me from switching over. Do you have any suggestions there? Is it the position of the current proposal that such use cases do not need to be served and WeakMap for private state should continue to be the solution there? |
@bathos Apologies for my delay here. My hope is that decorators will provide a useful mechanism for friendship while maintaining strong encapsulation. See https://github.com/tc39/proposal-decorators/blob/master/friend.js for some examples of how this can be done. Would this sort of mechanism work for you? |
@littledan it seems like it might. I’m gonna have to dig in more to understand this though (like, what is the type of the "key" value that decorator observes in PrivateName?) |
I'll be interested to hear your thoughts! The type of a PrivateName key is 'object'. |
@ljharb but brand-check is another feature and I don't see ANY reason for combining it with encapsulation if it breaks metaprogramming scenarios, because creating of As conclusion, you don't get |
@Igmat Truth is, any fully encapsulating |
@rdking, only if |
@Igmat Brand-checking is just checking for knowledge of something that can't otherwise be known. The symbolic name of a private field is such a brand. Simply accessing a private field is therefore a brand check. It doesn't matter if it's the current proposal, mine, or |
There's two things that are normally considered "brand checking":
The second necessarily requires the first, but the first doesn't obligate the second. As far as I understand (I'm not part of TC39 itself, or I'd have that "Member" badge in the upper right of every comment I make), people within the committee don't all agree on it being okay to not assert the lack of a brand. As for dynamically defined private symbols vs statically defined private fields, here's the main difference:
Each one can give rise to the other, but there are pros and cons to each:
Keep in mind, the two abstractions are equivalent mathematically, just they have very different strengths in practice. And on top of that, the implementation of each would be closer than what you might expect, thanks to the various edge cases. Details on how implementations would overlap - hidden for brevity
Even though they expose different APIs, they would likely work more or less the same way internally:
|
We discussed @zenparsing 's private symbols proposal at the September 2018 TC39 meeting, and concluded that we are not adopting that approach and sticking with the proposal in this repository. |
@littledan I've checked your presentation and it has some falsy statements about that everybody agreed with limitations of this proposal. |
@Igmat I think they've trying to make it clear that "everybody" is limited to the current proposal contributors and members of the TC39. It doesn't matter that their reasoning contains logical contradictions as long as that collection of people is willing to accept them. |
@lgmat Are you referring to "Everyone I've talked to sees # as reasonable when I explain it" (from this slide)? I guess the phrasing was a bit rough, but I meant my experiences meeting developers in conferences, etc when there's a chance to discuss a topic in person. I didn't mean that everyone in the world who understands this proposal likes the |
Edit: This proposal lives here now. I'm leaving the original proposal below for archival purposes, but you should refer to here for the latest proposal text. Also, if you find a typo, have questions, or anything similar that's too specific to be here, feel free to file an issue there.
Original proposal
*Yes, I know I'm proposing a variant of [something that's been proposed before](https://esdiscuss.org/topic/proposal-about-private-symbol).*The basic idea here is that instead of explicitly creating weak maps, using symbols you can't enumerate. It's also mostly polyfillable (fully if leaks are allowed), and it requires few changes to the spec and runtimes to support. Something like this might work:
Symbol.private(desc?)
- Create a local private symbol with the optional given description.Symbol.isPrivate(sym)
- Check if a symbol is private.Object.getOwnPropertySymbols()
and friends ignore private symbols. There is no way to enumerate them without storing them in your own data structure.ownKeys
would be modified accordingly.There are several perks to this:
And of course, there are cons:
async
/await
is to promises - it takes most of the grief out of the common case, while still letting you dive deep when you need to.Examples
Here's the counter example from the private methods proposal, adapted to use private symbols.
If you want to emulate the existing class field proposal, you can create a wrapper to check the object appropriately:
A follow-on proposal for syntax sugar
A follow-on proposal could add syntax sugar for private symbols, detailed in the code snippet below. Consider it to this as
async
/await
was to promises. There are perks to making it pure syntax sugar:async
/await
doesn't require an engine to even use promises in the middle - it's all just callbacks and microtasks internally.Note that with this sugar, you can't combine both
this.#xValue
andthis[xValue]
- you can only use one or the other, since the names are generated uniquely per-name, per-scope. If you need to expose them to subclasses or friend classes, you should use normal private symbols and export them instead. Additionally, you can't access the private symbols' descriptors using this sugar - you also need to use normal private symbols if you wish to do that.There are a few optimizations you can make to the transpiler output, as I demonstrated above:
And, a partial polyfill.
There are two caveats with this polyfill:
TypeError
any timeownKeys
is called on a frozen object with a private symbol. Avoiding this is extremely non-trivial because the invariant for that method needs modified to exclude private keys from the list it checks against, but because the spec doesn't callownKeys
implicitly from syntax exposing symbols returned from it, I could just overwrite all the methods that delegate to it. Of course, this would double or even triple the size of the polyfill below, so I ignored it.I know this seems a lot like what was proposed before, but I do feel it's sufficiently different, and it does address most of the various pitfalls in the other past "private symbol" proposals:
WeakMap
methods are unmodified, and 2. proxies aren't involved (which change thethis
value).The text was updated successfully, but these errors were encountered: