You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Implement meta-object trap(s) to make an object's [[Prototype]] immutable #538
This is followup on making the global object's [[Prototype]] chain immutable. It was agreed that a proposal to extend this to be a full proxy trap, so that ordinary objects and arrays and so on could have immutable [[Prototype]], would be entertained, and I'd write up the spec-proposal for it. Said spec-proposal isn't complete yet, but I have an approximate start on it thus far.
Right now the gist of the idea is to add a [[SetImmutablePrototype]] trap and (on most objects) a [[MutablePrototype]] Boolean internal slot indicating mutability (initially true). I have this completed, generally sensibly enough, except for a few issues still. I'll post my current, un-landable progress after filing this issue and figuring out how to attach that progress.
on most objects) a [[MutablePrototype]] Boolean internal slot indicating mutability (initially true).
I think this woudl be better integrated into [[Extensible]]. So instead of being a boolean, [[Extensible]] becomes a tri-state "extensible", "immutable prototype", "non-extensible". It isn't possible to have an object that is non-existensible but with a mutable prototype, so a two-boolean design gives too many degrees of freedom.
In order to add a proxy trap, a corresponding Reflect method would need to be added. I suspect this would need to be part of a full proposal and maybe wouldn't be able to be just a PR.
https://github.com/jswalden/ecma262/tree/setimmutableprototype is the current quasi-state of what I have. It adds the extra [[MutablePrototype]] thing, rather than the [[Extensibility]] thing mooted already. The issue is that it only adds one trap, [[SetImmutablePrototype]], right now. But another is needed (or so it seems) to expose whether the [[Prototype]] is immutable. See the XXX bits in the branch above (and tell me how to attach its current state here, rather than literally just linking it). Does it seem like two additional traps are required here, or is there some other better approach for this?
Could we do this without a separate couple traps, and instead repurposing preventExtensions, passing an options bag indicating that we're just talking about the prototype? In the future, maybe this trap could also be used with a different flag for "deep freezing".
Imagine that it would look like this:
letobj={}Object.isExtensible(obj);// trueObject.isExtensible(obj,{proto: true});// trueObject.preventExtensions(obj,{proto: true});obj.property=1;// Worksobj.__proto__={};// throws in strict modeObject.isExtensible(obj);// trueObject.isExtensible(obj,{proto: true});// falseObject.preventExtensions(obj);obj.property=1;// throws in strict modeobj.__proto__={};// throws in strict modeObject.isExtensible(obj);// falseObject.isExtensible(obj,{proto: true});// false
On the Proxy side, the options bag could be passed as a second argument to the preventExtensions and isExtensible traps.
When running code that expects to be able to use this options bag when the JavaScript engine or Proxy doesn't support it, the observed behavior would only be more conservative: In the preventExtensions call, it would do even more freezing, and in the isExtensible call, it would return non-extensible in fewer cases. For this reason, from an ocap security perspective, I hope it would be considered safe. (A deep-freeze flag would not be conservative in the same way, for example.)
I don't know how other engines work, but the way V8 implemented immutable prototype exotic objects already supports objects transitioning into an immutable prototype state (though maybe caching of the map should be implemented if we go this way); I don't think the Proxy or Reflect changes will be particularly taxing to implement. But of course we should think this through and be confident that this is a good design.
I'm in favor of bringing this back too; thanks for starting the discussion, @littledan.
Could we do this without a separate couple traps
Out of curiosity, what's the motivation for this? I suspect we might be able to, but new traps seem easier to me, to spec correctly, to use correctly, and to feature-detect.
the observed behavior would only be more conservative: In the preventExtensions call, it would do even more freezing, and in the isExtensible call, it would return non-extensible in fewer cases
How so? It looks like it wouldn't do more freezing; rather it would do different freezing. Freezing the properties but not the prototype when you intended to freeze the prototype but not the properties seems undesirable.
Out of curiosity, what's the motivation for this? I suspect we might be able to, but new traps seem easier to me, to spec correctly, to use correctly, and to feature-detect.
I'd be fine with separate traps as well; I just got the sense that it'd be annoying to add two extra Proxy traps. Maybe, when we bring this to the committee, we can give them the multiple options to bikeshed over as well. I don't have a strong opinion here. Agree with you about option bags not being as easily feature-testable, but I'm not sure that means we shouldn't add options.
How so? It looks like it wouldn't do more freezing; rather it would do different freezing. Freezing the properties but not the prototype when you intended to freeze the prototype but not the properties seems undesirable.
This would be "conservative" in the sense that, if you have an old browser which doesn't support the options bag, it would silently do more freezing. And, for objects which are just prototype-frozen, like Object.prototype, it would report that they are non-extensible.
@ljharb Note that making the prototype immutable is even less strong than preventExtensions. No one has proposed a MOP operation for freeze in this thread.
That's very true - the only current operation that makes the prototype immutable is Object.freeze, so if we went with an options bag, I'd want the object to be guaranteed to make the prototype immutable, and make freezing the keys the backwards-compatible side effect.
Right, preventExtensions is our MOP operation to freeze the prototype, and passing the options bag would do a strict subset of what that operation does.
ah, my mistake :-) i'd forgotten that all of them freeze the prototype as well; in which case preventExtensions would be the right place to add the option.
That leaves this opinion:
It seems like a more robust approach is throwing when the desired functionality isn’t present - which separate methods/traps provides.
@zenparsing This could let a class always call its parent constructor, while continuing to let additional static methods be monkey-patched on, as discussed in tc39/proposal-class-fields#179
Activity
domenic commentedon Apr 12, 2016
Yay!
I think this woudl be better integrated into [[Extensible]]. So instead of being a boolean, [[Extensible]] becomes a tri-state "extensible", "immutable prototype", "non-extensible". It isn't possible to have an object that is non-existensible but with a mutable prototype, so a two-boolean design gives too many degrees of freedom.
ljharb commentedon Apr 12, 2016
In order to add a proxy trap, a corresponding Reflect method would need to be added. I suspect this would need to be part of a full proposal and maybe wouldn't be able to be just a PR.
bterlson commentedon Apr 12, 2016
Agree with @domenic, s/[[Extensible]]/[[Extensibility]] and tri-state seems good.
Add a way to make the [[Prototype]] of an object immutable, without h…
jswalden commentedon Apr 12, 2016
https://github.com/jswalden/ecma262/tree/setimmutableprototype is the current quasi-state of what I have. It adds the extra
[[MutablePrototype]]thing, rather than the[[Extensibility]]thing mooted already. The issue is that it only adds one trap,[[SetImmutablePrototype]], right now. But another is needed (or so it seems) to expose whether the[[Prototype]]is immutable. See the XXX bits in the branch above (and tell me how to attach its current state here, rather than literally just linking it). Does it seem like two additional traps are required here, or is there some other better approach for this?littledan commentedon Dec 17, 2018
I'd like to revive this thread, given its connection with tc39/proposal-class-fields#179 .
Could we do this without a separate couple traps, and instead repurposing preventExtensions, passing an options bag indicating that we're just talking about the prototype? In the future, maybe this trap could also be used with a different flag for "deep freezing".
Imagine that it would look like this:
On the Proxy side, the options bag could be passed as a second argument to the
preventExtensionsandisExtensibletraps.When running code that expects to be able to use this options bag when the JavaScript engine or Proxy doesn't support it, the observed behavior would only be more conservative: In the
preventExtensionscall, it would do even more freezing, and in theisExtensiblecall, it would return non-extensible in fewer cases. For this reason, from an ocap security perspective, I hope it would be considered safe. (A deep-freeze flag would not be conservative in the same way, for example.)I don't know how other engines work, but the way V8 implemented immutable prototype exotic objects already supports objects transitioning into an immutable prototype state (though maybe caching of the map should be implemented if we go this way); I don't think the Proxy or Reflect changes will be particularly taxing to implement. But of course we should think this through and be confident that this is a good design.
cc @erights @allenwb
bakkot commentedon Dec 17, 2018
I'm in favor of bringing this back too; thanks for starting the discussion, @littledan.
Out of curiosity, what's the motivation for this? I suspect we might be able to, but new traps seem easier to me, to spec correctly, to use correctly, and to feature-detect.
How so? It looks like it wouldn't do more freezing; rather it would do different freezing. Freezing the properties but not the prototype when you intended to freeze the prototype but not the properties seems undesirable.
littledan commentedon Dec 17, 2018
I'd be fine with separate traps as well; I just got the sense that it'd be annoying to add two extra Proxy traps. Maybe, when we bring this to the committee, we can give them the multiple options to bikeshed over as well. I don't have a strong opinion here. Agree with you about option bags not being as easily feature-testable, but I'm not sure that means we shouldn't add options.
This would be "conservative" in the sense that, if you have an old browser which doesn't support the options bag, it would silently do more freezing. And, for objects which are just prototype-frozen, like Object.prototype, it would report that they are non-extensible.
ljharb commentedon Dec 17, 2018
It seems like a more robust approach is throwing when the desired functionality isn’t present - which separate methods/traps provides.
If we overloaded, I’d expect the safer fallback to be freeze, not preventExtensions, because then it freezes more than intended (instead of less).
littledan commentedon Dec 17, 2018
@ljharb Note that making the prototype immutable is even less strong than preventExtensions. No one has proposed a MOP operation for freeze in this thread.
ljharb commentedon Dec 17, 2018
That's very true - the only current operation that makes the prototype immutable is
Object.freeze, so if we went with an options bag, I'd want the object to be guaranteed to make the prototype immutable, and make freezing the keys the backwards-compatible side effect.erights commentedon Dec 17, 2018
Also
Object.sealandObject.preventExtensionslittledan commentedon Dec 17, 2018
Right, preventExtensions is our MOP operation to freeze the prototype, and passing the options bag would do a strict subset of what that operation does.
ljharb commentedon Dec 17, 2018
ah, my mistake :-) i'd forgotten that all of them freeze the prototype as well; in which case
preventExtensionswould be the right place to add the option.That leaves this opinion:
littledan commentedon Dec 17, 2018
By throwing when it's not in place, you mean like Object.freezePrototype would throw as undefined is not a function?
ljharb commentedon Dec 17, 2018
Yup! That also (as mentioned earlier in the thread) makes feature detection and polyfilling much simpler.
zenparsing commentedon Dec 17, 2018
What's the use case for immutable-prototype-but-not-sealed?
ljharb commentedon Dec 17, 2018
@zenparsing for one, it would make
Object.prototypeno longer need to be exotic.littledan commentedon Dec 17, 2018
@zenparsing This could let a class always call its parent constructor, while continuing to let additional static methods be monkey-patched on, as discussed in tc39/proposal-class-fields#179
ljharb commentedon Apr 26, 2019
Linking to https://github.com/tc39/proposal-freeze-prototype
spec: GetOption reform for ECMA 2021 (tc39#538)
Throw a TypeError when calling `Intl.DisplayNames` with no options ar…