Bug 1214824 - Forbid CPOW usage if add-on declares it is multiprocessCompatible (r=mrbkap)
☠☠ backed out by 0a03bb6af604 ☠ ☠
authorBill McCloskey <billm@mozilla.com>
Tue, 07 Jun 2016 16:31:03 -0700
changeset 345435 14928a6b38f3d99912df076d13626b28fbaadac4
parent 345434 090e3a2d0f89a65e05f1fb8b372a4477be04ff3b
child 345436 13cd8e7c973a86d612e04819118d2ca1234059ef
push id1230
push userjlund@mozilla.com
push dateMon, 31 Oct 2016 18:13:35 +0000
treeherdermozilla-release@5e06e3766db2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmrbkap
bugs1214824
milestone50.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1214824 - Forbid CPOW usage if add-on declares it is multiprocessCompatible (r=mrbkap)
js/ipc/JavaScriptParent.cpp
js/xpconnect/idl/xpccomponents.idl
js/xpconnect/src/XPCComponents.cpp
js/xpconnect/src/XPCWrappedNativeScope.cpp
js/xpconnect/src/nsXPConnect.cpp
js/xpconnect/src/xpcprivate.h
js/xpconnect/src/xpcpublic.h
toolkit/components/telemetry/Histograms.json
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/xre/nsXREDirProvider.cpp
--- a/js/ipc/JavaScriptParent.cpp
+++ b/js/ipc/JavaScriptParent.cpp
@@ -58,45 +58,107 @@ ForbidUnsafeBrowserCPOWs()
     static bool cached = false;
     if (!cached) {
         cached = true;
         Preferences::AddBoolVarCache(&result, "dom.ipc.cpows.forbid-unsafe-from-browser", false);
     }
     return result;
 }
 
+// Should we allow CPOWs in aAddonId, even though it's marked as multiprocess
+// compatible? This is controlled by two prefs:
+//   If dom.ipc.cpows.forbid-cpows-in-compat-addons is false, then we allow the CPOW.
+//   If dom.ipc.cpows.forbid-cpows-in-compat-addons is true:
+//     We check if aAddonId is listed in dom.ipc.cpows.allow-cpows-in-compat-addons
+//     (which should be a comma-separated string). If it's present there, we allow
+//     the CPOW. Otherwise we forbid the CPOW.
+static bool
+ForbidCPOWsInCompatibleAddon(const nsACString& aAddonId)
+{
+    static bool forbid;
+    static nsCString allow;
+    static bool cached = false;
+    if (!cached) {
+        cached = true;
+        Preferences::AddBoolVarCache(&forbid, "dom.ipc.cpows.forbid-cpows-in-compat-addons", false);
+
+        allow.Assign(',');
+        allow.Append(Preferences::GetCString("dom.ipc.cpows.allow-cpows-in-compat-addons"));
+        allow.Append(',');
+    }
+
+    if (!forbid) {
+        return false;
+    }
+    nsCString searchString(",");
+    searchString.Append(aAddonId);
+    searchString.Append(',');
+    return allow.Find(searchString) == kNotFound;
+}
+
 bool
 JavaScriptParent::allowMessage(JSContext* cx)
 {
-    MessageChannel* channel = GetIPCChannel();
-    if (channel->IsInTransaction())
-        return true;
+    // If we're running browser code, then we allow all safe CPOWs and forbid
+    // unsafe CPOWs based on a pref (which defaults to forbidden). We also allow
+    // CPOWs unconditionally in selected globals (based on
+    // Cu.permitCPOWsInScope).
+    //
+    // If we're running add-on code, then we check if the add-on is multiprocess
+    // compatible (which eventually translates to a given setting of allowCPOWs
+    // on the scopw). If it's not compatible, then we allow the CPOW but
+    // warn. If it is marked as compatible, then we check the
+    // ForbidCPOWsInCompatibleAddon; see the comment there.
 
-    if (ForbidUnsafeBrowserCPOWs()) {
-        nsIGlobalObject* global = dom::GetIncumbentGlobal();
-        JSObject* jsGlobal = global ? global->GetGlobalJSObject() : nullptr;
-        if (jsGlobal) {
-            JSAutoCompartment ac(cx, jsGlobal);
-            if (!JS::AddonIdOfObject(jsGlobal) && !xpc::CompartmentPrivate::Get(jsGlobal)->allowCPOWs) {
+    MessageChannel* channel = GetIPCChannel();
+    bool isSafe = channel->IsInTransaction();
+
+    bool warn = !isSafe;
+    nsIGlobalObject* global = dom::GetIncumbentGlobal();
+    JSObject* jsGlobal = global ? global->GetGlobalJSObject() : nullptr;
+    if (jsGlobal) {
+        JSAutoCompartment ac(cx, jsGlobal);
+        JSAddonId* addonId = JS::AddonIdOfObject(jsGlobal);
+
+        if (!xpc::CompartmentPrivate::Get(jsGlobal)->allowCPOWs) {
+            if (!addonId && ForbidUnsafeBrowserCPOWs() && !isSafe) {
                 Telemetry::Accumulate(Telemetry::BROWSER_SHIM_USAGE_BLOCKED, 1);
                 JS_ReportError(cx, "unsafe CPOW usage forbidden");
                 return false;
             }
+
+            if (addonId) {
+                JSFlatString* flat = JS_ASSERT_STRING_IS_FLAT(JS::StringOfAddonId(addonId));
+                nsString addonIdString;
+                AssignJSFlatString(addonIdString, flat);
+                NS_ConvertUTF16toUTF8 addonIdCString(addonIdString);
+                Telemetry::Accumulate(Telemetry::ADDON_FORBIDDEN_CPOW_USAGE, addonIdCString);
+
+                if (ForbidCPOWsInCompatibleAddon(addonIdCString)) {
+                    JS_ReportError(cx, "CPOW usage forbidden in this add-on");
+                    return false;
+                }
+
+                warn = true;
+            }
         }
     }
 
+    if (!warn)
+        return true;
+
     static bool disableUnsafeCPOWWarnings = PR_GetEnv("DISABLE_UNSAFE_CPOW_WARNINGS");
     if (!disableUnsafeCPOWWarnings) {
         nsCOMPtr<nsIConsoleService> console(do_GetService(NS_CONSOLESERVICE_CONTRACTID));
         if (console && cx) {
             nsAutoString filename;
             uint32_t lineno = 0, column = 0;
             nsJSUtils::GetCallingLocation(cx, filename, &lineno, &column);
             nsCOMPtr<nsIScriptError> error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID));
-            error->Init(NS_LITERAL_STRING("unsafe CPOW usage"), filename,
+            error->Init(NS_LITERAL_STRING("unsafe/forbidden CPOW usage"), filename,
                         EmptyString(), lineno, column,
                         nsIScriptError::warningFlag, "chrome javascript");
             console->LogMessage(error);
         } else {
             NS_WARNING("Unsafe synchronous IPC message");
         }
     }
 
--- a/js/xpconnect/idl/xpccomponents.idl
+++ b/js/xpconnect/idl/xpccomponents.idl
@@ -674,16 +674,19 @@ interface nsIXPCComponents_Utils : nsISu
     void setAddonInterposition(in ACString addonId, in nsIAddonInterposition interposition);
 
     /*
      * Enables call interpositions from addon scopes to any functions in the scope of |target|.
      */
     [implicit_jscontext]
     void setAddonCallInterposition(in jsval target);
 
+    [implicit_jscontext]
+    void allowCPOWsInAddon(in ACString addonId, in bool allow);
+
     /*
      * Return a fractional number of milliseconds from process
      * startup, measured with a monotonic clock.
      */
     double now();
 };
 
 /**
--- a/js/xpconnect/src/XPCComponents.cpp
+++ b/js/xpconnect/src/XPCComponents.cpp
@@ -3354,16 +3354,30 @@ nsXPCComponents_Utils::SetAddonCallInter
     XPCWrappedNativeScope* xpcScope = ObjectScope(targetObj);
     NS_ENSURE_TRUE(xpcScope, NS_ERROR_INVALID_ARG);
 
     xpcScope->SetAddonCallInterposition();
     return NS_OK;
 }
 
 NS_IMETHODIMP
+nsXPCComponents_Utils::AllowCPOWsInAddon(const nsACString& addonIdStr,
+                                         bool allow,
+                                         JSContext* cx)
+{
+    JSAddonId* addonId = xpc::NewAddonId(cx, addonIdStr);
+    if (!addonId)
+        return NS_ERROR_FAILURE;
+    if (!XPCWrappedNativeScope::AllowCPOWsInAddon(cx, addonId, allow))
+        return NS_ERROR_FAILURE;
+
+    return NS_OK;
+}
+
+NS_IMETHODIMP
 nsXPCComponents_Utils::Now(double* aRetval)
 {
     bool isInconsistent = false;
     TimeStamp start = TimeStamp::ProcessCreation(isInconsistent);
     *aRetval = (TimeStamp::Now() - start).ToMilliseconds();
     return NS_OK;
 }
 
--- a/js/xpconnect/src/XPCWrappedNativeScope.cpp
+++ b/js/xpconnect/src/XPCWrappedNativeScope.cpp
@@ -21,18 +21,20 @@
 using namespace mozilla;
 using namespace xpc;
 using namespace JS;
 
 /***************************************************************************/
 
 XPCWrappedNativeScope* XPCWrappedNativeScope::gScopes = nullptr;
 XPCWrappedNativeScope* XPCWrappedNativeScope::gDyingScopes = nullptr;
+bool XPCWrappedNativeScope::gShutdownObserverInitialized = false;
 XPCWrappedNativeScope::InterpositionMap* XPCWrappedNativeScope::gInterpositionMap = nullptr;
 InterpositionWhitelistArray* XPCWrappedNativeScope::gInterpositionWhitelists = nullptr;
+XPCWrappedNativeScope::AddonSet* XPCWrappedNativeScope::gAllowCPOWAddonSet = nullptr;
 
 NS_IMPL_ISUPPORTS(XPCWrappedNativeScope::ClearInterpositionsObserver, nsIObserver)
 
 NS_IMETHODIMP
 XPCWrappedNativeScope::ClearInterpositionsObserver::Observe(nsISupports* subject,
                                                             const char* topic,
                                                             const char16_t* data)
 {
@@ -47,16 +49,21 @@ XPCWrappedNativeScope::ClearInterpositio
         gInterpositionMap = nullptr;
     }
 
     if (gInterpositionWhitelists) {
         delete gInterpositionWhitelists;
         gInterpositionWhitelists = nullptr;
     }
 
+    if (gAllowCPOWAddonSet) {
+        delete gAllowCPOWAddonSet;
+        gAllowCPOWAddonSet = nullptr;
+    }
+
     nsContentUtils::UnregisterShutdownObserver(this);
     return NS_OK;
 }
 
 static bool
 RemoteXULForbidsXBLScope(nsIPrincipal* aPrincipal, HandleObject aGlobal)
 {
   MOZ_ASSERT(aPrincipal);
@@ -151,16 +158,21 @@ XPCWrappedNativeScope::XPCWrappedNativeS
             "extensions.interposition.enabled", false);
           if (interpositionEnabled) {
             mInterposition = do_GetService("@mozilla.org/addons/default-addon-shims;1");
             MOZ_ASSERT(mInterposition);
             UpdateInterpositionWhitelist(cx, mInterposition);
           }
         }
     }
+
+    if (addonId) {
+        // We forbid CPOWs unless they're specifically allowed.
+        priv->allowCPOWs = gAllowCPOWAddonSet ? gAllowCPOWAddonSet->has(addonId) : false;
+    }
 }
 
 // static
 bool
 XPCWrappedNativeScope::IsDyingScope(XPCWrappedNativeScope* scope)
 {
     for (XPCWrappedNativeScope* cur = gDyingScopes; cur; cur = cur->mNext) {
         if (scope == cur)
@@ -699,30 +711,55 @@ XPCWrappedNativeScope::SetAddonInterposi
                                              JSAddonId* addonId,
                                              nsIAddonInterposition* interp)
 {
     if (!gInterpositionMap) {
         gInterpositionMap = new InterpositionMap();
         bool ok = gInterpositionMap->init();
         NS_ENSURE_TRUE(ok, false);
 
-        // Make sure to clear the map at shutdown.
-        // Note: this will take care of gInterpositionWhitelists too.
-        nsContentUtils::RegisterShutdownObserver(new ClearInterpositionsObserver());
+        if (!gShutdownObserverInitialized) {
+            gShutdownObserverInitialized = true;
+            nsContentUtils::RegisterShutdownObserver(new ClearInterpositionsObserver());
+        }
     }
     if (interp) {
         bool ok = gInterpositionMap->put(addonId, interp);
         NS_ENSURE_TRUE(ok, false);
         UpdateInterpositionWhitelist(cx, interp);
     } else {
         gInterpositionMap->remove(addonId);
     }
     return true;
 }
 
+/* static */ bool
+XPCWrappedNativeScope::AllowCPOWsInAddon(JSContext* cx,
+                                         JSAddonId* addonId,
+                                         bool allow)
+{
+    if (!gAllowCPOWAddonSet) {
+        gAllowCPOWAddonSet = new AddonSet();
+        bool ok = gAllowCPOWAddonSet->init();
+        NS_ENSURE_TRUE(ok, false);
+
+        if (!gShutdownObserverInitialized) {
+            gShutdownObserverInitialized = true;
+            nsContentUtils::RegisterShutdownObserver(new ClearInterpositionsObserver());
+        }
+    }
+    if (allow) {
+        bool ok = gAllowCPOWAddonSet->put(addonId);
+        NS_ENSURE_TRUE(ok, false);
+    } else {
+        gAllowCPOWAddonSet->remove(addonId);
+    }
+    return true;
+}
+
 nsCOMPtr<nsIAddonInterposition>
 XPCWrappedNativeScope::GetInterposition()
 {
     return mInterposition;
 }
 
 /* static */ InterpositionWhitelist*
 XPCWrappedNativeScope::GetInterpositionWhitelist(nsIAddonInterposition* interposition)
--- a/js/xpconnect/src/nsXPConnect.cpp
+++ b/js/xpconnect/src/nsXPConnect.cpp
@@ -1282,16 +1282,30 @@ SetAddonInterposition(const nsACString& 
     AutoJSAPI jsapi;
     jsapi.Init(xpc::PrivilegedJunkScope());
     addonId = NewAddonId(jsapi.cx(), addonIdStr);
     if (!addonId)
         return false;
     return XPCWrappedNativeScope::SetAddonInterposition(jsapi.cx(), addonId, interposition);
 }
 
+bool
+AllowCPOWsInAddon(const nsACString& addonIdStr, bool allow)
+{
+    JSAddonId* addonId;
+    // We enter the junk scope just to allocate a string, which actually will go
+    // in the system zone.
+    AutoJSAPI jsapi;
+    jsapi.Init(xpc::PrivilegedJunkScope());
+    addonId = NewAddonId(jsapi.cx(), addonIdStr);
+    if (!addonId)
+        return false;
+    return XPCWrappedNativeScope::AllowCPOWsInAddon(jsapi.cx(), addonId, allow);
+}
+
 } // namespace xpc
 
 namespace mozilla {
 namespace dom {
 
 bool
 IsChromeOrXBL(JSContext* cx, JSObject* /* unused */)
 {
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -992,16 +992,20 @@ public:
         }
     }
 
     typedef js::HashMap<JSAddonId*,
                         nsCOMPtr<nsIAddonInterposition>,
                         js::PointerHasher<JSAddonId*, 3>,
                         js::SystemAllocPolicy> InterpositionMap;
 
+    typedef js::HashSet<JSAddonId*,
+                        js::PointerHasher<JSAddonId*, 3>,
+                        js::SystemAllocPolicy> AddonSet;
+
     // Gets the appropriate scope object for XBL in this scope. The context
     // must be same-compartment with the global upon entering, and the scope
     // object is wrapped into the compartment of the global.
     JSObject* EnsureContentXBLScope(JSContext* cx);
 
     JSObject* EnsureAddonScope(JSContext* cx, JSAddonId* addonId);
 
     XPCWrappedNativeScope(JSContext* cx, JS::HandleObject aGlobal);
@@ -1023,16 +1027,18 @@ public:
 
     static InterpositionWhitelist* GetInterpositionWhitelist(nsIAddonInterposition* interposition);
     static bool UpdateInterpositionWhitelist(JSContext* cx,
                                              nsIAddonInterposition* interposition);
 
     void SetAddonCallInterposition() { mHasCallInterpositions = true; }
     bool HasCallInterposition() { return mHasCallInterpositions; };
 
+    static bool AllowCPOWsInAddon(JSContext* cx, JSAddonId* addonId, bool allow);
+
 protected:
     virtual ~XPCWrappedNativeScope();
 
     XPCWrappedNativeScope(); // not implemented
 
 private:
     class ClearInterpositionsObserver final : public nsIObserver {
         ~ClearInterpositionsObserver() {}
@@ -1040,17 +1046,19 @@ private:
       public:
         NS_DECL_ISUPPORTS
         NS_DECL_NSIOBSERVER
     };
 
     static XPCWrappedNativeScope* gScopes;
     static XPCWrappedNativeScope* gDyingScopes;
 
+    static bool                      gShutdownObserverInitialized;
     static InterpositionMap*         gInterpositionMap;
+    static AddonSet*                 gAllowCPOWAddonSet;
 
     static InterpositionWhitelistArray* gInterpositionWhitelists;
 
     XPCJSRuntime*                    mRuntime;
     Native2WrappedNativeMap*         mWrappedNativeMap;
     ClassInfo2WrappedNativeProtoMap* mWrappedNativeProtoMap;
     RefPtr<nsXPCComponentsBase>    mComponents;
     XPCWrappedNativeScope*           mNext;
--- a/js/xpconnect/src/xpcpublic.h
+++ b/js/xpconnect/src/xpcpublic.h
@@ -499,16 +499,19 @@ ShouldDiscardSystemSource();
 
 bool
 SharedMemoryEnabled();
 
 bool
 SetAddonInterposition(const nsACString& addonId, nsIAddonInterposition* interposition);
 
 bool
+AllowCPOWsInAddon(const nsACString& addonId, bool allow);
+
+bool
 ExtraWarningsForSystemJS();
 
 class ErrorReport {
   public:
     NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ErrorReport);
 
     ErrorReport() : mWindowID(0)
                   , mLineNumber(0)
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -37,16 +37,24 @@
   },
   "ADDON_SHIM_USAGE": {
     "expires_in_version": "never",
     "kind": "enumerated",
     "n_values": 15,
     "keyed": true,
     "description": "Reasons why add-on shims were used, keyed by add-on ID."
   },
+  "ADDON_FORBIDDEN_CPOW_USAGE": {
+    "expires_in_version": "never",
+    "kind": "count",
+    "keyed": true,
+    "description": "Counts the number of times a given add-on used CPOWs when it was marked as e10s compatible.",
+    "bug_numbers": [1214824],
+    "alert_emails": ["wmccloskey@mozilla.com"]
+  },
   "BROWSER_SHIM_USAGE_BLOCKED": {
     "expires_in_version": "never",
     "kind": "count",
     "description": "Counts the number of times a CPOW shim was blocked from being created by browser code.",
     "releaseChannelCollection": "opt-out",
     "bug_numbers": [1245901],
     "alert_emails": ["benjamin@smedbergs.us"]
   },
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -4601,16 +4601,17 @@ this.XPIProvider = {
     logger.debug("Loading bootstrap scope from " + aFile.path);
 
     let principal = Cc["@mozilla.org/systemprincipal;1"].
                     createInstance(Ci.nsIPrincipal);
     if (!aMultiprocessCompatible && Preferences.get(PREF_INTERPOSITION_ENABLED, false)) {
       let interposition = Cc["@mozilla.org/addons/multiprocess-shims;1"].
         getService(Ci.nsIAddonInterposition);
       Cu.setAddonInterposition(aId, interposition);
+      Cu.allowCPOWsInAddon(aId, true);
     }
 
     if (!aFile.exists()) {
       activeAddon.bootstrapScope =
         new Cu.Sandbox(principal, { sandboxName: aFile.path,
                                     wantGlobalProperties: ["indexedDB"],
                                     addonId: aId,
                                     metadata: { addonID: aId } });
@@ -4677,16 +4678,17 @@ this.XPIProvider = {
    *
    * @param  aId
    *         The add-on's ID
    */
   unloadBootstrapScope: function(aId) {
     // In case the add-on was not multiprocess-compatible, deregister
     // any interpositions for it.
     Cu.setAddonInterposition(aId, null);
+    Cu.allowCPOWsInAddon(aId, false);
 
     this.activeAddons.delete(aId);
     delete this.bootstrappedAddons[aId];
     this.persistBootstrappedAddons();
     this.addAddonsToCrashReporter();
 
     // Only access BrowserToolboxProcess if ToolboxProcess.jsm has been
     // initialized as otherwise, there won't be any addon globals added to it
--- a/toolkit/xre/nsXREDirProvider.cpp
+++ b/toolkit/xre/nsXREDirProvider.cpp
@@ -664,16 +664,19 @@ RegisterExtensionInterpositions(nsINIPar
 
     nsAutoCString addonId;
     rv = parser.GetString("MultiprocessIncompatibleExtensions", buf.get(), addonId);
     if (NS_FAILED(rv))
       return;
 
     if (!xpc::SetAddonInterposition(addonId, interposition))
       continue;
+
+    if (!xpc::AllowCPOWsInAddon(addonId, true))
+      continue;
   }
   while (true);
 }
 
 static void
 LoadExtensionDirectories(nsINIParser &parser,
                          const char *aSection,
                          nsCOMArray<nsIFile> &aDirectories,