Bug 1723797 - Add a separate pref to control process isolation strategy when fission.autostart is enabled, r=farre,johannh,necko-reviewers,dragana

This new pref will be used on android to enable high-value-only process
isolation. An initial version of high-value-only process isolation is
also implemented in this bug, using the permission manager to track
whether a site is high-value due to having served a
`Cross-Origin-Opener-Policy` header.

Future high-value permissions due to things like logging into a site and
OAuth flows can be tracked in the same way, by adding the permission to
the permissions database.

In the future, it might be valuable to provide UI for visualizing what
sites are considered high-value at any point in time, but this works
fine for now.

Differential Revision: https://phabricator.services.mozilla.com/D123127
This commit is contained in:
Nika Layzell 2021-09-08 17:45:31 +00:00
Родитель e2de461dc0
Коммит 3211ad72e0
7 изменённых файлов: 469 добавлений и 13 удалений

Просмотреть файл

@ -21,9 +21,11 @@
#include "mozilla/ExtensionPolicyService.h"
#include "mozilla/Logging.h"
#include "mozilla/NullPrincipal.h"
#include "mozilla/PermissionManager.h"
#include "mozilla/Preferences.h"
#include "mozilla/RefPtr.h"
#include "mozilla/StaticPrefs_browser.h"
#include "mozilla/StaticPrefs_fission.h"
#include "mozilla/StaticPtr.h"
#include "nsAboutProtocolUtils.h"
#include "nsDocShell.h"
@ -44,6 +46,27 @@ mozilla::LazyLogModule gProcessIsolationLog{"ProcessIsolation"};
namespace {
// Strategy used to determine whether or not a particular site should load into
// a webIsolated content process. The particular strategy chosen is controlled
// by the `fission.webContentIsolationStrategy` pref, which must hold one of the
// following values.
enum class WebContentIsolationStrategy : uint32_t {
// All web content is loaded into a shared `web` content process. This is
// similar to the non-Fission behaviour, however remote subframes may still
// be used for sites with special isolation behaviour, such as extension or
// mozillaweb content processes.
IsolateNothing = 0,
// Web content is always isolated into its own `webIsolated` content process
// based on site-origin, and will only load in a shared `web` content process
// if site-origin could not be determined.
IsolateEverything = 1,
// Only isolates web content loaded by sites which are considered "high
// value". A site is considered "high value" if it has been granted a
// `highValue*` permission by the permission manager, which is done in
// response to certain actions.
IsolateHighValue = 2,
};
/**
* Helper class for caching the result of splitting prefs which are represented
* as a comma-separated list of strings.
@ -398,19 +421,72 @@ static bool IsLargeAllocationLoad(CanonicalBrowsingContext* aBrowsingContext,
*/
static bool ShouldIsolateSite(nsIPrincipal* aPrincipal,
CanonicalBrowsingContext* aTopBC) {
// If Fission is disabled, we never want to isolate. We check the toplevel BC
// if it's available, or the global pref if checking for shared or service
// workers.
if (aTopBC && !aTopBC->UseRemoteSubframes()) {
return false;
}
if (!aTopBC && !mozilla::FissionAutostart()) {
return false;
}
// non-content principals currently can't have webIsolated remote types
// assigned to them, so should not be isolated.
if (!aPrincipal->GetIsContentPrincipal()) {
return false;
}
// FIXME: This should contain logic to allow enabling/disabling whether a
// particular site should be isolated for e.g. android, where we may want to
// turn on/off isolating certain sites at runtime.
if (aTopBC) {
return aTopBC->UseRemoteSubframes();
switch (WebContentIsolationStrategy(
StaticPrefs::fission_webContentIsolationStrategy())) {
case WebContentIsolationStrategy::IsolateNothing:
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("Not isolating '%s' as isolation is disabled",
OriginString(aPrincipal).get()));
return false;
case WebContentIsolationStrategy::IsolateEverything:
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("Isolating '%s' as isolation is enabled for all sites",
OriginString(aPrincipal).get()));
return true;
case WebContentIsolationStrategy::IsolateHighValue: {
RefPtr<PermissionManager> perms = PermissionManager::GetInstance();
if (NS_WARN_IF(!perms)) {
// If we somehow have no permission manager, fall back to the safest
// option, and try to isolate.
MOZ_ASSERT_UNREACHABLE("Permission manager is missing");
return true;
}
static constexpr nsLiteralCString kHighValuePermissions[] = {
mozilla::dom::kHighValueCOOPPermission,
};
for (const auto& type : kHighValuePermissions) {
uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION;
if (NS_SUCCEEDED(perms->TestPermissionFromPrincipal(aPrincipal, type,
&permission)) &&
permission == nsIPermissionManager::ALLOW_ACTION) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("Isolating '%s' due to high-value permission '%s'",
OriginString(aPrincipal).get(), type.get()));
return true;
}
}
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("Not isolating '%s' as it is not high-value",
OriginString(aPrincipal).get()));
return false;
}
default:
// An invalid pref value was used. Fall back to the safest option and
// isolate everything.
NS_WARNING("Invalid pref value for fission.webContentIsolationStrategy");
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("Isolating '%s' due to unknown strategy pref value",
OriginString(aPrincipal).get()));
return true;
}
return mozilla::FissionAutostart();
}
enum class WebProcessType {

Просмотреть файл

@ -24,6 +24,8 @@ class WindowGlobalParent;
extern mozilla::LazyLogModule gProcessIsolationLog;
constexpr nsLiteralCString kHighValueCOOPPermission = "highValueCOOP"_ns;
// NavigationIsolationOptions is passed through the methods to store the state
// of the possible process and/or browsing context change.
struct NavigationIsolationOptions {

Просмотреть файл

@ -42,3 +42,5 @@ support-files = file_dummy.html
run-if = widget == "gtk"
[browser_subframesPreferUsed.js]
skip-if = !fission # Test doesn't make sense without fission
[browser_web_process_isolation.js]
skip-if = !fission # Only relevant for fission

Просмотреть файл

@ -0,0 +1,291 @@
// This test is fission-only! Make that clear before continuing, to avoid
// confusing failures.
ok(
Services.appinfo.fissionAutostart,
"this test requires fission to function!"
);
const WebContentIsolationStrategy = {
IsolateNothing: 0,
IsolateEverything: 1,
IsolateHighValue: 2,
};
const COM_ORIGIN = "https://example.com";
const ORG_ORIGIN = "https://example.org";
const MOZ_ORIGIN = "https://www.mozilla.org";
// Helper for building document-builder.sjs URLs which have specific headers &
// HTML content.
function documentURL(origin, headers, html) {
let params = new URLSearchParams();
params.append("html", html.trim());
for (const [key, value] of Object.entries(headers)) {
params.append("headers", `${key}:${value}`);
}
return `${origin}/document-builder.sjs?${params.toString()}`;
}
async function testTreeRemoteTypes(name, testpage) {
// Use document-builder.sjs to build up the expected document tree.
function buildURL(path, page) {
let html = `<h1>${path}</h1>`;
for (let i = 0; i < page.children.length; ++i) {
const inner = buildURL(`${path}[${i}]`, page.children[i]);
html += `<iframe src=${JSON.stringify(inner)}></iframe>`;
}
return documentURL(page.origin, page.headers, html);
}
const url = buildURL(name, testpage);
// Load the tab and confirm that properties of the loaded documents match
// expectation.
await BrowserTestUtils.withNewTab(url, async browser => {
let stack = [
{
path: name,
bc: browser.browsingContext,
...testpage,
},
];
while (stack.length) {
const { path, bc, remoteType, children, origin } = stack.pop();
is(
Services.scriptSecurityManager.createContentPrincipal(
bc.currentWindowGlobal.documentURI,
{}
).originNoSuffix,
origin,
`Frame ${path} has expected originNoSuffix`
);
is(
bc.currentWindowGlobal.domProcess.remoteType,
remoteType,
`Frame ${path} has expected remote type`
);
is(
bc.children.length,
children.length,
`Frame ${path} has the expected number of children`
);
for (let i = 0; i < bc.children.length; ++i) {
stack.push({
path: `${path}[${i}]`,
bc: bc.children[i],
...children[i],
});
}
}
});
}
function mkTestPage({
comRemoteType,
orgRemoteType,
mozRemoteType,
topOrigin,
topHeaders = {},
frameHeaders = {},
}) {
const topRemoteType = {
[COM_ORIGIN]: comRemoteType,
[ORG_ORIGIN]: orgRemoteType,
[MOZ_ORIGIN]: mozRemoteType,
}[topOrigin];
const innerChildren = [
{
origin: COM_ORIGIN,
headers: frameHeaders,
remoteType: comRemoteType,
children: [],
},
{
origin: ORG_ORIGIN,
headers: frameHeaders,
remoteType: orgRemoteType,
children: [],
},
{
origin: MOZ_ORIGIN,
headers: frameHeaders,
remoteType: mozRemoteType,
children: [],
},
];
return {
origin: topOrigin,
headers: topHeaders,
remoteType: topRemoteType,
children: [
{
origin: COM_ORIGIN,
headers: frameHeaders,
remoteType: comRemoteType,
children: [...innerChildren],
},
{
origin: ORG_ORIGIN,
headers: frameHeaders,
remoteType: orgRemoteType,
children: [...innerChildren],
},
{
origin: MOZ_ORIGIN,
headers: frameHeaders,
remoteType: mozRemoteType,
children: [...innerChildren],
},
],
};
}
async function do_tests(expected) {
// Clear all site-specific data, as we don't want to have any high-value site
// permissions from any previous iterations.
await new Promise(resolve =>
Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve)
);
// Loads for basic URLs with no special headers set.
await testTreeRemoteTypes(
"basic_com",
mkTestPage({
topOrigin: COM_ORIGIN,
comRemoteType: expected.com_normal,
orgRemoteType: expected.org_normal,
mozRemoteType: expected.moz_normal,
})
);
await testTreeRemoteTypes(
"basic_org",
mkTestPage({
topOrigin: ORG_ORIGIN,
comRemoteType: expected.com_normal,
orgRemoteType: expected.org_normal,
mozRemoteType: expected.moz_normal,
})
);
// Set the COOP header, and load
await testTreeRemoteTypes(
"com_set_coop",
mkTestPage({
topOrigin: COM_ORIGIN,
topHeaders: { "Cross-Origin-Opener-Policy": "same-origin" },
comRemoteType: expected.com_high,
orgRemoteType: expected.org_normal,
mozRemoteType: expected.moz_normal,
})
);
// Load again after setting the COOP header
await testTreeRemoteTypes(
"com_after_coop",
mkTestPage({
topOrigin: COM_ORIGIN,
comRemoteType: expected.com_high,
orgRemoteType: expected.org_normal,
mozRemoteType: expected.moz_normal,
})
);
// Load again after setting the COOP header, with a .org toplevel
await testTreeRemoteTypes(
"org_after_coop",
mkTestPage({
topOrigin: ORG_ORIGIN,
comRemoteType: expected.com_high,
orgRemoteType: expected.org_normal,
mozRemoteType: expected.moz_normal,
})
);
// Load with both the COOP and COEP headers set.
await testTreeRemoteTypes(
"com_coop_coep",
mkTestPage({
topOrigin: COM_ORIGIN,
topHeaders: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
},
frameHeaders: {
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Resource-Policy": "cross-origin",
},
comRemoteType: expected.com_coop_coep,
orgRemoteType: expected.org_coop_coep,
mozRemoteType: expected.moz_coop_coep,
})
);
}
add_task(async function test_isolate_nothing() {
await SpecialPowers.pushPrefEnv({
set: [
["browser.tabs.remote.separatedMozillaDomains", "mozilla.org"],
[
"fission.webContentIsolationStrategy",
WebContentIsolationStrategy.IsolateNothing,
],
],
});
await do_tests({
com_normal: "web",
org_normal: "web",
moz_normal: "privilegedmozilla",
com_high: "web",
com_coop_coep: "webCOOP+COEP=https://example.com",
org_coop_coep: "webCOOP+COEP=https://example.org",
moz_coop_coep: "privilegedmozilla",
});
});
add_task(async function test_isolate_everything() {
await SpecialPowers.pushPrefEnv({
set: [
["browser.tabs.remote.separatedMozillaDomains", "mozilla.org"],
[
"fission.webContentIsolationStrategy",
WebContentIsolationStrategy.IsolateEverything,
],
],
});
await do_tests({
com_normal: "webIsolated=https://example.com",
org_normal: "webIsolated=https://example.org",
moz_normal: "privilegedmozilla",
com_high: "webIsolated=https://example.com",
com_coop_coep: "webCOOP+COEP=https://example.com",
org_coop_coep: "webCOOP+COEP=https://example.org",
moz_coop_coep: "privilegedmozilla",
});
});
add_task(async function test_isolate_high_value() {
await SpecialPowers.pushPrefEnv({
set: [
["browser.tabs.remote.separatedMozillaDomains", "mozilla.org"],
[
"fission.webContentIsolationStrategy",
WebContentIsolationStrategy.IsolateHighValue,
],
],
});
await do_tests({
com_normal: "web",
org_normal: "web",
moz_normal: "privilegedmozilla",
com_high: "webIsolated=https://example.com",
com_coop_coep: "webCOOP+COEP=https://example.com",
org_coop_coep: "webCOOP+COEP=https://example.org",
moz_coop_coep: "privilegedmozilla",
});
});

Просмотреть файл

@ -4262,6 +4262,22 @@
value: false
mirror: always
# The strategy used to control how sites are isolated into separate processes
# when Fisison is enabled. This pref has no effect if Fission is disabled.
# See the `WebContentIsolationStrategy` enum in `ProcessIsolation.cpp`.
- name: fission.webContentIsolationStrategy
type: uint32_t
value: 1
mirror: always
# Time in seconds before a site loaded with the Cross-Origin-Opener-Policy
# header is no longer considered high-value and isolated in the "highValueCOOP"
# configuration.
- name: fission.highValue.coop.expiration
type: uint32_t
value: 2592000 # 30 days (in seconds)
mirror: always
#---------------------------------------------------------------------------
# Prefs starting with "font."
#---------------------------------------------------------------------------

Просмотреть файл

@ -2022,6 +2022,14 @@ bool DocumentLoadListener::DocShellWillDisplayContent(nsresult aStatus) {
aStatus, mChannel, mLoadStateLoadType, loadingContext->IsTop(),
loadingContext->GetUseErrorPages(), isInitialDocument, nullptr);
if (NS_SUCCEEDED(rv)) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("Skipping process switch, as DocShell will not display content "
"(status: %s) %s",
GetStaticErrorName(aStatus),
mChannelCreationURI->GetSpecOrDefault().get()));
}
// If filtering returned a failure code, then an error page will
// be display for that code, so return true;
return NS_FAILED(rv);

Просмотреть файл

@ -23,8 +23,10 @@
#include "mozilla/DebugOnly.h"
#include "mozilla/InputStreamLengthHelper.h"
#include "mozilla/NullPrincipal.h"
#include "mozilla/PermissionManager.h"
#include "mozilla/Components.h"
#include "mozilla/StaticPrefs_browser.h"
#include "mozilla/StaticPrefs_fission.h"
#include "mozilla/StaticPrefs_security.h"
#include "mozilla/Telemetry.h"
#include "mozilla/Tokenizer.h"
@ -2309,6 +2311,56 @@ nsresult HttpBaseChannel::ProcessCrossOriginResourcePolicyHeader() {
return NS_OK;
}
// Called when a document request responds with a `Cross-Origin-Opener-Policy`
// header to add a `highValueCOOP` permission to the permissions database, and
// make loads of that origin isolated.
static void AddHighValueCOOPPermission(nsIPrincipal* aResultPrincipal) {
RefPtr<PermissionManager> perms = PermissionManager::GetInstance();
if (NS_WARN_IF(!perms)) {
return;
}
// We can't act on non-content principals, so if the load was sandboxed, try
// to use the unsandboxed precursor principal to add the highValueCOOP
// permission.
nsCOMPtr<nsIPrincipal> resultOrPrecursor(aResultPrincipal);
if (!aResultPrincipal->GetIsContentPrincipal()) {
resultOrPrecursor = aResultPrincipal->GetPrecursorPrincipal();
if (!resultOrPrecursor) {
return;
}
}
// Use the site-origin principal as we want to add the permission for the
// entire site, rather than a specific subdomain, as process isolation acts on
// a site granularity.
nsAutoCString siteOrigin;
if (NS_FAILED(resultOrPrecursor->GetSiteOrigin(siteOrigin))) {
return;
}
nsCOMPtr<nsIPrincipal> sitePrincipal =
BasePrincipal::CreateContentPrincipal(siteOrigin);
if (!sitePrincipal || !sitePrincipal->GetIsContentPrincipal()) {
return;
}
MOZ_LOG(dom::gProcessIsolationLog, LogLevel::Verbose,
("Adding HighValue COOP Permission for site '%s'", siteOrigin.get()));
// XXX: Would be nice if we could use `TimeStamp` here, but there's
// unfortunately no convenient way to recover a time in milliseconds since the
// unix epoch from `TimeStamp`.
int64_t expirationTime =
(PR_Now() / PR_USEC_PER_MSEC) +
(int64_t(StaticPrefs::fission_highValue_coop_expiration()) *
PR_MSEC_PER_SEC);
Unused << perms->AddFromPrincipal(
sitePrincipal, mozilla::dom::kHighValueCOOPPermission,
nsIPermissionManager::ALLOW_ACTION, nsIPermissionManager::EXPIRE_TIME,
expirationTime);
}
// See https://gist.github.com/annevk/6f2dd8c79c77123f39797f6bdac43f3e
// This method runs steps 1-4 of the algorithm to compare
// cross-origin-opener policies
@ -2337,6 +2389,8 @@ static bool CompareCrossOriginOpenerPolicies(
// This runs steps 1-5 of the algorithm when navigating a top level document.
// See https://gist.github.com/annevk/6f2dd8c79c77123f39797f6bdac43f3e
nsresult HttpBaseChannel::ComputeCrossOriginOpenerPolicyMismatch() {
MOZ_ASSERT(XRE_IsParentProcess());
StoreHasCrossOriginOpenerPolicyMismatch(false);
if (!StaticPrefs::browser_tabs_remote_useCrossOriginOpenerPolicy()) {
return NS_OK;
@ -2363,6 +2417,10 @@ nsresult HttpBaseChannel::ComputeCrossOriginOpenerPolicyMismatch() {
return NS_OK;
}
nsCOMPtr<nsIPrincipal> resultOrigin;
nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal(
this, getter_AddRefs(resultOrigin));
// Get the policy of the active document, and the policy for the result.
nsILoadInfo::CrossOriginOpenerPolicy documentPolicy = ctx->GetOpenerPolicy();
nsILoadInfo::CrossOriginOpenerPolicy resultPolicy =
@ -2370,6 +2428,11 @@ nsresult HttpBaseChannel::ComputeCrossOriginOpenerPolicyMismatch() {
Unused << ComputeCrossOriginOpenerPolicy(documentPolicy, &resultPolicy);
mComputedCrossOriginOpenerPolicy = resultPolicy;
// Add a permission to mark this site as high-value into the permission DB.
if (resultPolicy != nsILoadInfo::OPENER_POLICY_UNSAFE_NONE) {
AddHighValueCOOPPermission(resultOrigin);
}
// If bc's popup sandboxing flag set is not empty and potentialCOOP is
// non-null, then navigate bc to a network error and abort these steps.
if (resultPolicy != nsILoadInfo::OPENER_POLICY_UNSAFE_NONE &&
@ -2381,17 +2444,15 @@ nsresult HttpBaseChannel::ComputeCrossOriginOpenerPolicyMismatch() {
}
// In xpcshell-tests we don't always have a current window global
if (!ctx->Canonical()->GetCurrentWindowGlobal()) {
RefPtr<mozilla::dom::WindowGlobalParent> currentWindowGlobal =
ctx->Canonical()->GetCurrentWindowGlobal();
if (!currentWindowGlobal) {
return NS_OK;
}
// We use the top window principal as the documentOrigin
nsCOMPtr<nsIPrincipal> documentOrigin =
ctx->Canonical()->GetCurrentWindowGlobal()->DocumentPrincipal();
nsCOMPtr<nsIPrincipal> resultOrigin;
nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal(
this, getter_AddRefs(resultOrigin));
currentWindowGlobal->DocumentPrincipal();
bool compareResult = CompareCrossOriginOpenerPolicies(
documentPolicy, documentOrigin, resultPolicy, resultOrigin);
@ -2434,7 +2495,7 @@ nsresult HttpBaseChannel::ComputeCrossOriginOpenerPolicyMismatch() {
return NS_OK;
}
if (!ctx->Canonical()->GetCurrentWindowGlobal()->IsInitialDocument()) {
if (!currentWindowGlobal->IsInitialDocument()) {
StoreHasCrossOriginOpenerPolicyMismatch(true);
return NS_OK;
}