Files
sousa-gecko/dom/ipc/ProcessIsolation.cpp
Nika Layzell c51090b135 Bug 2052614 - Add a structured RemoteType representation. r=ipc-reviewers,necko-reviewers,geckoview-reviewers,extension-reviewers,media-playback-reviewers,webrtc-reviewers,places-reviewers,layout-reviewers,dom-worker-reviewers,ai-platform-reviewers,sandbox-reviewers,janerik,emilio,hiro,kershaw,bwc,asuth,mccr8,alwu,nordzilla,valentin,bobowen
Replace bare remote type strings in C++ process-selection plumbing with
a RemoteType type which stores the parsed kind, isolation URI, and
process selection attributes directly.

This preserves the existing serialized string form for IPC and JS-facing
APIs, while making native callers use explicit predicates and structured
fields instead of manually parsing remote type prefixes and suffixes.

No JS-exposed API for parsing or otherwise interpreting remote types are
currently exposed in this patch. My current expectation is that this
will likely look like a `nsIRemoteType` interface which wraps this
`RemoteType` value type, exposing helpful getters for JS callers.

Differential Revision: https://phabricator.services.mozilla.com/D310442
2026-08-31 23:49:43 +00:00

1649 lines
64 KiB
C++

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/dom/ProcessIsolation.h"
#include "mozilla/AppShutdown.h"
#include "mozilla/Assertions.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/ContentPrincipal.h"
#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 "mozilla/dom/BrowsingContextGroup.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/RemoteType.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/extensions/WebExtensionPolicy.h"
#include "nsAboutProtocolUtils.h"
#include "nsCExternalHandlerService.h"
#include "nsDocShell.h"
#include "nsError.h"
#include "nsEscape.h"
#include "nsIChromeRegistry.h"
#include "nsIEnterprisePolicies.h"
#include "nsIExternalProtocolHandler.h"
#include "nsIExternalProtocolService.h"
#include "nsIHttpChannel.h"
#include "nsIHttpChannelInternal.h"
#include "nsIMIMEInfo.h"
#include "nsIProtocolHandler.h"
#include "nsIXULRuntime.h"
#include "nsNetUtil.h"
#include "nsSHistory.h"
#include "nsServiceManagerUtils.h"
#include "nsURLHelper.h"
namespace mozilla::dom {
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.
*/
struct CommaSeparatedPref {
public:
explicit constexpr CommaSeparatedPref(nsLiteralCString aPrefName)
: mPrefName(aPrefName) {}
void OnChange() {
if (mValues) {
mValues->Clear();
nsAutoCString prefValue;
if (NS_SUCCEEDED(Preferences::GetCString(mPrefName.get(), prefValue))) {
for (const auto& value :
nsCCharSeparatedTokenizer(prefValue, ',').ToRange()) {
mValues->EmplaceBack(value);
}
}
}
}
const nsTArray<nsCString>& Get() {
if (!mValues) {
mValues = new nsTArray<nsCString>;
Preferences::RegisterCallbackAndCall(
[](const char*, void* aData) {
static_cast<CommaSeparatedPref*>(aData)->OnChange();
},
mPrefName, this);
RunOnShutdown([this] {
delete this->mValues;
this->mValues = nullptr;
});
}
return *mValues;
}
auto begin() { return Get().cbegin(); }
auto end() { return Get().cend(); }
private:
nsLiteralCString mPrefName;
nsTArray<nsCString>* MOZ_OWNING_REF mValues = nullptr;
};
CommaSeparatedPref sSeparatedMozillaDomains{
"browser.tabs.remote.separatedMozillaDomains"_ns};
bool AllowJITForSiteOrigin(nsIURI* aSiteOriginURI,
WindowGlobalParent* aParentWindow) {
nsresult rv;
nsCOMPtr<nsIEnterprisePolicies> policyService =
do_GetService("@mozilla.org/enterprisepolicies;1");
if (!policyService) {
return true;
}
nsCOMPtr<nsIURI> topSiteOriginURI = aSiteOriginURI;
// If this is a subframe then use the principal of the top window.
if (aParentWindow) {
nsAutoCString topSiteOriginNoSuffix;
rv = aParentWindow->TopWindowContext()
->DocumentPrincipal()
->GetSiteOriginNoSuffix(topSiteOriginNoSuffix);
NS_ENSURE_SUCCESS(rv, true);
rv = NS_NewURI(getter_AddRefs(topSiteOriginURI), topSiteOriginNoSuffix);
NS_ENSURE_SUCCESS(rv, true);
}
bool isJitAllowed = true;
if (NS_FAILED(policyService->IsAllowedForURI("jit"_ns, topSiteOriginURI,
&isJitAllowed))) {
return true;
}
if (!isJitAllowed) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Debug,
("JIT is disabled for site %s by enterprise policy",
topSiteOriginURI->GetSpecOrDefault().get()));
}
return isJitAllowed;
}
/**
* Certain URIs have special isolation behaviour, and need to be loaded within
* specific process types.
*/
enum class IsolationBehavior {
// This URI loads web content and should be treated as a content load, being
// isolated based on the response principal if enabled.
WebContent,
// Forcibly load in a process with the "web" remote type. This will ignore the
// response principal completely.
// This is generally reserved for internal documents which are loaded in
// content, but not in the privilegedabout content process.
ForceWebRemoteType,
// Load this URI in the privileged about content process.
PrivilegedAbout,
// Load this URI in the extension process.
Extension,
// Load this URI in the file content process.
File,
// Load this URI in the priviliged mozilla content process.
PrivilegedMozilla,
// Load this URI explicitly in the parent process.
Parent,
// Load this URI wherever the browsing context is currently loaded. This is
// generally used for error pages.
Anywhere,
// May only be returned for subframes. Inherits the remote type of the parent
// document which is embedding this document.
Inherit,
// Special case for the `about:reader` URI which should be loaded in the same
// process which would be used for the "url" query parameter.
AboutReader,
// There was a fatal error, and the load should be aborted.
Error,
};
/**
* Returns a static string with the name of the given isolation behaviour. For
* use in logging code.
*/
static const char* IsolationBehaviorName(IsolationBehavior aBehavior) {
switch (aBehavior) {
case IsolationBehavior::WebContent:
return "WebContent";
case IsolationBehavior::ForceWebRemoteType:
return "ForceWebRemoteType";
case IsolationBehavior::PrivilegedAbout:
return "PrivilegedAbout";
case IsolationBehavior::Extension:
return "Extension";
case IsolationBehavior::File:
return "File";
case IsolationBehavior::PrivilegedMozilla:
return "PrivilegedMozilla";
case IsolationBehavior::Parent:
return "Parent";
case IsolationBehavior::Anywhere:
return "Anywhere";
case IsolationBehavior::Inherit:
return "Inherit";
case IsolationBehavior::AboutReader:
return "AboutReader";
case IsolationBehavior::Error:
return "Error";
default:
return "Unknown";
}
}
/**
* Returns a static string with the name of the given worker kind. For use in
* logging code.
*/
static const char* WorkerKindName(WorkerKind aWorkerKind) {
switch (aWorkerKind) {
case WorkerKindDedicated:
return "Dedicated";
case WorkerKindShared:
return "Shared";
case WorkerKindService:
return "Service";
default:
return "Unknown";
}
}
/**
* Check if a given URI has specialized process isolation behaviour, such as
* needing to be loaded within a specific type of content process.
*
* When handling a navigation, this method will be called twice: first with the
* channel's creation URI, and then it will be called with a result principal's
* URI.
*
* `aIsWorker` selects process isolation for a remote worker rather than for a
* document; see the file:// URI allowlist handling below for why the two
* differ.
*/
static IsolationBehavior IsolationBehaviorForURI(nsIURI* aURI, bool aIsSubframe,
bool aForChannelCreationURI,
bool aIsWorker) {
MOZ_ASSERT(NS_IsMainThread());
nsAutoCString scheme;
MOZ_ALWAYS_SUCCEEDS(aURI->GetScheme(scheme));
if (scheme == "chrome"_ns) {
// `chrome://` URIs are always loaded in the parent process.
return IsolationBehavior::Parent;
}
if (scheme == "about"_ns) {
nsAutoCString path;
MOZ_ALWAYS_SUCCEEDS(NS_GetAboutModuleName(aURI, path));
// The `about:blank` and `about:srcdoc` pages are loaded by normal web
// content, and should be allocated processes based on their simple content
// principals.
if (path == "blank"_ns || path == "srcdoc"_ns) {
MOZ_ASSERT(NS_IsContentAccessibleAboutURI(aURI));
return IsolationBehavior::WebContent;
}
MOZ_ASSERT(!NS_IsContentAccessibleAboutURI(aURI));
// If we're loading an `about:reader` URI, perform isolation based on the
// principal of the URI being loaded.
if (path == "reader"_ns && aForChannelCreationURI) {
return IsolationBehavior::AboutReader;
}
// Otherwise, we're going to be loading an about: page. Consult the module.
nsCOMPtr<nsIAboutModule> aboutModule;
if (NS_FAILED(NS_GetAboutModule(aURI, getter_AddRefs(aboutModule))) ||
!aboutModule) {
// If we don't know of an about: module for this load, it's going to end
// up being a network error. Allow the load to finish as normal.
return IsolationBehavior::WebContent;
}
// NOTE: about modules can be implemented in JS, so this may run script, and
// therefore can spuriously fail.
uint32_t flags = 0;
if (NS_FAILED(aboutModule->GetURIFlags(aURI, &flags))) {
NS_WARNING(
"nsIAboutModule::GetURIFlags unexpectedly failed. Abort the load");
return IsolationBehavior::Error;
}
if (flags & nsIAboutModule::URI_MUST_LOAD_IN_EXTENSION_PROCESS) {
return IsolationBehavior::Extension;
}
if (flags & nsIAboutModule::URI_MUST_LOAD_IN_CHILD) {
if (flags & nsIAboutModule::URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS) {
return IsolationBehavior::PrivilegedAbout;
}
return IsolationBehavior::ForceWebRemoteType;
}
if (flags & nsIAboutModule::URI_CAN_LOAD_IN_CHILD) {
return IsolationBehavior::Anywhere;
}
return IsolationBehavior::Parent;
}
// If the test-only `dataUriInDefaultWebProcess` pref is enabled, dump all
// `data:` URIs in a "web" content process, rather than loading them in
// content processes based on their precursor origins.
if (StaticPrefs::browser_tabs_remote_dataUriInDefaultWebProcess() &&
scheme == "data"_ns) {
return IsolationBehavior::ForceWebRemoteType;
}
// Make sure to unwrap nested URIs before we early return for channel creation
// URI. The checks past this point are intended to operate on the principal,
// which has it's origin constructed from the innermost URI.
nsCOMPtr<nsIURI> inner;
if (nsCOMPtr<nsINestedURI> nested = do_QueryInterface(aURI);
nested && NS_SUCCEEDED(nested->GetInnerURI(getter_AddRefs(inner)))) {
return IsolationBehaviorForURI(inner, aIsSubframe, aForChannelCreationURI,
aIsWorker);
}
// If we're doing the initial check based on the channel creation URI, stop
// here as we want to only perform the following checks on the true channel
// result principal.
if (aForChannelCreationURI) {
return IsolationBehavior::WebContent;
}
// Protocols used by Thunderbird to display email messages.
if (scheme == "imap"_ns || scheme == "mailbox"_ns || scheme == "news"_ns ||
scheme == "nntp"_ns || scheme == "snews"_ns || scheme == "x-moz-ews"_ns ||
scheme == "x-moz-graph"_ns) {
return IsolationBehavior::Parent;
}
// There is more handling for extension content processes in the caller, but
// they should load in an extension content process unless we're loading a
// subframe.
if (scheme == "moz-extension"_ns) {
if (aIsSubframe) {
// As a temporary measure, extension iframes must be loaded within the
// same process as their parent document.
return IsolationBehavior::Inherit;
}
return IsolationBehavior::Extension;
}
if (scheme == "file"_ns) {
return IsolationBehavior::File;
}
// Check if the URI is listed as a privileged mozilla content process.
if (scheme == "https"_ns &&
StaticPrefs::
browser_tabs_remote_separatePrivilegedMozillaWebContentProcess()) {
nsAutoCString host;
if (NS_SUCCEEDED(aURI->GetAsciiHost(host))) {
for (const auto& separatedDomain : sSeparatedMozillaDomains) {
// If the domain exactly matches our host, or our host ends with "." +
// separatedDomain, we consider it matching.
if (separatedDomain == host ||
(separatedDomain.Length() < host.Length() &&
host.CharAt(host.Length() - separatedDomain.Length() - 1) == '.' &&
StringEndsWith(host, separatedDomain))) {
return IsolationBehavior::PrivilegedMozilla;
}
}
}
}
// If the domain is allowlisted 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.
//
// This only applies to documents. A worker has no sub-resources of its own to
// render, and forcing it into the file process would additionally mean
// rejecting it outright whenever the requesting process isn't the file
// process (see ValidateBehaviorForWorker). Workers were likewise excluded
// from this rule back when it lived in E10SUtils; see bug 2064648.
if (!aIsWorker) {
nsCOMPtr<nsIScriptSecurityManager> secMan =
nsContentUtils::GetSecurityManager();
bool inFileURIAllowList = false;
if (NS_SUCCEEDED(secMan->InFileURIAllowlist(aURI, &inFileURIAllowList)) &&
inFileURIAllowList) {
return IsolationBehavior::File;
}
}
return IsolationBehavior::WebContent;
}
/**
* Helper method for logging the origin of a principal as a string.
*/
static nsAutoCString OriginString(nsIPrincipal* aPrincipal) {
nsAutoCString origin;
aPrincipal->GetOrigin(origin);
return origin;
}
/**
* Helper method for logging origin attributes as a string.
*/
static nsAutoCString OriginAttributesString(const OriginAttributes& aAttrs) {
nsAutoCString originSuffix;
aAttrs.CreateSuffix(originSuffix);
return originSuffix;
}
/**
* Given an about:reader URI, extract the "url" query parameter, and use it to
* construct a principal which should be used for process selection.
*/
static already_AddRefed<nsIURI> GetAboutReaderURL(nsIURI* aURI) {
#ifdef DEBUG
MOZ_ASSERT(aURI->SchemeIs("about"));
nsAutoCString path;
MOZ_ALWAYS_SUCCEEDS(NS_GetAboutModuleName(aURI, path));
MOZ_ASSERT(path == "reader"_ns);
#endif
nsAutoCString query;
MOZ_ALWAYS_SUCCEEDS(aURI->GetQuery(query));
// Extract the "url" parameter from the `about:reader`'s query parameters,
// and recover a content principal from it.
nsAutoCString readerSpec;
if (URLParams::Extract(query, "url"_ns, readerSpec)) {
nsCOMPtr<nsIURI> readerUri;
if (NS_SUCCEEDED(NS_NewURI(getter_AddRefs(readerUri), readerSpec))) {
return readerUri.forget();
}
}
return nullptr;
}
static already_AddRefed<BasePrincipal> GetAboutReaderURLPrincipal(
nsIURI* aURI, const OriginAttributes& aAttrs) {
if (nsCOMPtr<nsIURI> readerUri = GetAboutReaderURL(aURI)) {
return BasePrincipal::CreateContentPrincipal(readerUri, aAttrs);
}
return nullptr;
}
/**
* Check the Cross-Origin-Opener-Policy of the given channel or ancestor
* BrowsingContext, checking if the response should be cross-origin isolated.
*/
static bool ShouldCrossOriginIsolate(nsIChannel* aChannel,
WindowGlobalParent* aParentWindow) {
nsILoadInfo::CrossOriginOpenerPolicy coop =
nsILoadInfo::OPENER_POLICY_UNSAFE_NONE;
if (aParentWindow) {
coop = aParentWindow->BrowsingContext()->Top()->GetOpenerPolicy();
} else if (nsCOMPtr<nsIHttpChannelInternal> httpChannel =
do_QueryInterface(aChannel)) {
MOZ_ALWAYS_SUCCEEDS(httpChannel->GetCrossOriginOpenerPolicy(&coop));
}
return coop ==
nsILoadInfo::OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP;
}
/**
* Returns `true` if loads for this site should be isolated on a per-site basis.
* If `aTopBC` is nullptr, this is being called to check if a shared or service
* worker should be isolated.
*/
static bool ShouldIsolateSite(nsIPrincipal* aPrincipal,
bool aUseRemoteSubframes) {
// 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 (!aUseRemoteSubframes) {
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;
}
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,
mozilla::dom::kHighValueHasSavedLoginPermission,
mozilla::dom::kHighValueIsLoggedInPermission,
};
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;
}
}
static Result<RemoteType, nsresult> SpecialBehaviorRemoteType(
IsolationBehavior aBehavior, const RemoteType& aCurrentRemoteType,
WindowGlobalParent* aParentWindow, const OriginAttributes& aAttrs) {
switch (aBehavior) {
case IsolationBehavior::ForceWebRemoteType:
return {RemoteType::SharedWeb(aAttrs)};
case IsolationBehavior::PrivilegedAbout:
// The privileged about: content process cannot be disabled, as it
// causes various actors to break.
return {RemoteType(RemoteType::Kind::PrivilegedAbout)};
case IsolationBehavior::Extension:
if (ExtensionPolicyService::GetSingleton().UseRemoteExtensions()) {
return {RemoteType(RemoteType::Kind::Extension)};
}
return {RemoteType(RemoteType::Kind::NotRemote)};
case IsolationBehavior::File:
if (StaticPrefs::browser_tabs_remote_separateFileUriProcess()) {
return {RemoteType(RemoteType::Kind::File)};
}
return {RemoteType::SharedWeb(aAttrs)};
case IsolationBehavior::PrivilegedMozilla:
return {RemoteType(RemoteType::Kind::PrivilegedMozilla)};
case IsolationBehavior::Parent:
return {RemoteType(RemoteType::Kind::NotRemote)};
case IsolationBehavior::Anywhere:
return {aCurrentRemoteType};
case IsolationBehavior::Inherit:
MOZ_DIAGNOSTIC_ASSERT(aParentWindow);
return {aParentWindow->GetRemoteType()};
case IsolationBehavior::Error:
return Err(NS_ERROR_UNEXPECTED);
default:
MOZ_ASSERT_UNREACHABLE();
return Err(NS_ERROR_UNEXPECTED);
}
}
} // namespace
Result<NavigationIsolationOptions, nsresult> IsolationOptionsForNavigation(
CanonicalBrowsingContext* aTopBC, WindowGlobalParent* aParentWindow,
nsIURI* aChannelCreationURI, nsIChannel* aChannel,
const RemoteType& aCurrentRemoteType, bool aHasCOOPMismatch,
bool aForNewTab, uint32_t aLoadStateLoadType,
const Maybe<uint64_t>& aChannelId,
const Maybe<RemoteType>& aRemoteTypeOverride) {
// Get the final principal, used to select which process to load into.
nsCOMPtr<nsIPrincipal> resultPrincipal;
nsresult rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal(
aChannel, getter_AddRefs(resultPrincipal));
if (NS_FAILED(rv)) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Error,
("failed to get channel result principal"));
return Err(rv);
}
MOZ_LOG(
gProcessIsolationLog, LogLevel::Verbose,
("IsolationOptionsForNavigation principal:%s, uri:%s, parentUri:%s",
OriginString(resultPrincipal).get(),
aChannelCreationURI->GetSpecOrDefault().get(),
aParentWindow ? aParentWindow->GetDocumentURI()->GetSpecOrDefault().get()
: ""));
// If we're loading a null principal, we can't easily make a process
// selection decision off ot it. Instead, we'll use our null principal's
// precursor principal to make process selection decisions.
bool isNullPrincipalPrecursor = false;
nsCOMPtr<nsIPrincipal> resultOrPrecursor(resultPrincipal);
if (nsCOMPtr<nsIPrincipal> precursor =
resultOrPrecursor->GetPrecursorPrincipal()) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("using null principal precursor origin %s",
OriginString(precursor).get()));
resultOrPrecursor = precursor;
isNullPrincipalPrecursor = true;
}
NavigationIsolationOptions options;
options.mReplaceBrowsingContext = aHasCOOPMismatch;
options.mShouldCrossOriginIsolate =
ShouldCrossOriginIsolate(aChannel, aParentWindow);
// Check if this load has an explicit remote type override. This is used to
// perform an about:blank load within a specific content process.
if (aRemoteTypeOverride) {
MOZ_DIAGNOSTIC_ASSERT(
NS_IsAboutBlank(aChannelCreationURI),
"Should only have aRemoteTypeOverride for about:blank URIs");
if (NS_WARN_IF(!resultPrincipal->GetIsNullPrincipal())) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Error,
("invalid remote type override on non-null principal"));
return Err(NS_ERROR_DOM_SECURITY_ERR);
}
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("using remote type override (%s) for load",
aRemoteTypeOverride->Stringify().get()));
options.mRemoteType = *aRemoteTypeOverride;
return options;
}
// First, check for any special cases which should be handled using the
// channel creation URI, and handle them.
auto behavior = IsolationBehaviorForURI(aChannelCreationURI, aParentWindow,
/* aForChannelCreationURI */ true,
/* aIsWorker */ false);
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("Channel Creation Isolation Behavior: %s",
IsolationBehaviorName(behavior)));
// In the about:reader special case, we want to fetch the relevant information
// from the URI, an then treat it as a normal web content load.
if (behavior == IsolationBehavior::AboutReader) {
if (RefPtr<BasePrincipal> readerURIPrincipal = GetAboutReaderURLPrincipal(
aChannelCreationURI, resultOrPrecursor->OriginAttributesRef())) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("using about:reader's url origin %s",
OriginString(readerURIPrincipal).get()));
resultOrPrecursor = readerURIPrincipal;
}
behavior = IsolationBehavior::WebContent;
// If loading an about:reader page in a BrowsingContext which shares a
// BrowsingContextGroup with other toplevel documents, replace the
// BrowsingContext to destroy any references.
// With SHIP we can apply this to all about:reader loads.
options.mReplaceBrowsingContext = true;
}
// If we're running in a test which is requesting that system-triggered
// about:blank documents load within the current process, override the
// behaviour for loads which meet the requirements.
if (StaticPrefs::browser_tabs_remote_systemTriggeredAboutBlankAnywhere() &&
NS_IsAboutBlank(aChannelCreationURI)) {
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal() &&
resultOrPrecursor->GetIsNullPrincipal()) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Warning,
("Forcing system-principal triggered about:blank load to "
"complete in the current process"));
behavior = IsolationBehavior::Anywhere;
}
}
#ifdef MOZ_WIDGET_ANDROID
// If we're loading an error page on android, it must complete within the same
// process as the errored page load would complete in due to code expecting
// that behavior. See bug 1673763.
if (aLoadStateLoadType == LOAD_ERROR_PAGE) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("Forcing error page load to complete in the current process"));
behavior = IsolationBehavior::Anywhere;
}
#endif
// If we're loading for a specific extension, we'll need to perform a
// BCG-switching load to get our toplevel extension window in the correct
// BrowsingContextGroup.
if (auto* addonPolicy =
BasePrincipal::Cast(resultOrPrecursor)->AddonPolicy()) {
if (aParentWindow) {
// As a temporary measure, extension iframes must be loaded within the
// same process as their parent document.
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("Loading extension subframe in same process as parent"));
behavior = IsolationBehavior::Inherit;
} else {
MOZ_LOG(
gProcessIsolationLog, LogLevel::Verbose,
("Found extension frame with addon policy. Will use group id %" PRIx64
" (currentId: %" PRIx64 ")",
addonPolicy->GetBrowsingContextGroupId(), aTopBC->Group()->Id()));
behavior = IsolationBehavior::Extension;
if (aTopBC->Group()->Id() != addonPolicy->GetBrowsingContextGroupId()) {
options.mReplaceBrowsingContext = true;
options.mSpecificGroupId = addonPolicy->GetBrowsingContextGroupId();
}
}
}
// Do a second run of `GetIsolationBehavior`, this time using the
// principal's URI to handle additional special cases such as the file and
// privilegedmozilla content process.
if (behavior == IsolationBehavior::WebContent) {
if (resultOrPrecursor->IsSystemPrincipal()) {
// We're loading something with a system principal which isn't caught in
// one of our other edge-cases. If the load started in the parent process,
// and it's safe for it to end in the parent process, we should finish the
// load there.
bool isUIResource = false;
if (aCurrentRemoteType.IsNotRemote() &&
(aChannelCreationURI->SchemeIs("about") ||
(NS_SUCCEEDED(NS_URIChainHasFlags(
aChannelCreationURI, nsIProtocolHandler::URI_IS_UI_RESOURCE,
&isUIResource)) &&
isUIResource))) {
behavior = IsolationBehavior::Parent;
} else {
// In general, we don't want to load documents with a system principal
// in a content process, however we need to in some cases, such as when
// loading blob: URLs created by system code. We can force the load to
// finish in a content process instead.
behavior = IsolationBehavior::ForceWebRemoteType;
}
} else if (nsCOMPtr<nsIURI> principalURI = resultOrPrecursor->GetURI()) {
behavior = IsolationBehaviorForURI(principalURI, aParentWindow,
/* aForChannelCreationURI */ false,
/* aIsWorker */ false);
}
}
// If we're currently loaded in the extension process, and are going to switch
// to some other remote type, make sure we leave the extension's BCG which we
// may have entered earlier to separate extension and non-extension BCGs from
// each-other.
if (!aParentWindow && aCurrentRemoteType.IsExtension() &&
behavior != IsolationBehavior::Extension &&
behavior != IsolationBehavior::Anywhere) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("Forcing BC replacement to leave extension BrowsingContextGroup "
"%" PRIx64 " on navigation",
aTopBC->Group()->Id()));
options.mReplaceBrowsingContext = true;
}
// We don't want to load documents with sandboxed null principals, like
// `data:` URIs, in the parent process, even if they were created by a
// document which would otherwise be loaded in the parent process.
if (behavior == IsolationBehavior::Parent && isNullPrincipalPrecursor) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Debug,
("Ensuring sandboxed null-principal load doesn't occur in the "
"parent process"));
behavior = IsolationBehavior::ForceWebRemoteType;
}
MOZ_LOG(
gProcessIsolationLog, LogLevel::Debug,
("Using IsolationBehavior %s for %s (original uri %s)",
IsolationBehaviorName(behavior), OriginString(resultOrPrecursor).get(),
aChannelCreationURI->GetSpecOrDefault().get()));
// The channel carries the container the load has been switched into, which
// the toplevel BC doesn't have: the load is going to be retargeted into a new
// tab created for that container, so select the process for it.
OriginAttributes originAttributes = aTopBC->OriginAttributesRef();
if (aForNewTab && !aParentWindow &&
resultOrPrecursor->GetIsContentPrincipal()) {
originAttributes.mUserContextId =
resultOrPrecursor->OriginAttributesRef().mUserContextId;
}
// Check if we can put the previous document into the BFCache.
if (mozilla::BFCacheInParent() && nsSHistory::GetMaxTotalViewers() > 0 &&
!aForNewTab && !aParentWindow && !aTopBC->HadOriginalOpener() &&
behavior != IsolationBehavior::Parent &&
(ExtensionPolicyService::GetSingleton().UseRemoteExtensions() ||
behavior != IsolationBehavior::Extension) &&
!aCurrentRemoteType.IsNotRemote() &&
aTopBC->GetHasLoadedNonInitialDocument() &&
(aLoadStateLoadType == LOAD_NORMAL ||
aLoadStateLoadType == LOAD_HISTORY || aLoadStateLoadType == LOAD_LINK ||
aLoadStateLoadType == LOAD_STOP_CONTENT ||
aLoadStateLoadType == LOAD_STOP_CONTENT_AND_REPLACE) &&
(!aTopBC->GetActiveSessionHistoryEntry() ||
aTopBC->GetActiveSessionHistoryEntry()->GetSaveLayoutStateFlag())) {
if (nsCOMPtr<nsIURI> uri = aTopBC->GetCurrentURI()) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("current uri: %s", uri->GetSpecOrDefault().get()));
}
options.mTryUseBFCache =
aTopBC->AllowedInBFCache(aChannelId, aChannelCreationURI);
if (options.mTryUseBFCache) {
options.mReplaceBrowsingContext = true;
options.mActiveSessionHistoryEntry =
aTopBC->GetActiveSessionHistoryEntry();
}
}
// If the load has any special remote type handling, do so at this point.
if (behavior != IsolationBehavior::WebContent) {
options.mRemoteType = MOZ_TRY(SpecialBehaviorRemoteType(
behavior, aCurrentRemoteType, aParentWindow, originAttributes));
if (options.mRemoteType != aCurrentRemoteType &&
(options.mRemoteType.IsNotRemote() ||
aCurrentRemoteType.IsNotRemote())) {
options.mReplaceBrowsingContext = true;
}
MOZ_LOG(
gProcessIsolationLog, LogLevel::Debug,
("Selecting specific remote type (%s) due to a special case isolation "
"behavior %s",
options.mRemoteType.Stringify().get(),
IsolationBehaviorName(behavior)));
return options;
}
// At this point we're definitely not going to be loading in the parent
// process anymore, so we're definitely going to be replacing BrowsingContext
// if we're in the parent process.
if (aCurrentRemoteType.IsNotRemote()) {
MOZ_ASSERT(!aParentWindow);
options.mReplaceBrowsingContext = true;
}
// NOTE: Currently we always perform process isolation based on the
// siteOrigin, not based on the full origin, even if the
// `Origin-Agent-Cluster` header is provided and we are keying DocGroups
// by-origin.
//
// If in the future we want to start keying based on full origin in some
// cases, the logic below will need to be updated to handle this. Note that
// the UseOriginAgentCluster bit may not have been set on the
// BrowsingContextGroup when this check is being evaluated (as it is set after
// process selection, which may cause a BrowsingContextGroup switch).
nsAutoCString siteOriginNoSuffix;
MOZ_TRY(resultOrPrecursor->GetSiteOriginNoSuffix(siteOriginNoSuffix));
nsCOMPtr<nsIURI> siteOriginURI;
MOZ_TRY(NS_NewURI(getter_AddRefs(siteOriginURI), siteOriginNoSuffix));
// Check if we've already loaded a document with the given principal in some
// content process. We want to finish the load in the same process in that
// case.
//
// The exception to that is with extension loads and the system principal,
// where we may have multiple documents with the same principal in different
// processes. Those have been handled above, and will not be reaching here.
//
// If we're doing a replace load or opening a new tab, we won't be staying in
// the same BrowsingContextGroup, so ignore this step.
if (!options.mReplaceBrowsingContext && !aForNewTab) {
// Helper for efficiently determining if a given origin is same-site. This
// will attempt to do a fast equality check, and will only fall back to
// computing the site-origin for content principals.
auto principalIsSameSite = [&](nsIPrincipal* aDocumentPrincipal) -> bool {
// If we're working with a null principal with a precursor, compare
// precursors, as `resultOrPrecursor` has already been stripped to its
// precursor.
nsCOMPtr<nsIPrincipal> documentPrincipal(aDocumentPrincipal);
if (nsCOMPtr<nsIPrincipal> precursor =
documentPrincipal->GetPrecursorPrincipal()) {
documentPrincipal = precursor;
}
// First, attempt to use `Equals` to compare principals, and if that
// fails compare siteOrigins. Only compare siteOrigin for content
// principals, as non-content principals will never have siteOrigin !=
// origin.
nsAutoCString documentSiteOrigin;
return resultOrPrecursor->Equals(documentPrincipal) ||
(documentPrincipal->GetIsContentPrincipal() &&
resultOrPrecursor->GetIsContentPrincipal() &&
NS_SUCCEEDED(documentPrincipal->GetSiteOriginNoSuffix(
documentSiteOrigin)) &&
documentSiteOrigin == siteOriginNoSuffix);
};
// XXX: Consider also checking in-flight process switches to see if any have
// matching principals?
AutoTArray<RefPtr<BrowsingContext>, 8> contexts;
aTopBC->Group()->GetToplevels(contexts);
while (!contexts.IsEmpty()) {
auto bc = contexts.PopLastElement();
for (const auto& wc : bc->GetWindowContexts()) {
WindowGlobalParent* wgp = wc->Canonical();
// Check if this WindowGlobalParent has the given resultPrincipal, and
// if it does, we need to load in that process.
if (!wgp->GetRemoteType().IsNotRemote() &&
principalIsSameSite(wgp->DocumentPrincipal())) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Debug,
("Found existing frame with matching principal "
"(remoteType:(%s), origin:%s)",
wgp->GetRemoteType().Stringify().get(),
OriginString(wgp->DocumentPrincipal()).get()));
options.mRemoteType = wgp->GetRemoteType();
return options;
}
// Also enumerate over this WindowContexts' subframes.
contexts.AppendElements(wc->Children());
}
}
}
options.mRemoteType = RemoteType::SharedWeb(originAttributes);
if (!AllowJITForSiteOrigin(siteOriginURI, aParentWindow)) {
options.mRemoteType = options.mRemoteType.WithDisableJit(true);
}
if (options.mShouldCrossOriginIsolate ||
ShouldIsolateSite(resultOrPrecursor, aTopBC->UseRemoteSubframes())) {
options.mRemoteType = options.mRemoteType.WithSiteOrigin(
siteOriginNoSuffix, options.mShouldCrossOriginIsolate
? RemoteType::Kind::WebCoopCoep
: RemoteType::Kind::WebContent);
}
return options;
}
static bool ValidateBehaviorForWorker(IsolationBehavior aBehavior,
const RemoteType& aCurrentRemoteType) {
if (aCurrentRemoteType.IsNotRemote()) {
return true;
}
switch (aBehavior) {
case IsolationBehavior::Parent:
// Can't load in a parent process from any other process.
return false;
case IsolationBehavior::AboutReader:
case IsolationBehavior::Inherit:
// Not relevant for Workers.
return false;
case IsolationBehavior::WebContent:
case IsolationBehavior::ForceWebRemoteType:
case IsolationBehavior::Anywhere:
return true;
case IsolationBehavior::Extension:
// Extension iframes could be loaded in any process.
return true;
case IsolationBehavior::PrivilegedAbout:
return aCurrentRemoteType.IsPrivilegedAbout();
case IsolationBehavior::File:
return !StaticPrefs::browser_tabs_remote_separateFileUriProcess() ||
aCurrentRemoteType.IsFile();
case IsolationBehavior::PrivilegedMozilla:
return aCurrentRemoteType.IsPrivilegedMozilla();
case IsolationBehavior::Error:
break;
}
return false;
}
Result<WorkerIsolationOptions, nsresult> IsolationOptionsForWorker(
nsIPrincipal* aPrincipal, WorkerKind aWorkerKind,
const RemoteType& aCurrentRemoteType, bool aUseRemoteSubframes) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("IsolationOptionsForWorker principal:%s, kind:%s, current:%s",
OriginString(aPrincipal).get(), WorkerKindName(aWorkerKind),
aCurrentRemoteType.Stringify().get()));
MOZ_ASSERT(NS_IsMainThread());
MOZ_RELEASE_ASSERT(
aWorkerKind == WorkerKindService || aWorkerKind == WorkerKindShared,
"Unexpected remote worker kind");
if (aWorkerKind == WorkerKindService &&
!aPrincipal->GetIsContentPrincipal()) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Warning,
("Rejecting service worker with non-content principal"));
return Err(NS_ERROR_UNEXPECTED);
}
if (aPrincipal->GetIsExpandedPrincipal()) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Warning,
("Rejecting remote worker with expanded principal"));
return Err(NS_ERROR_UNEXPECTED);
}
// In some cases, such as for null principals without precursors, we will want
// to load a shared worker in a process based on the current process. This is
// not done for service workers - process selection for those should function
// the same in all processes.
//
// We only allow the current remote type to be used if it is not a COOP+COEP
// remote type, in order to avoid loading a shared worker in one of these
// processes. Currently process selection for workers occurs before response
// headers are available, so we will never select to load a shared worker in a
// COOP+COEP content process.
RemoteType preferredRemoteType =
RemoteType::SharedWeb(aPrincipal->OriginAttributesRef());
if (aWorkerKind == WorkerKind::WorkerKindShared &&
!aCurrentRemoteType.IsWebCoopCoep()) {
preferredRemoteType = aCurrentRemoteType;
}
WorkerIsolationOptions options;
// If we're loading a null principal, we can't easily make a process
// selection decision off ot it. Instead, we'll use our null principal's
// precursor principal to make process selection decisions.
bool isNullPrincipalPrecursor = false;
nsCOMPtr<nsIPrincipal> resultOrPrecursor(aPrincipal);
if (nsCOMPtr<nsIPrincipal> precursor =
resultOrPrecursor->GetPrecursorPrincipal()) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("using null principal precursor origin %s",
OriginString(precursor).get()));
resultOrPrecursor = precursor;
isNullPrincipalPrecursor = true;
}
IsolationBehavior behavior = IsolationBehavior::WebContent;
if (resultOrPrecursor->GetIsContentPrincipal()) {
nsCOMPtr<nsIURI> uri = resultOrPrecursor->GetURI();
behavior = IsolationBehaviorForURI(uri, /* aIsSubframe */ false,
/* aForChannelCreationURI */ false,
/* aIsWorker */ true);
} else if (resultOrPrecursor->IsSystemPrincipal()) {
MOZ_ASSERT(aWorkerKind == WorkerKindShared);
// Only allow system principal shared workers to load within the parent
// process, and only if that process is responsible for the load.
if (preferredRemoteType.IsNotRemote()) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Debug,
("Loading system principal shared worker in parent process"));
behavior = IsolationBehavior::Parent;
} else {
MOZ_LOG(
gProcessIsolationLog, LogLevel::Warning,
("Cannot load system-principal shared worker in content process"));
return Err(NS_ERROR_UNEXPECTED);
}
} else {
MOZ_ASSERT(resultOrPrecursor->GetIsNullPrincipal());
MOZ_ASSERT(aWorkerKind == WorkerKindShared);
if (preferredRemoteType.IsNotRemote()) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Debug,
("Ensuring precursorless null principal shared worker loads in a "
"content process"));
behavior = IsolationBehavior::ForceWebRemoteType;
} else {
MOZ_LOG(gProcessIsolationLog, LogLevel::Debug,
("Loading precursorless null principal shared worker within "
"current remotetype: (%s)",
preferredRemoteType.Stringify().get()));
behavior = IsolationBehavior::Anywhere;
}
}
if (behavior == IsolationBehavior::Parent && isNullPrincipalPrecursor) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Debug,
("Ensuring sandboxed null-principal shared worker doesn't load in "
"the parent process"));
behavior = IsolationBehavior::ForceWebRemoteType;
}
if (!ValidateBehaviorForWorker(behavior, aCurrentRemoteType)) {
MOZ_LOG(
gProcessIsolationLog, LogLevel::Warning,
("Rejecting invalid worker isolation behavior %s for remote type %s",
IsolationBehaviorName(behavior),
aCurrentRemoteType.Stringify().get()));
return Err(NS_ERROR_FAILURE);
}
if (behavior != IsolationBehavior::WebContent) {
options.mRemoteType = MOZ_TRY(
SpecialBehaviorRemoteType(behavior, preferredRemoteType, nullptr,
resultOrPrecursor->OriginAttributesRef()));
MOZ_LOG(
gProcessIsolationLog, LogLevel::Debug,
("Selecting specific %s worker remote type (%s) due to a special case "
"isolation behavior %s",
WorkerKindName(aWorkerKind), options.mRemoteType.Stringify().get(),
IsolationBehaviorName(behavior)));
return options;
}
nsAutoCString siteOriginNoSuffix;
MOZ_TRY(resultOrPrecursor->GetSiteOriginNoSuffix(siteOriginNoSuffix));
nsCOMPtr<nsIURI> siteOriginURI;
MOZ_TRY(NS_NewURI(getter_AddRefs(siteOriginURI), siteOriginNoSuffix));
options.mRemoteType =
RemoteType::SharedWeb(resultOrPrecursor->OriginAttributesRef());
if (!AllowJITForSiteOrigin(siteOriginURI, nullptr)) {
options.mRemoteType = options.mRemoteType.WithDisableJit(true);
}
// If we should be isolating this site, we can determine the correct fission
// remote type from the principal's site-origin.
if (ShouldIsolateSite(resultOrPrecursor, aUseRemoteSubframes)) {
options.mRemoteType = options.mRemoteType.WithSiteOrigin(
siteOriginNoSuffix, aWorkerKind == WorkerKindService
? RemoteType::Kind::WebServiceWorker
: RemoteType::Kind::WebContent);
}
MOZ_LOG(gProcessIsolationLog, LogLevel::Debug,
("Loading web content %s worker in remote type (%s)",
WorkerKindName(aWorkerKind), options.mRemoteType.Stringify().get()));
return options;
}
// External protocols never directly load web content, so they aren't relevant
// for actual navigation or worker isolation options, but are potentially
// relevant when predicting a remote type for a new tab.
//
// This function is called to check if we have a default web-app handler for a
// scheme in order to perform process prediction based on the handler's URI.
//
// This sort of information is unfortunately buried pretty deep within the
// external protocol handler, so is unfortunately verbose to extract and handle.
static already_AddRefed<nsIURI> MaybeResolveWebAppHandler(nsIURI* aURI) {
MOZ_ASSERT(XRE_IsParentProcess());
nsCOMPtr<nsIIOService> ioSvc = do_GetIOService();
NS_ENSURE_TRUE(ioSvc, nullptr);
nsCOMPtr<nsIExternalProtocolService> extProtService =
do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID);
NS_ENSURE_TRUE(extProtService, nullptr);
// NOTE: We intentionally do not unwrap nested URIs. As external protocols do
// not return content, they are not supported by protocols like view-source,
// and should be treated as normal content.
nsAutoCString scheme;
nsresult rv = aURI->GetScheme(scheme);
NS_ENSURE_SUCCESS(rv, nullptr);
// First, we check if the scheme is internally handled by Gecko. Only schemes
// which are handled externally could be handled by a web handler app.
nsCOMPtr<nsIProtocolHandler> handler;
rv = ioSvc->GetProtocolHandler(scheme.get(), getter_AddRefs(handler));
NS_ENSURE_SUCCESS(rv, nullptr);
nsCOMPtr<nsIExternalProtocolHandler> extHandler = do_QueryInterface(handler);
if (!extHandler) {
return nullptr;
}
// Now that we know the scheme is being handled by the external protocol
// handler, we can get any handler information for the scheme. This will
// unfortunately query the OS, but that appears to be unavoidable.
nsCOMPtr<nsIHandlerInfo> handlerInfo;
rv = extProtService->GetProtocolHandlerInfo(scheme,
getter_AddRefs(handlerInfo));
if (NS_FAILED(rv) || !handlerInfo) {
return nullptr;
}
// If the user is going to be prompted, we don't pre-emptively predict the web
// handler's process, as a different handler could be used. ProcessIsolation
// will select the correct process during navigation.
bool alwaysAskBeforeHandling = false;
rv = handlerInfo->GetAlwaysAskBeforeHandling(&alwaysAskBeforeHandling);
if (NS_FAILED(rv) || alwaysAskBeforeHandling) {
return nullptr;
}
nsCOMPtr<nsIHandlerApp> handlerApp;
rv = handlerInfo->GetPreferredApplicationHandler(getter_AddRefs(handlerApp));
if (NS_FAILED(rv) || !handlerApp) {
return nullptr;
}
nsCOMPtr<nsIWebHandlerApp> webHandlerApp = do_QueryInterface(handlerApp);
if (!webHandlerApp) {
return nullptr;
}
// We've located the web handler app, and can now perform substitution on the
// template to build the final URI to use for process selection.
nsAutoCString uriTemplate;
rv = webHandlerApp->GetUriTemplate(uriTemplate);
NS_ENSURE_SUCCESS(rv, nullptr);
nsAutoCString spec;
rv = aURI->GetSpec(spec);
NS_ENSURE_SUCCESS(rv, nullptr);
nsAutoCString escapedSpec;
bool success = NS_Escape(spec, escapedSpec, url_XAlphas);
NS_ENSURE_TRUE(success, nullptr);
uriTemplate.ReplaceSubstring("%s"_ns, escapedSpec);
nsCOMPtr<nsIURI> newURI;
rv = NS_NewURI(getter_AddRefs(newURI), uriTemplate);
NS_ENSURE_SUCCESS(rv, nullptr);
return newURI.forget();
}
Result<RemoteType, nsresult> PredictRemoteTypeForURI(
nsIURI* aURI, const OriginAttributes& aOriginAttributes,
const RemoteType& aPreferredRemoteType, bool aUseRemoteSubframes) {
MOZ_LOG(
gProcessIsolationLog, LogLevel::Verbose,
("PredictRemoteTypeForURI uri:%s, preferred:%s, oa:%s, "
"useRemoteSubframes:%d",
aURI->GetSpecOrDefault().get(), aPreferredRemoteType.Stringify().get(),
OriginAttributesString(aOriginAttributes).get(), aUseRemoteSubframes));
IsolationBehavior behavior = IsolationBehaviorForURI(
aURI, /* aIsSubframe */ false, /* aForChannelCreationURI */ true,
/* aIsWorker */ false);
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("Base Isolation Behavior: %s", IsolationBehaviorName(behavior)));
// External protocol handlers are generally not relevant for navigation or
// worker loads, but are relevant pre-load when predicting a remote type.
// Attempt to resolve the URI for web-app protocol handlers before proceeding.
nsCOMPtr<nsIURI> uri = aURI;
if (nsCOMPtr<nsIURI> webAppHandlerURI = MaybeResolveWebAppHandler(uri)) {
uri = webAppHandlerURI;
behavior = IsolationBehaviorForURI(uri, /* aIsSubframe */ false,
/* aForChannelCreationURI */ true,
/* aIsWorker */ false);
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("Resolved WebAppHandler uri:%s isolationBehavior:%s",
uri->GetSpecOrDefault().get(), IsolationBehaviorName(behavior)));
}
// Allow javascript: and about:{blank,srcdoc} to load anywhere, as we don't
// have precursor principal information when predicting remote types.
if (uri->SchemeIs("javascript") ||
(uri->SchemeIs("about") && NS_IsContentAccessibleAboutURI(uri))) {
behavior = IsolationBehavior::Anywhere;
}
// Note that this may resolve a non-content,non-null principal for a blob URI.
nsCOMPtr<nsIPrincipal> principal =
BasePrincipal::CreateContentPrincipal(uri, aOriginAttributes);
if (behavior == IsolationBehavior::AboutReader) {
if (nsCOMPtr<nsIPrincipal> readerURIPrincipal =
GetAboutReaderURLPrincipal(uri, aOriginAttributes)) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("using about:reader's url origin %s",
OriginString(readerURIPrincipal).get()));
principal = readerURIPrincipal;
}
behavior = IsolationBehavior::WebContent;
}
// Perform a second pass with GetIsolationBehavior now using the principal
// URI, and after having resolved things like about:reader URIs.
if (behavior == IsolationBehavior::WebContent) {
if (principal->IsSystemPrincipal()) {
// This should only be possible for a blob URI at this point. Force it to
// a web content process.
MOZ_ASSERT(uri->SchemeIs("blob"),
"unexpected non-blob URI with system principal");
behavior = IsolationBehavior::ForceWebRemoteType;
} else if (nsCOMPtr<nsIURI> principalURI = principal->GetURI()) {
behavior = IsolationBehaviorForURI(principalURI, /* aIsSubframe */ false,
/* aForChannelCreationURI */ false,
/* aIsWorker */ false);
}
}
MOZ_LOG(gProcessIsolationLog, LogLevel::Debug,
("Predicting IsolationBehavior %s for %s (principal %s)",
IsolationBehaviorName(behavior), uri->GetSpecOrDefault().get(),
OriginString(principal).get()));
// If we have a special behaviour RemoteType, return it.
if (behavior != IsolationBehavior::WebContent) {
RemoteType remoteType = MOZ_TRY(SpecialBehaviorRemoteType(
behavior, aPreferredRemoteType, nullptr, aOriginAttributes));
MOZ_LOG(gProcessIsolationLog, LogLevel::Debug,
("Predicting specific remote type (%s) due to a special case "
"isolation behavior %s",
remoteType.Stringify().get(), IsolationBehaviorName(behavior)));
return remoteType;
}
// We're dealing with a web remote type, get the site origin and origin suffix
// to build a remoteType from.
nsAutoCString siteOriginNoSuffix;
MOZ_TRY(principal->GetSiteOriginNoSuffix(siteOriginNoSuffix));
nsCOMPtr<nsIURI> siteOriginURI;
MOZ_TRY(NS_NewURI(getter_AddRefs(siteOriginURI), siteOriginNoSuffix));
RemoteType remoteType = RemoteType::SharedWeb(aOriginAttributes);
if (!AllowJITForSiteOrigin(siteOriginURI, nullptr)) {
remoteType = remoteType.WithDisableJit(true);
}
// The only situation we'll return a coop+coep remote type is if the preferred
// remote type would perfectly match. Check if that is the case.
if (aPreferredRemoteType.IsWebCoopCoep()) {
RemoteType coopCoepRemoteType = remoteType.WithSiteOrigin(
siteOriginNoSuffix, RemoteType::Kind::WebCoopCoep);
if (coopCoepRemoteType == aPreferredRemoteType) {
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("Predicting preferred COOP+COEP remote type (%s) due to "
"compatible site-origin %s",
coopCoepRemoteType.Stringify().get(),
OriginString(principal).get()));
return coopCoepRemoteType;
}
}
if (ShouldIsolateSite(principal, aUseRemoteSubframes)) {
remoteType = remoteType.WithSiteOrigin(siteOriginNoSuffix);
}
MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
("Predicting web remote type (%s)", remoteType.Stringify().get()));
return remoteType;
}
void AddHighValuePermission(nsIPrincipal* aResultPrincipal,
const nsACString& aPermissionType) {
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 highValue 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 %s Permission for site '%s'",
PromiseFlatCString(aPermissionType).get(), siteOrigin.get()));
uint32_t expiration = 0;
if (aPermissionType.Equals(mozilla::dom::kHighValueCOOPPermission)) {
expiration = StaticPrefs::fission_highValue_coop_expiration();
} else if (aPermissionType.Equals(
mozilla::dom::kHighValueHasSavedLoginPermission) ||
aPermissionType.Equals(
mozilla::dom::kHighValueIsLoggedInPermission)) {
expiration = StaticPrefs::fission_highValue_login_expiration();
} else {
MOZ_ASSERT_UNREACHABLE("Unknown permission type");
}
// 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(expiration) * PR_MSEC_PER_SEC);
(void)perms->AddFromPrincipal(
sitePrincipal, aPermissionType, nsIPermissionManager::ALLOW_ACTION,
nsIPermissionManager::EXPIRE_TIME, expirationTime);
}
void AddHighValuePermission(const nsACString& aOrigin,
const nsACString& aPermissionType) {
nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
nsCOMPtr<nsIPrincipal> principal;
nsresult rv =
ssm->CreateContentPrincipalFromOrigin(aOrigin, getter_AddRefs(principal));
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
AddHighValuePermission(principal, aPermissionType);
}
bool IsIsolateHighValueSiteEnabled() {
return mozilla::FissionAutostart() &&
WebContentIsolationStrategy(
StaticPrefs::fission_webContentIsolationStrategy()) ==
WebContentIsolationStrategy::IsolateHighValue;
}
bool ValidatePrincipalCouldPotentiallyBeLoadedBy(
nsIPrincipal* aPrincipal, const RemoteType& aRemoteType,
const EnumSet<ValidatePrincipalOptions>& aOptions,
FunctionRef<bool(nsIPrincipal*)> aIsPrincipalLoaded) {
#ifdef DEBUG
if (!aIsPrincipalLoaded) {
MOZ_ASSERT(
aOptions.contains(ValidatePrincipalOptions::AllowNotLoadedOrigin),
"`AllowNotLoadedOrigin` is required if calling "
"ValidatePrincipalCouldPotentiallyBeLoadedBy directly");
MOZ_ASSERT(
!aOptions.contains(ValidatePrincipalOptions::AllowSystemIfLoaded),
"`AllowSystemIfLoaded` is invalid if calling "
"ValidatePrincipalCouldPotentiallyBeLoadedBy directly");
}
#endif
// Don't bother validating principals from the parent process.
if (aRemoteType.IsNotRemote()) {
return true;
}
// If there is no principal, only allow it if AllowNullPtr is specified.
if (!aPrincipal) {
return aOptions.contains(ValidatePrincipalOptions::AllowNullPtr);
}
// We currently do not reliably track relationships between specific null
// principals and content processes, so we can not validate much here.
if (aPrincipal->GetIsNullPrincipal()) {
return true;
}
// If we have a system principal, only allow it when explicitly requested.
if (aPrincipal->IsSystemPrincipal()) {
return aOptions.contains(ValidatePrincipalOptions::AlwaysAllowSystem) ||
(aOptions.contains(ValidatePrincipalOptions::AllowSystemIfLoaded) &&
aIsPrincipalLoaded(aPrincipal));
}
// Performing checks against the remote type requires the IOService and
// ThirdPartyService to be available, check we're not late in shutdown.
if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownFinal)) {
return true;
}
// Only allow expanded principals if AllowExpanded is passed. Each
// sub-principal will be validated independently.
if (aPrincipal->GetIsExpandedPrincipal()) {
if (!aOptions.contains(ValidatePrincipalOptions::AllowExpanded)) {
return false;
}
// FIXME: There are more constraints on expanded principals in-practice,
// such as the structure of extension expanded principals. This may need
// to be investigated more in the future.
nsCOMPtr<nsIExpandedPrincipal> expandedPrincipal =
do_QueryInterface(aPrincipal);
const auto& allowList = expandedPrincipal->AllowList();
for (const auto& innerPrincipal : allowList) {
if (!ValidatePrincipalCouldPotentiallyBeLoadedBy(
innerPrincipal, aRemoteType, aOptions, aIsPrincipalLoaded)) {
return false;
}
}
return true;
}
// At this point we know we're working with a content principal.
MOZ_ASSERT(aPrincipal->GetIsContentPrincipal());
nsAutoCString originNoSuffix;
MOZ_ALWAYS_SUCCEEDS(aPrincipal->GetOriginNoSuffix(originNoSuffix));
// NOTE: We intentionally do scheme checks against the origin here, rather
// than the principal's URI. This is because nested URIs like `view-source:`
// are preserved in the principal, but do not impact the origin.
nsAutoCString originScheme;
MOZ_ALWAYS_SUCCEEDS(net_ExtractURLScheme(originNoSuffix, originScheme));
// We never load a chrome:// principal within a content process.
if (originScheme == "chrome"_ns) {
return false;
}
// We can load a `resource://` URI in any process without the parent process
// being involved. This usually comes up due to pdf.js and the JSON viewer.
// See bug 1686200.
if (originScheme == "resource"_ns) {
return true;
}
// Web content can contain extension content frames and contain extension
// content scripts, so any content process may send us an extension's
// principal.
// NOTE: We don't check AddonPolicy here, as that can disappear if the add-on
// is disabled or uninstalled. As this is a lax check, looking at the scheme
// should be sufficient.
if (originScheme == "moz-extension"_ns) {
return true;
}
// All other content principal schemes are always loaded via. the parent
// process, so we can early-return if `aIsPrincipalLoaded` returns false.
if (!aOptions.contains(ValidatePrincipalOptions::AllowNotLoadedOrigin) &&
!aIsPrincipalLoaded(aPrincipal)) {
return false;
}
// A URI with a file:// scheme can never load in a non-file content process
// due to sandboxing.
if (originScheme == "file"_ns) {
// If we don't support a separate 'file' process, then we can return here.
if (!StaticPrefs::browser_tabs_remote_separateFileUriProcess()) {
return true;
}
return aRemoteType.IsFile();
}
if (originScheme == "about"_ns) {
nsCOMPtr<nsIURI> aboutURI;
if (NS_FAILED(NS_NewURI(getter_AddRefs(aboutURI), originNoSuffix))) {
MOZ_DIAGNOSTIC_ASSERT(false, "The originNoSuffix isn't a valid URI?");
return false;
}
MOZ_ASSERT(aboutURI->SchemeIs("about"));
// We cannot validate about module flags off-main-thread, as about modules
// are not threadsafe, and can be implemented in JS.
if (!NS_IsMainThread()) {
return true;
}
// NOTE: The logic for about URIs is somewhat complex, so we lean on
// IsolationBehaviorForURI to ensure it matches.
switch (IsolationBehaviorForURI(aboutURI, /* aIsSubframe */ false,
/* aForChannelCreationURI */ true,
/* aIsWorker */ false)) {
case IsolationBehavior::Parent:
return false;
case IsolationBehavior::Anywhere:
return true;
case IsolationBehavior::AboutReader:
// Allow about:reader to load anywhere, as it is process-allocated based
// on the content it displays, and which content is being displayed is
// unfortunately not part of the principal.
return true;
case IsolationBehavior::Extension:
return aRemoteType.IsExtension();
case IsolationBehavior::PrivilegedAbout:
return aRemoteType.IsPrivilegedAbout();
case IsolationBehavior::ForceWebRemoteType:
return aRemoteType.IsSharedWeb();
case IsolationBehavior::WebContent:
case IsolationBehavior::Error:
// NOTE: We can encounter races around about: pages being unregistered.
// To avoid false positives, we fail open if no about module is present.
return true;
default:
MOZ_CRASH("Unexpected IsolationBehaviorForURI for about: URI");
return false;
}
}
// Only validate webIsolated and webServiceWorker remote types for now. This
// should be expanded in the future.
if (!aRemoteType.IsIsolatedWeb()) {
return true;
}
// COOP+COEP processes might have cross-site iframes without remote subframes.
//
// HACK: Unfortunately, we can't easily check useRemoteSubframes here, but we
// shouldn't be loading any webCOOP+COEP windows without useRemoteSubframes if
// Fission is enabled.
if (aRemoteType.IsWebCoopCoep() && !mozilla::FissionAutostart()) {
return true;
}
// NOTE: Currently every webIsolated remote type is site-origin keyed, meaning
// we can unconditionally compare site origin to the origin from the
// remoteType. If this changes in the future, this logic will need to be
// updated to reflect that.
nsAutoCString siteOriginNoSuffix;
if (NS_FAILED(aPrincipal->GetSiteOriginNoSuffix(siteOriginNoSuffix))) {
MOZ_ASSERT_UNREACHABLE("Failed when not late in shutdown?");
return false;
}
return aRemoteType.OriginNoSuffix() == siteOriginNoSuffix;
}
} // namespace mozilla::dom