Bug 1437864 - Save a copy of the Error and Promise globals from extension context before they can be redefined. draft
authorLuca Greco <lgreco@mozilla.com>
Mon, 20 Aug 2018 19:59:38 +0200
changeset 830258 00ccd3de6c194ab70261ecfe1b7475b05a888da3
parent 830115 1462e2ed11cb16935d9ce76c54dfe2d365bd30a8
child 830259 29777e6e0e77c2fa023e8fee07ef8519bf298e17
push id118830
push userluca.greco@alcacoop.it
push dateTue, 21 Aug 2018 11:07:53 +0000
bugs1437864
milestone63.0a1
Bug 1437864 - Save a copy of the Error and Promise globals from extension context before they can be redefined. MozReview-Commit-ID: ALNJDiBwZL7
toolkit/components/extensions/ExtensionCommon.jsm
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/child/ext-test.js
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -371,16 +371,35 @@ class BaseContext {
     this.extension = extension;
     this.jsonSandbox = null;
     this.active = true;
     this.incognito = null;
     this.messageManager = null;
     this.docShell = null;
     this.contentWindow = null;
     this.innerWindowID = 0;
+
+    // This two properties are assigned in ContentScriptContextChild subclass
+    // to keep a copy of the content script sandbox Error and Promise globals
+    // (which are used by the WebExtensions internals) before any extension
+    // content script code had any chance to redefine them.
+    this.cloneScopeError = null;
+    this.cloneScopePromise = null;
+  }
+
+  get Error() {
+    // Return the copy stored in the context instance (when the context is an instance of
+    // ContentScriptContextChild or the global from extension page window otherwise).
+    return this.cloneScopeError || this.cloneScope.Error;
+  }
+
+  get Promise() {
+    // Return the copy stored in the context instance (when the context is an instance of
+    // ContentScriptContextChild or the global from extension page window otherwise).
+    return this.cloneScopePromise || this.cloneScope.Promise;
   }
 
   setContentWindow(contentWindow) {
     let {document, docShell} = contentWindow;
 
     this.innerWindowID = getInnerWindowID(contentWindow);
     this.messageManager = docShell.messageManager;
 
@@ -554,17 +573,17 @@ class BaseContext {
    * clone scope, it is reported, and converted to an unexpected
    * exception error.
    *
    * @param {Error|object} error
    * @param {SavedFrame?} [caller]
    * @returns {Error}
    */
   normalizeError(error, caller) {
-    if (error instanceof this.cloneScope.Error) {
+    if (error instanceof this.Error) {
       return error;
     }
     let message, fileName;
     if (error && typeof error === "object") {
       const isPlain = ChromeUtils.getClassName(error) === "Object";
       if (isPlain && error.mozWebExtLocation) {
         caller = error.mozWebExtLocation;
       }
@@ -580,17 +599,17 @@ class BaseContext {
         fileName = error.fileName;
       }
     }
 
     if (!message) {
       Cu.reportError(error);
       message = "An unexpected error occurred";
     }
-    return new this.cloneScope.Error(message, fileName);
+    return new this.Error(message, fileName);
   }
 
   /**
    * Sets the value of `.lastError` to `error`, calls the given
    * callback, and reports an error if the value has not been checked
    * when the callback returns.
    *
    * @param {object} error An object with a `message` property. May
@@ -680,17 +699,17 @@ class BaseContext {
               Cu.reportError(`Promise rejected while context is inactive\n`,
                              caller);
             } else {
               this.applySafeWithoutClone(callback, [], caller);
             }
           });
         });
     } else {
-      return new this.cloneScope.Promise((resolve, reject) => {
+      return new this.Promise((resolve, reject) => {
         promise.then(
           value => {
             if (this.unloaded) {
               Cu.reportError(`Promise resolved after context unloaded\n`,
                              caller);
             } else if (!this.active) {
               Cu.reportError(`Promise resolved while context is inactive\n`,
                              caller);
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -667,16 +667,22 @@ class ContentScriptContextChild extends 
         sameZoneAs: contentWindow,
         wantXrays: true,
         isWebExtensionContentScript: true,
         wantExportHelpers: true,
         wantGlobalProperties: ["XMLHttpRequest", "fetch"],
         originAttributes: attrs,
       });
 
+      // Preserve a copy of the original Error and Promise globals from the sandbox object,
+      // which are used in the WebExtensions internals (before any content script code had
+      // any chance to redefine them).
+      this.cloneScopePromise = this.sandbox.Promise;
+      this.cloneScopeError = this.sandbox.Error;
+
       // Preserve a copy of the original window's XMLHttpRequest and fetch
       // in a content object (fetch is manually binded to the window
       // to prevent it from raising a TypeError because content object is not
       // a real window).
       Cu.evalInSandbox(`
         this.content = {
           XMLHttpRequest: window.XMLHttpRequest,
           fetch: window.fetch.bind(window),
--- a/toolkit/components/extensions/child/ext-test.js
+++ b/toolkit/components/extensions/child/ext-test.js
@@ -70,17 +70,17 @@ const toSource = value => {
   }
 };
 
 this.test = class extends ExtensionAPI {
   getAPI(context) {
     const {extension} = context;
 
     function getStack() {
-      return new context.cloneScope.Error().stack.replace(/^/gm, "    ");
+      return new context.Error().stack.replace(/^/gm, "    ");
     }
 
     function assertTrue(value, msg) {
       extension.emit("test-result", Boolean(value), String(msg), getStack());
     }
 
     class TestEventManager extends EventManager {
       addListener(callback, ...args) {