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

IE11 - Out of Stack Space - Setting Symbol property on global prototype objects #735

Closed
ecammit opened this issue Dec 19, 2019 · 4 comments

Comments

@ecammit
Copy link

ecammit commented Dec 19, 2019

Versions: v3.3.6 through latest(v3.6.0)
(Issue likely occurs before v3.3.6, but that's the earliest core-js I tested.)

Code to reproduce (transpiled using babel 7.6.0 with webpack 4.41.0):

import "core-js";
const sym = Symbol("mySymbol");

// Each line below causes an "Out of stack space" error in IE11 (in Win 10; didn't test Win 7-8.1):
Node[sym] = true;
Window[sym] = true;
Element[sym] = true;

// The prototype of each causes "Out of stack space".
Node.prototype[sym] = true; // Is actually an instance of hidden type NodePrototype

// This does not error because the prototype of NodePrototype is Object:
Object.getPrototypeOf(Node.prototype)[sym] = true;

// Setting a string key on same objects works fine:
Node["foo"] = true;
Node.prototype["foo"] = true;

// Regular symbol assignment on other core objects work fine:
window[sym] = true;
window.document[sym] = true;

All browsers with a native Symbol implementation (Edge, Firefox, Chrome, Safari, etc.) support setting a symbol on Node, Window, Element, etc. So this is limited to the polyfill.

Error seems to be caused by infinite recursion in core-js of:

var setter = function (value) {
      if (this === ObjectPrototype) setter.call(ObjectPrototypeSymbols, value);
      if (has(this, HIDDEN) && has(this[HIDDEN], tag)) this[HIDDEN][tag] = false;
----->setSymbolDescriptor(this, tag, createPropertyDescriptor(1, value));
    };

The setSymbolDescriptor resolves to: function defineProperty() { [native code] }, so it's not being overridden by anything else. It's the native method. Calling setSymbolDescriptor is calling setter again, causing the infinite recursion and filling the stack.

Webpack config for babel:

        module: { 
                rules: [{ 
                        test: /\.js$/, 
                        use: { 
                                loader: 'babel-loader', 
                                options: { 
                                        presets: [ 
                                                ['@babel/preset-env', { 
                                                        'useBuiltIns': 'entry', 
                                                        'corejs': { 'version': 3, 'proposals': false }, 
                                                        'debug': false 
                                                }] 
 
                                        ]
                                } 
                        } 
                }

.browserlistrc:

> 0.05%
last 2 versions
IE >= 11
not IE < 11
Firefox ESR

package.json

  "devDependencies": {
    "@babel/core": "^7.6.0",
    "@babel/preset-env": "^7.6.0",
    "babel-loader": "^8.0.6",
    "webpack": "^4.41.0",
  },
  "dependencies": {
    "@babel/polyfill": "^7.6.0",
    "@babel/runtime": "^7.6.0",
    "@babel/runtime-corejs3": "^7.6.0",
    "core-js": "^3.6.0",
  },

If you need any further info, I stand ready to provide.

@ecammit
Copy link
Author

ecammit commented Dec 19, 2019

More about the issue:

Apparently in IE11, if you define a property with a setter on a parent, that parent's setter is called when defineProperty(,,{value: ...}) is called on an object that inherits from that parent. No other modern browsers seem to do this:

Object.defineProperty(Object.prototype, "foo", {configurable: true, enumerable: false, set: function() {console.log("Executed Object.prototype's setter");}});

Object.defineProperty(Node, "foo", {configurable: true, enumerable: false, value: true});
// Outputs in IE11: "Executed Object.prototype's setter"

So the issue with the Symbol polyfill is that this setter is added to Object.prototype:

var setter = function (value) {
      if (this === ObjectPrototype) setter.call(ObjectPrototypeSymbols, value);
      if (has(this, HIDDEN) && has(this[HIDDEN], tag)) this[HIDDEN][tag] = false;
----->setSymbolDescriptor(this, tag, createPropertyDescriptor(1, value));
    };

Since this is not an ObjectPrototype, but a class that inherits from ObjectPrototype, it gets past the first if(). Then when it calls setSymbolDescriptor, that is calling defineProperty(,,{value: }), which is also executing the ObjectPrototype's setter again and causing the loop.

@ecammit
Copy link
Author

ecammit commented Dec 19, 2019

Furthermore ie11 has a problem in that it essentially ignores a defineProperty(,,value: ) call on a child if the parent has a setter already defined on the parent:

Object.defineProperty(Object.prototype, "foo", {configurable: true, enumerable: false, set: function() {console.log("Executed Object.prototype's setter");}});

Object.defineProperty(Node, "foo", {configurable: true, enumerable: false, value: true});
// Outputs in IE11: "Executed Object.prototype's setter"

Object.getOwnPropertyDescriptor(Node, "foo");
// Outputs in IE11: undefined

However, if you define a setter on the child, it will overwrite the parent and not call the parent either:

var val;
Object.defineProperty(Node, "foo", {configurable: true, enumerable: false, set: function(v) {val = v;}, get: function() {return val;});
// Does not output "Executed Object.prototype's setter"

Object.getOwnPropertyDescriptor(Node, "foo");
// Outputs proper descriptor object

Node["foo"] = true;
// Does not output "Executed Object.prototype's setter".

So one potential fix is to change the setter on ObjectPrototype to assign a setter and getter rather than a value:

var setter = function (value) {
      if (this === ObjectPrototype) setter.call(ObjectPrototypeSymbols, value);
      if (has(this, HIDDEN) && has(this[HIDDEN], tag)) this[HIDDEN][tag] = false;
      let valueBuffer = value;
      setSymbolDescriptor(this, tag, {
            configurable: true,
            enumerable: false,
           set: function(v) {
                 valueBuffer = v;
            },
            get: function() {
                  return valueBuffer;
            }
      });
};

Of course there my be other implications of this to which I'm not aware, but I'll put in a pull request for this.

@zloirock
Copy link
Owner

zloirock commented Dec 19, 2019

It's a known and documented issue. We can't add workarounds for all possible exotic environment objects. Usage setters and getters here will break too many use cases.

@zloirock
Copy link
Owner

zloirock commented Dec 20, 2019

BTW why you need 3 copies of core-js in your direct dependencies?

  "dependencies": {
    "@babel/polyfill": "^7.6.0", // <- core-js@2 wrapper
    "@babel/runtime": "^7.6.0",
    "@babel/runtime-corejs3": "^7.6.0", // <- core-js-pure@3 wrapper
    "core-js": "^3.6.0", // <- core-js@3
  },

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

No branches or pull requests

2 participants