Bug 1469072 - Add infrastructure to move Activity Stream into its own content process. draft
authorimjching <jlim@mozilla.com>
Wed, 20 Jun 2018 14:04:51 -0400
changeset 811548 623abdaf7b2c765f4e3bfcb40f3a7cf75f448217
parent 811302 9c7bb8874337c2d40aef3d9945b10490a5115188
push id114341
push userbmo:jlim@mozilla.com
push dateWed, 27 Jun 2018 19:43:40 +0000
bugs1469072
milestone63.0a1
Bug 1469072 - Add infrastructure to move Activity Stream into its own content process. Summary: This patch adds the infrastructure to move Activity Stream (about:newtab, about:home, and about:welcome) into its own special content process - the privileged content process. This feature of running Activity Stream in the privileged content process is disabled by default. (See "browser.tabs.remote.separatePrivilegedContentProcess" preference.) We can deal with other about: pages in a follow-up. Reviewers: mconley Tags: #secure-revision Bug #: 1469072 Differential Revision: https://phabricator.services.mozilla.com/D1731 MozReview-Commit-ID: 5gIrP4LxcIt
browser/base/content/test/tabs/browser.ini
browser/base/content/test/tabs/browser_new_tab_in_privileged_process_pref.js
dom/ipc/ContentChild.cpp
dom/ipc/ContentParent.cpp
dom/ipc/ContentParent.h
js/xpconnect/loader/ScriptPreloader.cpp
js/xpconnect/loader/ScriptPreloader.h
js/xpconnect/loader/script_cache.py
modules/libpref/init/all.js
toolkit/modules/E10SUtils.jsm
--- a/browser/base/content/test/tabs/browser.ini
+++ b/browser/base/content/test/tabs/browser.ini
@@ -26,16 +26,18 @@ support-files =
 [browser_multiselect_tabs_using_Ctrl.js]
 [browser_multiselect_tabs_using_Shift.js]
 [browser_navigatePinnedTab.js]
 [browser_new_file_whitelisted_http_tab.js]
 skip-if = !e10s # Test only relevant for e10s.
 [browser_new_tab_insert_position.js]
 skip-if = (debug && os == 'linux' && bits == 32) #Bug 1455882, disabled on Linux32 for almost permafailing
 support-files = file_new_tab_page.html
+[browser_new_tab_in_privileged_process_pref.js]
+skip-if = !e10s # Pref and test only relevant for e10s.
 [browser_new_web_tab_in_file_process_pref.js]
 skip-if = !e10s # Pref and test only relevant for e10s.
 [browser_newwindow_tabstrip_overflow.js]
 [browser_open_newtab_start_observer_notification.js]
 [browser_opened_file_tab_navigated_to_web.js]
 [browser_overflowScroll.js]
 [browser_pinnedTabs_clickOpen.js]
 [browser_pinnedTabs_closeByKeyboard.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_new_tab_in_privileged_process_pref.js
@@ -0,0 +1,212 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Tests to ensure that Activity Stream loads in the privileged content process.
+ * Normal http web pages should load in the web content process.
+ * Ref: Bug 1469072.
+ */
+
+const ABOUT_BLANK = "about:blank";
+const ABOUT_HOME = "about:home";
+const ABOUT_NEWTAB = "about:newtab";
+const ABOUT_WELCOME = "about:welcome";
+const TEST_HTTP = "http://example.org/";
+
+/**
+ * Takes a xul:browser and makes sure that the remoteTypes for the browser in
+ * both the parent and the child processes are the same.
+ *
+ * @param {xul:browser} browser
+ *        A xul:browser.
+ * @param {string} expectedRemoteType
+ *        The expected remoteType value for the browser in both the parent
+ *        and child processes.
+ * @param {optional string} message
+ *        If provided, shows this string as the message when remoteType values
+ *        do not match. If not present, it uses the default message defined
+ *        in the function parameters.
+ */
+async function checkBrowserRemoteType(
+  browser,
+  expectedRemoteType,
+  message = `Ensures that tab runs in the ${expectedRemoteType} content process.`
+) {
+  // Check both parent and child to ensure that they have the correct remoteType.
+  is(browser.remoteType, expectedRemoteType, message);
+  is(browser.messageManager.remoteType, expectedRemoteType,
+    "Parent and child process should agree on the remote type.");
+}
+
+add_task(async function setup() {
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      ["browser.newtab.preload", false],
+      ["browser.tabs.remote.separatePrivilegedContentProcess", true],
+      ["dom.ipc.processCount.privileged", 1],
+      ["dom.ipc.keepProcessesAlive.privileged", 1],
+    ]
+  });
+});
+
+/*
+ * Test to ensure that the Activity Stream tabs open in privileged content
+ * process. We will first open an about:newtab page that acts as a reference to
+ * the privileged content process. With the reference, we can then open Activity
+ * Stream links in a new tab and ensure that the new tab opens in the same
+ * privileged content process as our reference.
+ */
+add_task(async function activity_stream_in_privileged_content_process() {
+  Services.ppmm.releaseCachedProcesses();
+
+  await BrowserTestUtils.withNewTab(ABOUT_NEWTAB, async function(browser1) {
+    await checkBrowserRemoteType(browser1, E10SUtils.PRIVILEGED_REMOTE_TYPE);
+
+    // Note the processID for about:newtab for comparison later.
+    let privilegedPid = browser1.frameLoader.tabParent.osPid;
+
+    for (let url of [
+      ABOUT_NEWTAB,
+      ABOUT_WELCOME,
+      ABOUT_HOME,
+      `${ABOUT_NEWTAB}#foo`,
+      `${ABOUT_WELCOME}#bar`,
+      `${ABOUT_HOME}#baz`,
+      `${ABOUT_NEWTAB}?q=foo`,
+      `${ABOUT_WELCOME}?q=bar`,
+      `${ABOUT_HOME}?q=baz`
+    ]) {
+      await BrowserTestUtils.withNewTab(url, async function(browser2) {
+        is(browser2.frameLoader.tabParent.osPid, privilegedPid,
+          "Check that about:newtab tabs are in the same privileged content process.");
+      });
+    }
+  });
+
+  Services.ppmm.releaseCachedProcesses();
+});
+
+/*
+ * Test to ensure that a process switch occurs when navigating between normal
+ * web pages and Activity Stream pages in the same tab.
+ */
+add_task(async function process_switching_through_loading_in_the_same_tab() {
+  Services.ppmm.releaseCachedProcesses();
+
+  await BrowserTestUtils.withNewTab(TEST_HTTP, async function(browser) {
+    await checkBrowserRemoteType(browser, E10SUtils.WEB_REMOTE_TYPE);
+
+    for (let [url, remoteType] of [
+      [ABOUT_NEWTAB, E10SUtils.PRIVILEGED_REMOTE_TYPE],
+      [ABOUT_BLANK, E10SUtils.PRIVILEGED_REMOTE_TYPE],
+      [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE],
+      [ABOUT_HOME, E10SUtils.PRIVILEGED_REMOTE_TYPE],
+      [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE],
+      [ABOUT_WELCOME, E10SUtils.PRIVILEGED_REMOTE_TYPE],
+      [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE],
+      [ABOUT_BLANK, E10SUtils.WEB_REMOTE_TYPE],
+      [`${ABOUT_NEWTAB}#foo`, E10SUtils.PRIVILEGED_REMOTE_TYPE],
+      [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE],
+      [`${ABOUT_WELCOME}#bar`, E10SUtils.PRIVILEGED_REMOTE_TYPE],
+      [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE],
+      [`${ABOUT_HOME}#baz`, E10SUtils.PRIVILEGED_REMOTE_TYPE],
+      [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE],
+      [`${ABOUT_NEWTAB}?q=foo`, E10SUtils.PRIVILEGED_REMOTE_TYPE],
+      [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE],
+      [`${ABOUT_WELCOME}?q=bar`, E10SUtils.PRIVILEGED_REMOTE_TYPE],
+      [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE],
+      [`${ABOUT_HOME}?q=baz`, E10SUtils.PRIVILEGED_REMOTE_TYPE],
+      [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE]
+    ]) {
+      BrowserTestUtils.loadURI(browser, url);
+      await BrowserTestUtils.browserLoaded(browser, false, url);
+      await checkBrowserRemoteType(browser, remoteType);
+    }
+  });
+
+  Services.ppmm.releaseCachedProcesses();
+});
+
+/*
+ * Test to ensure that a process switch occurs when navigating between normal
+ * web pages and Activity Stream pages using the browser's navigation features
+ * such as history and location change.
+ */
+add_task(async function process_switching_through_navigation_features() {
+  Services.ppmm.releaseCachedProcesses();
+
+  await BrowserTestUtils.withNewTab(ABOUT_NEWTAB, async function(browser) {
+    await checkBrowserRemoteType(browser, E10SUtils.PRIVILEGED_REMOTE_TYPE);
+
+    // Note the processID for about:newtab for comparison later.
+    let privilegedPid = browser.frameLoader.tabParent.osPid;
+
+    // Check that about:newtab opened from JS in about:newtab page is in the same process.
+    let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, ABOUT_NEWTAB, true);
+    await ContentTask.spawn(browser, ABOUT_NEWTAB, uri => {
+      content.open(uri, "_blank");
+    });
+    let newTab = await promiseTabOpened;
+    registerCleanupFunction(async function() {
+      BrowserTestUtils.removeTab(newTab);
+    });
+    browser = newTab.linkedBrowser;
+    is(browser.frameLoader.tabParent.osPid, privilegedPid,
+      "Check that new tab opened from about:newtab is loaded in privileged content process.");
+
+    // Check that reload does not break the privileged content process affinity.
+    BrowserReload();
+    await BrowserTestUtils.browserLoaded(browser, false, ABOUT_NEWTAB);
+    is(browser.frameLoader.tabParent.osPid, privilegedPid,
+      "Check that about:newtab is still in privileged content process after reload.");
+
+    // Load http webpage
+    BrowserTestUtils.loadURI(browser, TEST_HTTP);
+    await BrowserTestUtils.browserLoaded(browser, false, TEST_HTTP);
+    await checkBrowserRemoteType(browser, E10SUtils.WEB_REMOTE_TYPE);
+
+    // Check that using the history back feature switches back to privileged content process.
+    let promiseLocation = BrowserTestUtils.waitForLocationChange(gBrowser, ABOUT_NEWTAB);
+    browser.goBack();
+    await promiseLocation;
+    // We will need to ensure that the process flip has fully completed so that
+    // the navigation history data will be available when we do browser.goForward();
+    await BrowserTestUtils.waitForEvent(newTab, "SSTabRestored");
+    is(browser.frameLoader.tabParent.osPid, privilegedPid,
+      "Check that about:newtab is still in privileged content process after history goBack.");
+
+    // Check that using the history forward feature switches back to the web content process.
+    promiseLocation = BrowserTestUtils.waitForLocationChange(gBrowser, TEST_HTTP);
+    browser.goForward();
+    await promiseLocation;
+    // We will need to ensure that the process flip has fully completed so that
+    // the navigation history data will be available when we do browser.gotoIndex(0);
+    await BrowserTestUtils.waitForEvent(newTab, "SSTabRestored");
+    await checkBrowserRemoteType(browser, E10SUtils.WEB_REMOTE_TYPE,
+      "Check that tab runs in the web content process after using history goForward.");
+
+    // Check that goto history index does not break the affinity.
+    promiseLocation = BrowserTestUtils.waitForLocationChange(gBrowser, ABOUT_NEWTAB);
+    browser.gotoIndex(0);
+    await promiseLocation;
+    is(browser.frameLoader.tabParent.osPid, privilegedPid,
+      "Check that about:newtab is in privileged content process after history gotoIndex.");
+
+    BrowserTestUtils.loadURI(browser, TEST_HTTP);
+    await BrowserTestUtils.browserLoaded(browser, false, TEST_HTTP);
+    await checkBrowserRemoteType(browser, E10SUtils.WEB_REMOTE_TYPE);
+
+    // Check that location change causes a change in process type as well.
+    await ContentTask.spawn(browser, ABOUT_NEWTAB, uri => {
+      content.location = uri;
+    });
+    await BrowserTestUtils.browserLoaded(browser, false, ABOUT_NEWTAB);
+    is(browser.frameLoader.tabParent.osPid, privilegedPid,
+      "Check that about:newtab is in privileged content process after location change.");
+  });
+
+  Services.ppmm.releaseCachedProcesses();
+});
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -2741,16 +2741,18 @@ ContentChild::RecvRemoteType(const nsStr
   mRemoteType.Assign(aRemoteType);
 
   // For non-default ("web") types, update the process name so about:memory's
   // process names are more obvious.
   if (aRemoteType.EqualsLiteral(FILE_REMOTE_TYPE)) {
     SetProcessName(NS_LITERAL_STRING("file:// Content"));
   } else if (aRemoteType.EqualsLiteral(EXTENSION_REMOTE_TYPE)) {
     SetProcessName(NS_LITERAL_STRING("WebExtensions"));
+  } else if (aRemoteType.EqualsLiteral(PRIVILEGED_REMOTE_TYPE)) {
+    SetProcessName(NS_LITERAL_STRING("Privileged Content"));
   } else if (aRemoteType.EqualsLiteral(LARGE_ALLOCATION_REMOTE_TYPE)) {
     SetProcessName(NS_LITERAL_STRING("Large Allocation Web Content"));
   }
 
   return IPC_OK();
 }
 
 const nsAString&
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -1852,18 +1852,18 @@ ContentParent::ShouldKeepProcessAlive() 
     return false;
   }
 
   auto contentParents = sBrowserContentParents->Get(mRemoteType);
   if (!contentParents) {
     return false;
   }
 
-  // We might want to keep alive some content processes alive during test runs,
-  // for performance reasons. This should never be used in production.
+  // We might want to keep some content processes alive for performance reasons.
+  // e.g. test runs and privileged content process for some about: pages.
   // We don't want to alter behavior if the pref is not set, so default to 0.
   int32_t processesToKeepAlive = 0;
 
   nsAutoCString keepAlivePref("dom.ipc.keepProcessesAlive.");
   keepAlivePref.Append(NS_ConvertUTF16toUTF8(mRemoteType));
   if (NS_FAILED(Preferences::GetInt(keepAlivePref.get(), &processesToKeepAlive))) {
     return false;
   }
--- a/dom/ipc/ContentParent.h
+++ b/dom/ipc/ContentParent.h
@@ -38,16 +38,17 @@
 
 // These must match the similar ones in E10SUtils.jsm.
 // Process names as reported by about:memory are defined in
 // ContentChild:RecvRemoteType.  Add your value there too or it will be called
 // "Web Content".
 #define DEFAULT_REMOTE_TYPE "web"
 #define FILE_REMOTE_TYPE "file"
 #define EXTENSION_REMOTE_TYPE "extension"
+#define PRIVILEGED_REMOTE_TYPE "privileged"
 
 // This must start with the DEFAULT_REMOTE_TYPE above.
 #define LARGE_ALLOCATION_REMOTE_TYPE "webLargeAllocation"
 
 class nsConsoleService;
 class nsIContentProcessInfo;
 class nsICycleCollectorLogSink;
 class nsIDumpGCAndCCLogsCallback;
--- a/js/xpconnect/loader/ScriptPreloader.cpp
+++ b/js/xpconnect/loader/ScriptPreloader.cpp
@@ -187,16 +187,19 @@ ScriptPreloader::InitContentChild(Conten
 }
 
 ProcessType
 ScriptPreloader::GetChildProcessType(const nsAString& remoteType)
 {
     if (remoteType.EqualsLiteral(EXTENSION_REMOTE_TYPE)) {
         return ProcessType::Extension;
     }
+    if (remoteType.EqualsLiteral(PRIVILEGED_REMOTE_TYPE)) {
+        return ProcessType::Privileged;
+    }
     return ProcessType::Web;
 }
 
 
 namespace {
 
 static void
 TraceOp(JSTracer* trc, void* data)
--- a/js/xpconnect/loader/ScriptPreloader.h
+++ b/js/xpconnect/loader/ScriptPreloader.h
@@ -38,16 +38,17 @@ namespace ipc {
 namespace loader {
     class InputBuffer;
     class ScriptCacheChild;
 
     enum class ProcessType : uint8_t {
         Parent,
         Web,
         Extension,
+        Privileged,
     };
 
     template <typename T>
     struct Matcher
     {
         virtual bool Matches(T) = 0;
     };
 }
--- a/js/xpconnect/loader/script_cache.py
+++ b/js/xpconnect/loader/script_cache.py
@@ -15,28 +15,31 @@ def usage():
 
     sys.exit(1)
 
 
 class ProcessTypes:
     Default = 0
     Web = 1
     Extension = 2
+    Privileged = 3
 
     def __init__(self, val):
         self.val = val
 
     def __str__(self):
         res = []
         if self.val & (1 << self.Default):
             res.append('Parent')
         if self.val & (1 << self.Web):
             res.append('Web')
         if self.val & (1 << self.Extension):
             res.append('Extension')
+        if self.val & (1 << self.Privileged):
+            res.append('Privileged')
         return '|'.join(res)
 
 
 class InputBuffer(object):
 
     def __init__(self, data):
         self.data = data
         self.offset = 0
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -3213,16 +3213,24 @@ pref("dom.ipc.plugins.forcedirect.enable
 pref("dom.ipc.processCount", 4);
 
 // Default to allow only one file:// URL content process.
 pref("dom.ipc.processCount.file", 1);
 
 // WebExtensions only support a single extension process.
 pref("dom.ipc.processCount.extension", 1);
 
+// Privileged content only supports a single content process.
+pref("dom.ipc.processCount.privileged", 1);
+
+// Keep a single privileged content process alive for performance reasons.
+// e.g. we do not want to throw content processes out every time we navigate
+// away from about:newtab.
+pref("dom.ipc.keepProcessesAlive.privileged", 1);
+
 // Whether a native event loop should be used in the content process.
 #if defined(XP_WIN)
 pref("dom.ipc.useNativeEventProcessing.content", false);
 #else
 pref("dom.ipc.useNativeEventProcessing.content", true);
 #endif
 
 // Quantum DOM scheduling:
@@ -3249,16 +3257,19 @@ pref("browser.tabs.remote.separateFileUr
 
 // Pref that enables top level web content pages that are opened from file://
 // URI pages to run in the file content process.
 // This has been added in case breaking any window references between these
 // sorts of pages, which we have to do when we run them in the normal web
 // content process, causes compatibility issues.
 pref("browser.tabs.remote.allowLinkedWebInFileUriProcess", true);
 
+// Pref to control whether we use separate privileged content processes.
+pref("browser.tabs.remote.separatePrivilegedContentProcess", false);
+
 // Enable the use of display-lists for SVG hit-testing and painting.
 pref("svg.display-lists.hit-testing.enabled", true);
 pref("svg.display-lists.painting.enabled", true);
 
 // Is support for the <marker orient="auto-start-reverse"> feature enabled?
 pref("svg.marker-improvements.enabled", true);
 
 // Is support for the new getBBox method from SVG 2 enabled?
--- a/toolkit/modules/E10SUtils.jsm
+++ b/toolkit/modules/E10SUtils.jsm
@@ -8,16 +8,18 @@ var EXPORTED_SYMBOLS = ["E10SUtils"];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "useSeparateFileUriProcess",
                                       "browser.tabs.remote.separateFileUriProcess", false);
 XPCOMUtils.defineLazyPreferenceGetter(this, "allowLinkedWebInFileUriProcess",
                                       "browser.tabs.remote.allowLinkedWebInFileUriProcess", false);
+XPCOMUtils.defineLazyPreferenceGetter(this, "useSeparatePrivilegedContentProcess",
+                                      "browser.tabs.remote.separatePrivilegedContentProcess", false);
 ChromeUtils.defineModuleGetter(this, "Utils",
                                "resource://gre/modules/sessionstore/Utils.jsm");
 
 function getAboutModule(aURL) {
   // Needs to match NS_GetAboutModuleName
   let moduleName = aURL.pathQueryRef.replace(/[#?].*/, "").toLowerCase();
   let contract = "@mozilla.org/network/protocol/about;1?what=" + moduleName;
   try {
@@ -30,21 +32,24 @@ function getAboutModule(aURL) {
 }
 
 const NOT_REMOTE = null;
 
 // These must match any similar ones in ContentParent.h.
 const WEB_REMOTE_TYPE = "web";
 const FILE_REMOTE_TYPE = "file";
 const EXTENSION_REMOTE_TYPE = "extension";
+const PRIVILEGED_REMOTE_TYPE = "privileged";
 
 // This must start with the WEB_REMOTE_TYPE above.
 const LARGE_ALLOCATION_REMOTE_TYPE = "webLargeAllocation";
 const DEFAULT_REMOTE_TYPE = WEB_REMOTE_TYPE;
 
+const ACTIVITY_STREAM_PAGES = new Set(["home", "newtab", "welcome"]);
+
 function validatedWebRemoteType(aPreferredRemoteType, aTargetUri, aCurrentUri) {
   // If the domain is whitelisted to allow it to use file:// URIs, then we have
   // to run it in a file content process, in case it uses file:// sub-resources.
   const sm = Services.scriptSecurityManager;
   if (sm.inFileURIWhitelist(aTargetUri)) {
     return FILE_REMOTE_TYPE;
   }
 
@@ -77,16 +82,17 @@ function validatedWebRemoteType(aPreferr
 }
 
 var E10SUtils = {
   DEFAULT_REMOTE_TYPE,
   NOT_REMOTE,
   WEB_REMOTE_TYPE,
   FILE_REMOTE_TYPE,
   EXTENSION_REMOTE_TYPE,
+  PRIVILEGED_REMOTE_TYPE,
   LARGE_ALLOCATION_REMOTE_TYPE,
 
   canLoadURIInProcess(aURL, aProcess) {
     let remoteType = aProcess == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT
                      ? DEFAULT_REMOTE_TYPE : NOT_REMOTE;
     return remoteType == this.getRemoteTypeForURI(aURL, true, remoteType);
   },
 
@@ -148,16 +154,21 @@ var E10SUtils = {
         // If the module doesn't exist then an error page will be loading, that
         // should be ok to load in any process
         if (!module) {
           return aPreferredRemoteType;
         }
 
         let flags = module.getURIFlags(aURI);
         if (flags & Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD) {
+          // Load Activity Stream in a separate process.
+          if (useSeparatePrivilegedContentProcess &&
+              ACTIVITY_STREAM_PAGES.has(aURI.filePath)) {
+            return PRIVILEGED_REMOTE_TYPE;
+          }
           return DEFAULT_REMOTE_TYPE;
         }
 
         // If the about page can load in parent or child, it should be safe to
         // load in any remote type.
         if (flags & Ci.nsIAboutModule.URI_CAN_LOAD_IN_CHILD) {
           return aPreferredRemoteType;
         }