Bug 1521808 - Implement process switching based on Cross-Opener-Origin-Policy header r=nika,qdot
☠☠ backed out by 45aaded7cf63 ☠ ☠
authorValentin Gosu <valentin.gosu@gmail.com>
Fri, 15 Feb 2019 12:14:49 +0000
changeset 459534 42641f88d7dae6eb41a3aec04cb118b00be693c9
parent 459533 32ce09b2c33ac1f59b3caeeec27e8992c54ebb63
child 459535 709a25d2a3451acc26833a4cfdf769300ab56e91
push id111964
push usercsabou@mozilla.com
push dateFri, 15 Feb 2019 18:54:44 +0000
treeherdermozilla-inbound@db3c4f905082 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnika, qdot
bugs1521808
milestone67.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 1521808 - Implement process switching based on Cross-Opener-Origin-Policy header r=nika,qdot * New topLevel loads get the nsILoadInfo.openerPolicy of the current top level document * Parsing the Cross-Opener-Origin-Policy of a channel will update mLoadInfo.openerPolicy and this value will get propagated to the child process. * SessionStore now checks nsIHttpChannel.hasCrossOriginOpenerPolicyMismatch (preffed off) and performs a process switch if needed Differential Revision: https://phabricator.services.mozilla.com/D19000
browser/components/sessionstore/SessionStore.jsm
docshell/base/nsDocShell.cpp
ipc/glue/BackgroundUtils.cpp
netwerk/protocol/http/nsHttpChannel.cpp
toolkit/components/remotebrowserutils/tests/browser/browser.ini
toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js
toolkit/components/remotebrowserutils/tests/browser/coop_header.sjs
toolkit/modules/E10SUtils.jsm
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -2310,17 +2310,18 @@ var SessionStoreInternal = {
     let tabParent = aBrowser.frameLoader.tabParent;
     debug(`[process-switch]: new tabID: ${tabParent.tabId}`);
     return tabParent;
   },
 
   // Examine the channel response to see if we should change the process
   // performing the given load.
   onMayChangeProcess(aChannel) {
-    if (!E10SUtils.useHttpResponseProcessSelection()) {
+    if (!E10SUtils.useHttpResponseProcessSelection() &&
+        !E10SUtils.useCrossOriginOpenerPolicy()) {
       return;
     }
 
     if (!aChannel.isDocument || !aChannel.loadInfo) {
       return; // Not a document load.
     }
 
     // Check that this is a toplevel document load.
@@ -2365,16 +2366,17 @@ var SessionStoreInternal = {
       debug(`[process-switch]: No nsITabParent for channel - ignoring`);
       return;
     }
 
     // Ensure we're loaded in a regular tabbrowser environment, and can swap processes.
     let browser = tabParent.ownerElement;
     if (!browser) {
       debug(`[process-switch]: TabParent has no ownerElement - ignoring`);
+      return;
     }
 
     let tabbrowser = browser.ownerGlobal.gBrowser;
     if (!tabbrowser) {
       debug(`[process-switch]: cannot find tabbrowser for loading tab - ignoring`);
       return;
     }
 
@@ -2386,17 +2388,19 @@ var SessionStoreInternal = {
 
     // Determine the process type the load should be performed in.
     let resultPrincipal =
       Services.scriptSecurityManager.getChannelResultPrincipal(aChannel);
     let remoteType = E10SUtils.getRemoteTypeForPrincipal(resultPrincipal,
                                                          true,
                                                          browser.remoteType,
                                                          currentPrincipal);
-    if (browser.remoteType == remoteType) {
+    if (browser.remoteType == remoteType &&
+        (!E10SUtils.useCrossOriginOpenerPolicy() ||
+         !aChannel.hasCrossOriginOpenerPolicyMismatch())) {
       debug(`[process-switch]: type (${remoteType}) is compatible - ignoring`);
       return;
     }
 
     if (remoteType == E10SUtils.NOT_REMOTE ||
         browser.remoteType == E10SUtils.NOT_REMOTE) {
       debug(`[process-switch]: non-remote source/target - ignoring`);
       return;
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -9800,18 +9800,16 @@ nsresult nsDocShell::DoURILoad(nsDocShel
 
   if (inheritPrincipal) {
     securityFlags |= nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL;
   }
   if (isSandBoxed) {
     securityFlags |= nsILoadInfo::SEC_SANDBOXED;
   }
 
-  // TODO: pass openerPolicy through loadInfo?
-
   RefPtr<LoadInfo> loadInfo =
       (contentPolicyType == nsIContentPolicy::TYPE_DOCUMENT)
           ? new LoadInfo(loadingWindow, aLoadState->TriggeringPrincipal(),
                          topLevelLoadingContext, securityFlags)
           : new LoadInfo(loadingPrincipal, aLoadState->TriggeringPrincipal(),
                          loadingNode, securityFlags, contentPolicyType);
 
   if (aLoadState->PrincipalToInherit()) {
@@ -9825,16 +9823,22 @@ nsresult nsDocShell::DoURILoad(nsDocShel
 
   // We have to do this in case our OriginAttributes are different from the
   // OriginAttributes of the parent document. Or in case there isn't a
   // parent document.
   bool isTopLevelDoc = mItemType == typeContent &&
                        (contentPolicyType == nsIContentPolicy::TYPE_DOCUMENT ||
                         GetIsMozBrowser());
 
+  if (isTopLevelDoc && GetDocument() && GetDocument()->GetChannel()) {
+    nsCOMPtr<nsILoadInfo> oldLoadInfo =
+        GetDocument()->GetChannel()->GetLoadInfo();
+    loadInfo->SetOpenerPolicy(oldLoadInfo->GetOpenerPolicy());
+  }
+
   OriginAttributes attrs;
 
   // Inherit origin attributes from PrincipalToInherit if inheritAttrs is
   // true. Otherwise we just use the origin attributes from docshell.
   if (inheritAttrs) {
     MOZ_ASSERT(aLoadState->PrincipalToInherit(),
                "We should have PrincipalToInherit here.");
     attrs = aLoadState->PrincipalToInherit()->OriginAttributesRef();
--- a/ipc/glue/BackgroundUtils.cpp
+++ b/ipc/glue/BackgroundUtils.cpp
@@ -710,17 +710,18 @@ nsresult MergeParentLoadInfoForwarder(
 
   if (aForwarderArgs.serviceWorkerTaintingSynthesized()) {
     aLoadInfo->SynthesizeServiceWorkerTainting(
         static_cast<LoadTainting>(aForwarderArgs.tainting()));
   } else {
     aLoadInfo->MaybeIncreaseTainting(aForwarderArgs.tainting());
   }
 
-  // TODO: merge openerPolicy
+  MOZ_ALWAYS_SUCCEEDS(
+      aLoadInfo->SetOpenerPolicy(aForwarderArgs.openerPolicy()));
 
   MOZ_ALWAYS_SUCCEEDS(aLoadInfo->SetDocumentHasUserInteracted(
       aForwarderArgs.documentHasUserInteracted()));
   MOZ_ALWAYS_SUCCEEDS(
       aLoadInfo->SetDocumentHasLoaded(aForwarderArgs.documentHasLoaded()));
 
   return NS_OK;
 }
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -7252,16 +7252,18 @@ nsHttpChannel::HasCrossOriginOpenerPolic
   }
 
   // Get the policy of the active document, and the policy for the result.
   nsILoadInfo::CrossOriginOpenerPolicy documentPolicy =
       mLoadInfo->GetOpenerPolicy();
   nsILoadInfo::CrossOriginOpenerPolicy resultPolicy =
       GetCrossOriginOpenerPolicy(head);
 
+  mLoadInfo->SetOpenerPolicy(resultPolicy);
+
   // We use the top window principal as the documentOrigin
   if (!mTopWindowPrincipal) {
     GetTopWindowPrincipal(getter_AddRefs(mTopWindowPrincipal));
   }
 
   nsCOMPtr<nsIPrincipal> documentOrigin = mTopWindowPrincipal;
   nsCOMPtr<nsIPrincipal> resultOrigin;
 
--- a/toolkit/components/remotebrowserutils/tests/browser/browser.ini
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser.ini
@@ -1,11 +1,13 @@
 [DEFAULT]
 run-if = e10s
 support-files =
   dummy_page.html
   print_postdata.sjs
   307redirect.sjs
   head.js
+  coop_header.sjs
 
 [browser_RemoteWebNavigation.js]
 [browser_httpResponseProcessSelection.js]
+[browser_httpCrossOriginOpenerPolicy.js]
 [browser_httpToFileHistory.js]
copy from toolkit/components/remotebrowserutils/tests/browser/browser_httpResponseProcessSelection.js
copy to toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js
--- a/toolkit/components/remotebrowserutils/tests/browser/browser_httpResponseProcessSelection.js
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js
@@ -1,140 +1,77 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
 const {E10SUtils} = ChromeUtils.import("resource://gre/modules/E10SUtils.jsm");
 
-let PREF_NAME = "browser.tabs.remote.useHTTPResponseProcessSelection";
+const PREF_NAME = "browser.tabs.remote.useCrossOriginOpenerPolicy";
+
+function httpURL(filename, host = "https://example.com") {
+  let root = getRootDirectory(gTestPath)
+    .replace("chrome://mochitests/content", host);
+  return root + filename;
+}
 
 async function performLoad(browser, opts, action) {
-  let loadedPromise = BrowserTestUtils.browserLoaded(
-    browser, false, opts.url, opts.maybeErrorPage);
+  let loadedPromise = BrowserTestUtils.browserStopped(
+    browser, opts.url, opts.maybeErrorPage);
   await action();
   await loadedPromise;
 }
 
-const EXTENSION_DATA = {
-  manifest: {
-    "name": "Simple extension test",
-    "version": "1.0",
-    "manifest_version": 2,
-    "description": "",
-  },
-
-  files: {
-    "dummy.html": "<html>webext dummy</html>",
-  },
-};
+async function test_coop(start, target, expectedProcessSwitch) {
+  return BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: start,
+    waitForStateStop: true,
+  }, async function(_browser) {
+    info(`Test tab ready: ${start}`);
 
-async function withExtensionDummy(callback) {
-  let extension = ExtensionTestUtils.loadExtension(EXTENSION_DATA);
-  await extension.startup();
-  let rv = await callback(`moz-extension://${extension.uuid}/dummy.html`);
-  await extension.unload();
-  return rv;
-}
-
-const PRINT_POSTDATA = httpURL("print_postdata.sjs");
-const FILE_DUMMY = fileURL("dummy_page.html");
-
-async function postFrom(start, target) {
-  return BrowserTestUtils.withNewTab({
-    gBrowser, url: start,
-  }, async function(browser) {
-    info("Test tab ready: " + start);
-
-    // Create the form element in our loaded URI.
-    await ContentTask.spawn(browser, { target }, function({ target }) {
-      // eslint-disable-next-line no-unsanitized/property
-      content.document.body.innerHTML = `
-        <form method="post" action="${target}">
-          <input type="text" name="initialRemoteType" value="${Services.appinfo.remoteType}">
-          <input type="submit" id="submit">
-        </form>`;
+    await new Promise(resolve => setTimeout(resolve, 20));
+    let browser = gBrowser.selectedBrowser;
+    let firstProcessID = await ContentTask.spawn(browser, null, () => {
+      return Services.appinfo.processID;
     });
 
-    // Perform a form POST submit load.
-    info("Performing POST submission");
+    info(`firstProcessID: ${firstProcessID}`);
+
     await performLoad(browser, {
-      url(url) { return url == PRINT_POSTDATA || url == target; },
-      maybeErrorPage: true,
+      url: target,
+      maybeErrorPage: false,
     }, async () => {
-      await ContentTask.spawn(browser, null, () => {
-        content.document.querySelector("#submit").click();
-      });
+      BrowserTestUtils.loadURI(browser, target);
+      if (expectedProcessSwitch) {
+        await BrowserTestUtils.waitForEvent(gBrowser.getTabForBrowser(browser), "SSTabRestored");
+      }
     });
 
-    // Check that the POST data was submitted.
-    info("Fetching results");
-    return ContentTask.spawn(browser, null, () => {
-      return {
-        remoteType: Services.appinfo.remoteType,
-        location: "" + content.location.href,
-        body: content.document.body.textContent,
-      };
+    info(`Navigated to: ${target}`);
+    await new Promise(resolve => setTimeout(resolve, 20));
+    browser = gBrowser.selectedBrowser;
+    let secondProcessID = await ContentTask.spawn(browser, null, () => {
+      return Services.appinfo.processID;
     });
+
+    info(`secondProcessID: ${secondProcessID}`);
+    if (expectedProcessSwitch) {
+      Assert.notEqual(firstProcessID, secondProcessID, `from: ${start} to ${target}`);
+    } else {
+      Assert.equal(firstProcessID, secondProcessID, `from: ${start} to ${target}`);
+    }
   });
 }
 
 add_task(async function test_disabled() {
   await SpecialPowers.pushPrefEnv({set: [[PREF_NAME, false]]});
-
-  // With the pref disabled, file URIs should successfully POST, but remain in
-  // the 'file' process.
-  info("DISBALED -- FILE -- raw URI load");
-  let resp = await postFrom(FILE_DUMMY, PRINT_POSTDATA);
-  is(resp.remoteType, E10SUtils.FILE_REMOTE_TYPE, "no process switch");
-  is(resp.location, PRINT_POSTDATA, "correct location");
-  is(resp.body, "initialRemoteType=file", "correct POST body");
-
-  info("DISABLED -- FILE -- 307-redirect URI load");
-  let resp307 = await postFrom(FILE_DUMMY, add307(PRINT_POSTDATA));
-  is(resp307.remoteType, E10SUtils.FILE_REMOTE_TYPE, "no process switch");
-  is(resp307.location, PRINT_POSTDATA, "correct location");
-  is(resp307.body, "initialRemoteType=file", "correct POST body");
-
-  // With the pref disabled, extension URIs should fail to POST, but correctly
-  // switch processes.
-  await withExtensionDummy(async (dummy) => {
-    info("DISABLED -- EXTENSION -- raw URI load");
-    let respExt = await postFrom(dummy, PRINT_POSTDATA);
-    is(respExt.remoteType, E10SUtils.WEB_REMOTE_TYPE, "process switch");
-    is(respExt.location, PRINT_POSTDATA, "correct location");
-    is(respExt.body, "", "no POST body");
-
-    info("DISABLED -- EXTENSION -- 307-redirect URI load");
-    let respExt307 = await postFrom(dummy, add307(PRINT_POSTDATA));
-    is(respExt307.remoteType, E10SUtils.WEB_REMOTE_TYPE, "process switch");
-    is(respExt307.location, PRINT_POSTDATA, "correct location");
-    is(respExt307.body, "", "no POST body");
-  });
+  await test_coop(httpURL("coop_header.sjs", "https://example.com"), httpURL("coop_header.sjs", "https://example.com"), false);
+  await test_coop(httpURL("coop_header.sjs?same-origin", "http://example.com"), httpURL("coop_header.sjs", "http://example.com"), false);
+  await test_coop(httpURL("coop_header.sjs", "http://example.com"), httpURL("coop_header.sjs?same-origin", "http://example.com"), false);
+  await test_coop(httpURL("coop_header.sjs?same-origin", "http://example.com"), httpURL("coop_header.sjs?same-site", "http://example.com"), false); // assuming we don't have fission yet :)
 });
 
 add_task(async function test_enabled() {
   await SpecialPowers.pushPrefEnv({set: [[PREF_NAME, true]]});
-
-  // With the pref enabled, URIs should correctly switch processes & the POST
-  // should succeed.
-  info("ENABLED -- FILE -- raw URI load");
-  let resp = await postFrom(FILE_DUMMY, PRINT_POSTDATA);
-  is(resp.remoteType, E10SUtils.WEB_REMOTE_TYPE, "process switch");
-  is(resp.location, PRINT_POSTDATA, "correct location");
-  is(resp.body, "initialRemoteType=file", "correct POST body");
-
-  info("ENABLED -- FILE -- 307-redirect URI load");
-  let resp307 = await postFrom(FILE_DUMMY, add307(PRINT_POSTDATA));
-  is(resp307.remoteType, E10SUtils.WEB_REMOTE_TYPE, "process switch");
-  is(resp307.location, PRINT_POSTDATA, "correct location");
-  is(resp307.body, "initialRemoteType=file", "correct POST body");
-
-  // Same with extensions
-  await withExtensionDummy(async (dummy) => {
-    info("ENABLED -- EXTENSION -- raw URI load");
-    let respExt = await postFrom(dummy, PRINT_POSTDATA);
-    is(respExt.remoteType, E10SUtils.WEB_REMOTE_TYPE, "process switch");
-    is(respExt.location, PRINT_POSTDATA, "correct location");
-    is(respExt.body, "initialRemoteType=extension", "correct POST body");
-
-    info("ENABLED -- EXTENSION -- 307-redirect URI load");
-    let respExt307 = await postFrom(dummy, add307(PRINT_POSTDATA));
-    is(respExt307.remoteType, E10SUtils.WEB_REMOTE_TYPE, "process switch");
-    is(respExt307.location, PRINT_POSTDATA, "correct location");
-    is(respExt307.body, "initialRemoteType=extension", "correct POST body");
-  });
+  await test_coop(httpURL("coop_header.sjs", "https://example.com"), httpURL("coop_header.sjs", "https://example.com"), false);
+  await test_coop(httpURL("coop_header.sjs", "https://example.com"), httpURL("coop_header.sjs?same-origin", "https://example.org"), true);
+  await test_coop(httpURL("coop_header.sjs?same-origin#1", "https://example.com"), httpURL("coop_header.sjs?same-origin#1", "https://example.org"), true);
+  await test_coop(httpURL("coop_header.sjs?same-origin#2", "https://example.com"), httpURL("coop_header.sjs?same-site#2", "https://example.org"), true);
 });
new file mode 100644
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/coop_header.sjs
@@ -0,0 +1,13 @@
+function handleRequest(request, response)
+{
+  response.setStatusLine(request.httpVersion, 200, "OK");
+
+  let coop = request.queryString;
+  if (coop.length > 0) {
+    response.setHeader("Cross-Origin-Opener-Policy", unescape(coop), false);
+  }
+
+  response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+
+  response.write("<!DOCTYPE html><html><body><p>Hello world</p></body></html>");
+}
--- a/toolkit/modules/E10SUtils.jsm
+++ b/toolkit/modules/E10SUtils.jsm
@@ -12,19 +12,22 @@ const {XPCOMUtils} = ChromeUtils.import(
 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);
 XPCOMUtils.defineLazyPreferenceGetter(this, "useHttpResponseProcessSelection",
                                       "browser.tabs.remote.useHTTPResponseProcessSelection", false);
+XPCOMUtils.defineLazyPreferenceGetter(this, "useCrossOriginOpenerPolicy",
+                                      "browser.tabs.remote.useCrossOriginOpenerPolicy", false);
 XPCOMUtils.defineLazyServiceGetter(this, "serializationHelper",
                                    "@mozilla.org/network/serialization-helper;1",
                                    "nsISerializationHelper");
+
 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 {
@@ -93,16 +96,19 @@ var E10SUtils = {
   FILE_REMOTE_TYPE,
   EXTENSION_REMOTE_TYPE,
   PRIVILEGED_REMOTE_TYPE,
   LARGE_ALLOCATION_REMOTE_TYPE,
 
   useHttpResponseProcessSelection() {
     return useHttpResponseProcessSelection;
   },
+  useCrossOriginOpenerPolicy() {
+    return useCrossOriginOpenerPolicy;
+  },
 
   canLoadURIInRemoteType(aURL, aRemoteType = DEFAULT_REMOTE_TYPE,
                          aPreferredRemoteType = undefined) {
     // We need a strict equality here because the value of `NOT_REMOTE` is
     // `null`, and there is a possibility that `undefined` is passed as an
     // argument, which might result a load in the parent process.
     if (aPreferredRemoteType === undefined) {
       aPreferredRemoteType = aRemoteType === NOT_REMOTE