Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Object.fromEntries ignores this / Symbol.species #2582

Closed
WebReflection opened this issue Nov 18, 2021 · 53 comments
Closed

Object.fromEntries ignores this / Symbol.species #2582

WebReflection opened this issue Nov 18, 2021 · 53 comments

Comments

@WebReflection
Copy link

WebReflection commented Nov 18, 2021

Description:

The following code works:

class MyArray extends Array {}
MyArray.from([]) instanceof MyArray;
// true

while the following code doesn't:

class MyObject extends Object {}
MyObject.fromEntries([]) instanceof MyObject;
// false

Even after setting explicitly species:

class MyObject extends Object {
  get [Symbol.sepcies]() { return this; /* or MyObject */ }
}

I find this inconsistency very surprise prone, and I think this is a specification bug, because we can brand our own Arrays but we cannot brand our own Objects unless we use Object.setPrototypeOf(Object.fromEntries(arr), MyObject.prototype) which is verbose, ugly, and also slow.

Thank you for considering updating specs around this behavior.

@zloirock
Copy link

  1. Subclassing of static methods like Array.from does not use @@species, they use just this.
  2. It was discussed in Object.fromEntries proposal and was decided not to do it How about fromEntries() for extended Object class proposal-object-from-entries#26, however, I agree with you.

@WebReflection WebReflection changed the title Object.fromEntries ignores Symbol.species Object.fromEntries ignores this / Symbol.species Nov 18, 2021
@WebReflection
Copy link
Author

  1. thanks for the correction, I've updated the title accordingly
  2. I don't understand why Object.fromEntries cannot use this too ...

We're using builtin extend to actually secure some of our code that lands in user-land-minefield where there are evil scripts overriding Object.prototype.hasOwnProperty, or public methods, as example, and we instead seal that method as own property to the single builtin extend:

class SecuredObject extends Object {
  static get[Symbol.species]() { return SecuredObject; }
}
sealAsOwnPropertiesStatics(SecuredObject);
sealAsOwnPropertiesPrototype(SecuredObject.prototype);

And then we discovered that fromEntries doesn't work so we need to pass through setPrototypeOf after using fromEntries to create our own sub classes ... this is counter-intuitive to say the least, but surely unexpected, and in the name of "increased security" it looks like not everyone thought about using subclassing to actually seal/secure native classes, at least until import Object from 'std::Object' exist ... and there again, with all other builtins too it'd be awesome to extend and have consistent behavior.

@bakkot
Copy link
Contributor

bakkot commented Nov 18, 2021

The collection of static methods on Object should be understood to be a namespace, not a class; having it respect Symbol.species would be extremely surprising to me.

@bakkot
Copy link
Contributor

bakkot commented Nov 18, 2021

In any case, this isn't the right forum for this discussion; to propose changes to the language, please open an issue on es-discourse, as mentioned in CONTRIBUTING.md.

@bakkot bakkot closed this as completed Nov 18, 2021
@WebReflection
Copy link
Author

@bakkot I'll do that when I can, but I consider this a bug, not a feature request. Object is the root of all classes/instances, and new Object has been possible since about ever.

@bakkot
Copy link
Contributor

bakkot commented Nov 18, 2021

It's not a bug - it was intentional, it matches web reality, and it does not violate any assertion or written invariant.

It's an inconsistency, at best, and whether it is in fact an inconsistency is a matter of perspective.

@WebReflection
Copy link
Author

Fair enough, but I consider inconsistencies more like bugs, so there is where we can agree to disagree, and the reason I opened this here instead. Anyway, point taken, I'll raise this in the proper channel, thanks for your patience 👍

@claudepache
Copy link
Contributor

claudepache commented Nov 18, 2021

See this explanation from @allenwb , and specifically:

@@​species is primarily for use in instance methods that create derived collections whose class isn't explicit stated at the usage site.

With MyArray.from(...), you already indicate explicitly that the collection should be of kind MyArray; there is no point to invoke the @@​species-mechanism in order to determine the right kind of collection.

EDIT: Specifically, I’m saying that even if we consider that MyObject.fromEntries(...) should “work”, it is not by invoking Symbol.species.

@WebReflection
Copy link
Author

@claudepache sure, I think @zloirock already mentioned that, and it was my bad ... however, this is all I am after:

With MyArray.from(...), you already indicate explicitly that the collection should be of kind MyArray;

precisely that ... with MyObject.fromEntries I am explicitly indicating I want an instance of MyObject as result.

@WebReflection
Copy link
Author

WebReflection commented Nov 18, 2021

For history/documentation sake, the Object constructor (class) suffers the same curse other primitives suffer.

class MyString extends String {}
new MyString(" abc ").trim() instanceof MyString;
// false

Object extends there are all the same, but the funny part is that Object has only prototype's methods that return not objects, but it also has a single fromEntries method that is related to the constructor, while everything else is a mistake from the past, where Reflect didn't exist ... everything else looks like a static namespace trashbin with duplicates in Reflect namespace too ... at least Reflect is not a constructor though, Object is.

So, how big of an argument is that Object is not meant to be used as class when:

  • it is practically a class
  • it can be subclassed like every other classes
  • it's literally at the root of inheritance of every other class
  • it has a single method that is supposed to create an instance of its invoker, that doesn't work like that

These are all the reason I've opened an issue here: this is a bug to me, nothing more, nothing less.

@ljharb
Copy link
Member

ljharb commented Nov 18, 2021

Yes, because Object is special, and isn’t meant to be subclassed in that manner. Object.getOwnPropertyDescriptor doesn’t look at the receiver either. As was said above, Object is a namespace for its static methods; they’re not part of a class.

@ljharb
Copy link
Member

ljharb commented Nov 18, 2021

Put another way, if Object worked this way, then Boolean.fromEntries and Function.fromEntries would work as well, because proper class inheritance means the constructor inherits too. They don’t, though, because Object is special.

@WebReflection
Copy link
Author

WebReflection commented Nov 18, 2021

Put another way as well:

Object.prototype.typeof = function () {'use strict';
  return typeof this;
};

(true).typeof();
// "boolean"

I understand the special part of Object is the fact nothing inherits from its static properties, which is OK, core speaking, but the moment one can extend it, and all statics are inherited, I don't understand what's so special about the only method that shouldn't fail expectations which is fromEntries.

I actually wouldn't be here if:

  • class extends Object {} would've thrown in the first place
  • class MyObj extends Object {} wouldn't inherit all static methods too, including fromEntries
  • I don't think we really need any other reason to not expect fromEntries to work after what you also said ... yeah, Object is special, but it can be extended, as class, like everything else

@zloirock
Copy link

Object.getOwnPropertyDescriptor doesn’t look at the receiver either.

TBC, I'd prefer to have something like this:

NullProtoObject.getOwnPropertyDescriptor(...) instanceof Object; // => false

@bathos
Copy link
Contributor

bathos commented Nov 18, 2021

I don't think this came up at all when it was a proposal - probably because everyone was already just on the same page with how @ljharb described it above.* It was intended as a utility for "generic" object transformations with no more specific relationship to Object-as-constructor than Object.entries (which returns an array of arrays, but has no Symbol.entrySpecies hook - would be pretty weird to have an internal subclass extensibility contract for one but not the other).

Edit: Missed zloirock's link earlier - sorry, I see it did have prior discussion, I'd forgotten.

Is shadowing fromEntries in the subclass not acceptable here? super is a more generic (and less problematic) extensibility affordance than @@species:

class SpecialObject extends Object {
  static fromEntries(iterable) {
    return super.setPrototypeOf(super.fromEntries(iterable), this.prototype);`
  }
   
  // or maybe, if the subclass constructor here does its own stuff or adds a brand?:
  
  static fromEntries(iterable) {
    return new this(super.fromEntries(iterable));
  }
}

Like other plain-object returning ops, it doesn't actually invoke the/an Object constructor. With no constructor in the mix, I don't think it makes sense to say a constructor's species is not being honored.

* There was however a discussion about specifying a prototype argument iirc. Although that wasn't where things landed, I think that would make more sense than @@species here, since the other "make a new object" static method, create, also does.

@bathos
Copy link
Contributor

bathos commented Nov 18, 2021

while everything else is a mistake from the past, where Reflect didn't exist ... everything else looks like a static namespace trashbin with duplicates in Reflect namespace too ... at least Reflect is not a constructor though, Object is.

FWIW, the Object variants, despite their similar names and behaviors, remain useful. The Reflect variants specifically implement the Proxy handler contract - for example, they return false where the Object variants would throw (which is what you'd usually want - a Proxy will ultimately throw for most false results, too) and will return true where the Object variants would return the first argument (e.g. preventExtensions, defineProperty). The Object variants are generally safer and more ergonomic (e.g. can nest calls) unless you're writing proxy handlers (modulo ownKeys, which is indispensible).

@WebReflection
Copy link
Author

WebReflection commented Nov 18, 2021

Is shadowing fromEntries in the subclass not acceptable here?

my question is: why is that needed in the first place ???

there's no rationale to explain why methods are inherited but methods that construct resulting instances (just one, fromEntries) shouldn't consider this constructor, just like Array.from does, or every other extend.

Moreover, the reason we realized this footgun is that we secured classes so that every super.access is potentially poisoned and nobody is safe .. that's what we're dealing with, yet I don't think there's any valid reason to keep that method Object only.


edit also worth mentioning this was literally the first post:

unless we use Object.setPrototypeOf(Object.fromEntries(arr), MyObject.prototype) which is verbose, ugly, and also slow.

yes, we measured performance too .. it's 2 to 10 times slower on average, even pre-securing setPrototypeOf and fromEntries

@bakkot
Copy link
Contributor

bakkot commented Nov 18, 2021

just like Array.from does, or every other extend

Note that Array.from is not like every other builtin: it falls back to Array when this is undefined, such that

(0, ArraySubclass.from)(whatever)

returns an instance of Array (not ArraySubclass!), rather than throwing. This makes it different from e.g.

(0, Promise.resolve)(whatever)

which throws, or Proxy.revocable, which ignores its this entirely.

So if your goal here is that your subclass should behave just like the superclass except using its methods will produce instances of your subclass instead, you'll need to explicitly override from and of anyway.

Short of auto-binding getters, this tradeoff is inherent: either you can't use the "static method" as a function, or you can but then it doesn't do what it looks like if you pull it off a subclass instead of the base class. Both choices have significant downsides.

@WebReflection
Copy link
Author

Note that Array.from is not like every other builtin: it falls back to Array when this is undefined, such that

perfect, makes it consistent with Object too, pretty please?

if you justify MyArray.from(...) to return a MyArray, as it does, please make MyObject.from ... ahem ... MyObject.fromEntries return precisely the kind of constructor/instance one would expect, thanks!

Should it fall back to Object when this is undefined? Absolutely, and who cares, we had bind centuries ago for reasons.

@WebReflection
Copy link
Author

if anything left to say: please find any piece of code that would break in the world if Object.fromEntries actually returned the expected instanceof if called from a class extends Object {} ... pick just one library used out there that believes that, and I'd be sold this is not a specification bug, thank you.

@bathos
Copy link
Contributor

bathos commented Nov 18, 2021

my question is: why is that needed in the first place ???

I think, as others mentioned, subclassing Object being possible doesn’t imply it’s something the language has aimed to facilitate or privilege beyond “Object should do something rational when invoked with a new.target other than itself”.

I can try to enumerate the reasons why I think* Object subclassing is a technically-possible thing rather than a pattern facilitated or encouraged by the language. It won’t be anything you don’t already know I’m sure, but maybe seeing it laid out will be helpful anyway (e.g. so that you can pinpoint which aspects of these lines of thought you don’t think are sound.)

(This list isn’t meant to be like ... nailing up theses or anything. It’s my best understanding of why-things-are-as-they-are and some why-that's-reasonables, but the most subjective items are indeed subjective and not “correct”.)

  1. Object doesn’t represent anything in itself (unlike e.g. Date)
  2. Object doesn’t model a unique data structure (unlike e.g. Set)**
  3. Object doesn’t accord any unique behavior to instances it mints (unlike e.g. Array)
  4. The Object constructor exhibits special behavior when invoked using super() or Reflect.construct with a different new.target — effectively, to do nothing. It reimplements the normally-syntactically-directed behavior that would occur if the subclass weren’t derived. This special case exists (afaik) to prevent the strange/chaotic return override that would otherwise occur. This makes subclassing Object a kind of noop ... because the alternative was a surreal footgun***.
  5. Objects created with new Object are indistinguishable from objects which are not created this way.
  6. Object.prototype is the default prototype for nearly all objects, e.g. those created syntactically, and algorithms that produce such objects do not do so by invoking the Object constructor. Neither do any of the other intrinsic constructors despite their associated prototypes all inheriting from Object.prototype. The spec creates new objects that are instanceof Object constantly, yet the Object constructor / its algorithm play no role in this.
  7. The members of Object.prototype have themselves been a source of problems historically and presently. I believe TC39 members have stated directly that no new methods will ever be added to it. If a “class”’s primary API’s existence is problematic enough for that interface to end up cordoned off like a live minefield, this suggests pretty strongly that it may not constitute (conceptually) much of a class.
  8. Partly because the prototype is off-limits for further builtin extension, Object’s static space has developed into a hosting zone / namespace for utility functions that manipulate or produce objects at a relatively “low level”****. These operations are applicable to object (lowercase for language type) values regardless of whether they were created with the Object constructor, whether they inherit from Object.prototype, or whether they are exotic. (Well, those and — I’ve always wondered why... — Object.is.)

Affordances specifically meant to improve ergonomics of subclassing Object seem unlikely to get a buy-in given the above — but affordances that do so and also place a performance tax on non-subclass usage, even a very minor tax, seem particularly unlikely to find sympathies.

All that said, the case you’ve described (realm-poisoning paranoia) is very interesting and I’m curious to hear more about it — I’m not entirely sure why it’s led you down this particular path. As a ridiculously obsessed poisoning nut myself, my toolbox is full to the brim with __proto__: nulls. Oddly it is @@species and similar “internal contract” stuff that has always presented the most elaborate challenges for poison-guarding for me, especially in contexts where a fresh realm is unobtainable, and this is even the main reason why I eventually found myself mainly on the Dark Side (anti-@@species) despite using it extensively for certain things (e.g. TypedArray subclasses).

* I cannot claim to know in most cases whether I’ve inferred motivations/rationales correctly, nor if, in some cases, there was a “motivation” at all. Object in many ways seems more happenstance than design, as you noted earlier.

** The object language type can act as a data structure, but the Object constructor has no specific relationship to this — it is equally true for any/all objects created by any/all other means.

*** Though personally, my favorite super constructor, esp now that we have private fields, is function (o) { return o; } 😈

**** Object.fromEntries followed what was already, at that point, an established pattern — no other Object statics are receiver-sensitive either, and Object.create doesn’t have a default first argument value of this.prototype.

@WebReflection
Copy link
Author

WebReflection commented Nov 18, 2021

  1. in JS, objects are rather everything, as any of them inherit from Object.prototype as last resort
  2. Object modes hasOwnProperty like compatible structure ... the gotcha there, is the Object.prototype, nobody can fix, apparently, in these days
  3. previous point proves your third point wrong
  4. the Object constructor is like any other for users ... both statics and prototype behavior is passed along ... nothing special, really
  5. that's a limitation we're discussing here ... why is that new MyArray() returns an instance of MyArray and new MyObject doesn't do the same? ... oh, wait, it does !!! So, when you say "Objects created with new Object are indistinguishable from objects which are not created this way", what actually are you talking about?
  6. you could've skipped this point as the Object.prototype is not what we're discussing here at all, but we all know what it does, as I have personally already written by examples
  7. this issue is not about Object.prototyep, in case you didn't notice ... it's actually about the fact Object.prototype is evrrywhere, and so is Object statics, when you extend it, but there's this single method that doesn't return the expected instance, and the whole implementation filed as Merge Request to all meaningful browasers would be shorter than this thread
  8. prototype again ... you lost me, sorry .., can we focus on what's the issue here? Thank you!

@ljharb
Copy link
Member

ljharb commented Nov 18, 2021

All of these issues aside, the only change that could likely be made would be to make it like Array.from - reading the receiver, but falling back to %Object% - but I'd be willing to bet that there's already code on the web that relies on Object.fromEntries NOT being sensitive to its receiver (not just array.map(Object.fromEntries), but someone subclassing Object that relies on MyObject.fromEntries() being a normal object instead of a subclass).

In order to figure that out one way or the other, a browser would have to be willing to invest the time and effort into gathering that data in the first place, which they've historically been very hesitant to do even about much broader proposals like globalThis.

In order for a browser to be willing to do that in the first place, the change would almost certainly first have to gain consensus, which would mean every delegate would have to deem the change neutral or desirable - and there's a number of delegates in this thread that have already indicated they would find it undesirable.

This would have been a great item to bring up during the proposal process itself - ideally before stage 3 - whether it changed the outcome or not, but at this point, I don't see what more there is to discuss.

@bathos
Copy link
Contributor

bathos commented Nov 18, 2021

Andrea, I like you very much. I hope you can see that my attempt at answering the question was intended in good faith even if the lack of complete agreement here has been frustrating. :)

@WebReflection
Copy link
Author

By example (or last resort):

class MyObject extends Object {}

(new MyObject) instanceof MyObject;
// true

(new MyObject({a: 123})) instanceof MyObject;
// true

MyObject.fromEntries([]) instanceof MyObject;
// false

There is no rationale there, and that's the bug I've filed to TC39.

Thanks for considering fixing that.

@zloirock
Copy link

@WebReflection anyway, TC39 wants to remove built-ins subclassing https://github.com/tc39/proposal-rm-builtin-subclassing.

Subclassing support already removed from new proposals like grouping or changing array by copy, so adding it here could be strange.

They don't care that remove built-ins subclassing will break half of the web since even now they discuss it and they don't understand what and where will be broken, despite the fact that I showed enough examples.

@WebReflection
Copy link
Author

WebReflection commented Nov 18, 2021

I'd be willing to bet that there's already code on the web that ...

this is the gotcha I've found around these topics ... I know @ljharb was strong about this current spec'd behavior, and yet these sentences are worth zero without data behind.

No, I bet the other way ... I like having JS being bet driven programming language though, but I think TC39 members are influencing too much their own bets in there ...

edit I am pretty sure when Brendan mentioned "always bet on JS" he didn't really mean this kind of development ...

@ljharb
Copy link
Member

ljharb commented Nov 18, 2021

@WebReflection and you might be right that it's a viable change! but even if it was unanimously desirable, which it is currently not, the obstacles I mentioned still exist.

@WebReflection
Copy link
Author

TC39 wants to remove built-ins subclassing

that was my TIL, and we had a laugh on twitter already ... JS is like "we can't break the Web from 90's" but full steam ahead when it comes to "we can drop recent APIs from 2015 on because YOLO" ... it's bad for the language, and bad for the standard, in terms of reliability, or "betting", for the next cool thing ... way too many cool things have been regretted in last 10 years, and so many bad things still there 'cause "we can't break the Web" ... sad progress, imho, but also not the right venue to discuss this.

I'll show myself out this thread, but thank you all for participating, it's been surely interesting

@bakkot
Copy link
Contributor

bakkot commented Nov 18, 2021

I don't think making it work the way you suggest would break anything. I just don't think it's a good idea, because it only makes sense if you think of Object as a class, rather than as a namespace, and I don't think you ought to think of Object as a class.

Separately: in my experience, the overwhelming majority of object-like things which need a different prototype than Object.prototype want null. If we're agreed that there's a need to use Object.fromEntries to create things with a different prototype, I'd certainly want it to support null, and this change does not do that.

@WebReflection
Copy link
Author

WebReflection commented Nov 18, 2021

@bakkot I am OK about null but I already explained my use case around objects ... instead of betting, if you could search for the amount of literals created objects references that ends up using ref.hasOwnProperty(key) for historical reason, and you realize that for a reason or another any-bloody-piece of JS could override Object.prototype.hasOwnProperty to intercept every single Symbol, key, whatever with a toString method on it, to disrupt an environment, or make 3rd party libraries unreliable, you "kinda" realize the best thing any code could do to ensure no root prototype pollution happened, is to early extend Object, set all descriptors from both the class, and its prototype as own, and freeze all the things, so that when your code does:

const myObj = new MyObj({...whatever});

if (myObj.hasOwnProperty(mySecret)) {
  // ... we really want to trust this execution
}

So that's it: TC39 can remove subclassing in the name of security, and contextually enable the most insecure environment ever, because everyone can pollute globals, and not just ASAP, but literally whenever they land/want to, so last come, last shenanigans happens.

This is not where JS should go, and I have no idea why TC39 is pushing so hard to make it happen by removing builtin extends, either on the specs or on the DOM (but that's another story).

@bathos
Copy link
Contributor

bathos commented Nov 18, 2021

@WebReflection I doubt complete @@species removal will end up happening. As zloirock said, it's crystal clear that it will wreak chaos and on a more meta level might be pretty devastating to the faith people have in the standard. But IIUC (maybe?), the folks investigating that are using that as a kind of "this would be ideal from our POV" starting point from which to discover where the line really is. Some of the more overwrought interior hooks might end up safe to cut, and there's no harm in exploring whether that's possible and if so, what that would look like.

In any case, I don't think it's the same as "removing builtin extends" - it concerns (potentially) removing complex "interior" contract stuff, but those hooks don't really achieve things that can't already be achieved by other means. Some of those hooks, e.g. the current RegExp flag getter behaviors, are arguably kinda silly.

I think it's notable tho that "Object.fromEntries shouldn't be receiver-sensitive" is kinda unrelated? For one, it never was receiver sensitive (nothing's going away), but more importantly, the reason it's not receiver-sensitive isn't related to objections to interior contract hook stuff . They just stem from a different conception of what these methods "are" (object utility functions sitting on a namespace object).

@bakkot
Copy link
Contributor

bakkot commented Nov 18, 2021

I'm very familiar with proto poisoning attacks, yes, and understand the goal of writing code which is defensive against them.

But if your goal is to replace Object with MyObject and Array with MyArray and have things continue to work, but with your classes instead, you're going to need to write your own fromEntries anyway: there is plenty of code which does

let { fromEntries } = Object;
let obj = fromEntries(whatever);

and even with the change you propose, switching out Object for MyObject is going to leave the last line creating a base Object rather than your custom thing. So I still do not entirely understand why this change would be helpful for the use case you describe.

I have no idea why TC39 is pushing so hard to make it happen by removing builtin extends

As the readme of that proposal says, the primary motivation is that extending builtins is extremely complex for engines, such that it has been one of the more common sources of browser exploits (not just application security issues) across all browsers since its introduction, without providing a level of benefit to users which is even remotely commensurate with that level of complexity and risk.

I am not at all confident it will prove possible to remove wholesale, but that's the motivation. And I'm hopeful we can at least get rid of some of the worst excesses, like subclassing typed arrays and RegExps.

@bathos
Copy link
Contributor

bathos commented Nov 18, 2021

hisses you shall never take my precious lexer/parser toolkit of TypedArray subclasses! ~ UTF8Array, SourceText, SourceFrament ...

(I hope. But I will take my own medicine if I have to :) - everything in my TypedArray menagerie already has to do some extra stuff to choose to return either the base type or the subclass since @@species actually isn't expressive enough anyway, e.g. returning Uint32Array for slice, but SourceText for subarray. Having to shadow subarray etc won't really end my world ...)

@WebReflection
Copy link
Author

WebReflection commented Nov 18, 2021

you're going to need to write your own fromEntries anyway

sure thing, and that's because the spec is broken? hence we're here!

why this change would be helpful for the use case you describe

because the issue is with fromEntries.call(MyObject, whatever) being effective, which is not the case (I can secure call itself too, so lets please don't nitpick there, thank you ;-))

the primary motivation is that extending builtins is extremely complex for engines, such that it has been one of the more common sources of browser exploits ...

I don't understand this sentence at all, because nobody in the industry extends builtins in general, except people, or library, that know what they are doing. When they do, is because they .. actually, likely, know, what they are doing.

extending builtins is extremely complex

And they know that too, right?

I'm hopeful we can at least get rid of some of the worst excesses, like subclassing typed arrays and RegExps.

and I am sure whoever is doing that has a reason to do so, and we securing RegExp have a reason too ... for gosh sake, make the most powerful string manipulation tool impossible to secure and goodbye JS ?!?

@bathos
Copy link
Contributor

bathos commented Nov 18, 2021

(I think the complexity/risk in question is that of the JS engine's implementation, not the code using the features.)

@bathos
Copy link
Contributor

bathos commented Nov 18, 2021

RegExp have a reason too ... for gosh sake, make the most powerful string manipulation tool impossible to secure and goodbye JS ?!?

RegExp is currently the most challenging intrinsic (well, apart from Promise) to make robust in an env where you cannot just freeze or grab from a "private" realm. The reason it's so challenging is the hooks that they're trying to remove, i.e. that would actually become much easier, not harder / impossible.

@WebReflection
Copy link
Author

WebReflection commented Nov 18, 2021

We're securing RegExp through a class extend with sealed/frozen methods and statics ... honestly folks, if you ever want to know how dirty the game could be, please join my team, otherwise, please never tell me again not extending builtins is a secure measurement, because that's really not what it is: JS is exploitable from Object.prototype up, not from extended and secured Class down ... I am not sure this is relevant anymore, but I really hope somebody is taking notes in TC39, as we fight daily against these kind of attacks, and we also have secured successfully to date many instances, and it just works. You dropping that? In the name of security? Please talk to us first, thank you!

@bakkot
Copy link
Contributor

bakkot commented Nov 18, 2021

because the issue is with fromEntries.call(MyObject, whatever) being effective

OK, so it sounds like it's not that you specifically want MyObject.fromEntries() to work, but rather that you want to use fromEntries to create something with a different prototype than Object? If you'd have to be calling it with .call or anyway, it seems like it wouldn't really matter if the mechanism is that fromEntries is this-sensitive or if there's a different method which takes a parameter for the prototype.

If so, you could suggest a proposal for, say, Object.createFromEntries on es-discourse and see if there's interest.

We're securing RegExp through a class extend

Browser exploits are an entirely different class of security vulnerability. Application-level security measures aren't going to do anything if the adversary is capable of getting an RCE in the browser itself, as has happened several times as a consequence of the complexity inherent in trying to make extending builtins work with all the machinery that is currently specified.

@zloirock
Copy link

And I'm hopeful we can at least get rid of some of the worst excesses, like subclassing typed arrays and RegExps.

How do you think to get rid of such worst excesses?

@WebReflection
Copy link
Author

you want to use fromEntries to create something with a different prototype than Object?

like Array.from would do, yes, it's like ... using this internally, instead of assuming that fromEntries should create only an instance of Object, and not an instance of this which, if undefined, is Object, like it is for Array.from, although nobody ever had surprises there, because it works as expected ... is that a huge change to propose, making it work as expected?

@ljharb
Copy link
Member

ljharb commented Nov 18, 2021

Since it’s not what everyone expects, and is different from what some expect? Yes, it’s a huge change.

@bakkot
Copy link
Contributor

bakkot commented Nov 18, 2021

It would not be a huge change in terms of the semantics of the language. The problem is that many people, myself included, do not share your expectation of how it would work.

@WebReflection
Copy link
Author

WebReflection commented Nov 18, 2021

Since it’s not what everyone expects, and is different from what some expect? Yes, it’s a huge change.

Accordingly with your thoughts, nobody does that (as in, Object shouldn't be a class extend and so on) ... so how is nobody doing that expecting anything, and how is that somebody doing that found it a surprise?

@WebReflection
Copy link
Author

anyway ... my thoughts right now:

emptiness on one side, real-world uses cases on the other
emptiness is fulfilled, real-world use cases are actually questioning the status quo

this is where I am here, or elsewhere, if "Object is special because" or "can't extend built-ins because" 😢

@bathos
Copy link
Contributor

bathos commented Nov 18, 2021

It might be good to return to the motivating problem a bit (as I understand it)? The merits of and objections to a specific potential solution are fascinating, but there are likely other angles entirely.

Bakkot suggested Object.createFromEntries(requiredPrototype, iterable) (guessing about the contract). Would either this or Object.fromEntries(iterable, optionalPrototype) potentially provide a solution to the original problem? It seems neither would have a perf downside and I suspect both of these angles would be more likely to gain traction than receiver-sensitivity.

@bakkot
Copy link
Contributor

bakkot commented Nov 18, 2021

Every time someone proposes an extension to any API it's because they have a use case for it. We're not going to add all possible such extensions. So we're always going to have to talk about whether the proposed design makes sense in the context of the rest of the language. That's not emptiness.

I recognize that you and I disagree about which design makes more sense here; there's never going to be an objectively right answer to that. But, as I said above, it also seems to me that there are other possible things which would give you what you want, such as a possible Object.createFromEntries, which would also have the advantage of supporting use cases like extending null. So I think you would likely have better luck pursuing other changes. But that said, you are still welcome to propose the change you're asking for here on es-discourse and try to get consensus, as with any other change. I hope you are convinced at least that not everyone agrees the current behavior is a bug, even though you disagree.

@WebReflection
Copy link
Author

WebReflection commented Nov 19, 2021

what I really expect is something as simple as this:

// take current fromEntries and setPrototypeOf
const {fromEntries, setPrototypeOf} = Object;
const apply = Function.call.bind(Function.apply);

// but actually use fromEntries with a better meaning
Object.fromEntries = function () {
  return setPrototypeOf(
    apply(fromEntries, this, arguments),
    this.prototype
  );
};

// ... so that ...
class Thing extends Object {}
Thing.fromEntries([]) instanceof Thing;
// true

what I do think it should happen, is that the instance is created and populated based on this like it is for Array.for or, hopefully, others, in the future.

Object.fromEntries = function fromEntries(entries) {
  'use strict';
  const ref = new (this || Object);
  for (const [key, value] of entries)
    ref[key] = value;
  return ref;
};

@ljharb
Copy link
Member

ljharb commented Nov 19, 2021

It'd have to use [[Define]] and not [[Set]], of course, or it'd break a bunch of otherwise robust code, but I'm also not sure why this change - which would likely make things slower for everyone who wasn't subclassing Object - is better than you adding this to your subclass:

static fromEntries(entries) {
  return new this(super.fromEntries(entries));
}

@WebReflection
Copy link
Author

WebReflection commented Nov 19, 2021

because it doesn't reason with the fact nobody needs to do that with Array extends ... it promotes inconsistent, unexpected, behavior, "nobody use" as you say, yet anyone using it would benefit from ... so here the thing: which part would be slower if the only thing internally you have to check is this ???

where is the slowness there? or why isn't Array.from and extends slower?

@WebReflection
Copy link
Author

P.S. I've already said super.anything can be poisoned, and the reason extending grants behaviors "here" ...

@ljharb
Copy link
Member

ljharb commented Nov 19, 2021

Sure, but if that’s a concern then you’d want with your own proposal to still do static fromEntries = Object.fromEntries on your subclass anyways, and as you demonstrated in an earlier snippet, you can cache Object.fromEntries too.

@WebReflection
Copy link
Author

need to check if new this is faster than setPrototypeOf(thing, this.prototype) but I wasn't really looking for a workaround rather a good reason to explain why is that this is needed. Anyway, we've discussed plenty. Have you all a nice weekend.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants