diff options
Diffstat (limited to 'dom/ipc/ProcessIsolation.cpp')
-rw-r--r-- | dom/ipc/ProcessIsolation.cpp | 1139 |
1 files changed, 1139 insertions, 0 deletions
diff --git a/dom/ipc/ProcessIsolation.cpp b/dom/ipc/ProcessIsolation.cpp new file mode 100644 index 0000000000..f0c6dd8863 --- /dev/null +++ b/dom/ipc/ProcessIsolation.cpp @@ -0,0 +1,1139 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 : */ +/* 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/Assertions.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 "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 "nsAboutProtocolUtils.h" +#include "nsDocShell.h" +#include "nsError.h" +#include "nsIChromeRegistry.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIProtocolHandler.h" +#include "nsIXULRuntime.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsSHistory.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}; + +/** + * 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. + */ +static IsolationBehavior IsolationBehaviorForURI(nsIURI* aURI, bool aIsSubframe, + bool aForChannelCreationURI) { + nsAutoCString scheme; + MOZ_ALWAYS_SUCCEEDS(aURI->GetScheme(scheme)); + + if (scheme == "chrome"_ns) { + // `chrome://` URIs are always loaded in the parent process, unless they + // have opted in to loading in a content process. This is currently only + // done in tests. + // + // FIXME: These flags should be removed from `chrome` URIs at some point. + nsCOMPtr<nsIXULChromeRegistry> chromeReg = + do_GetService("@mozilla.org/chrome/chrome-registry;1"); + bool mustLoadRemotely = false; + if (NS_SUCCEEDED(chromeReg->MustLoadURLRemotely(aURI, &mustLoadRemotely)) && + mustLoadRemotely) { + return IsolationBehavior::ForceWebRemoteType; + } + bool canLoadRemotely = false; + if (NS_SUCCEEDED(chromeReg->CanLoadURLRemotely(aURI, &canLoadRemotely)) && + canLoadRemotely) { + return IsolationBehavior::Anywhere; + } + 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); + } + + // 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) { + 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; + } + } + } + } + + 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; +} + +/** + * Trim the OriginAttributes from aPrincipal, and use it to create a + * OriginSuffix string appropriate to use within a remoteType string. + */ +static nsAutoCString OriginSuffixForRemoteType(nsIPrincipal* aPrincipal) { + nsAutoCString originSuffix; + OriginAttributes attrs = aPrincipal->OriginAttributesRef(); + attrs.StripAttributes(OriginAttributes::STRIP_FIRST_PARTY_DOMAIN | + OriginAttributes::STRIP_PARITION_KEY); + attrs.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<BasePrincipal> GetAboutReaderURLPrincipal( + nsIURI* aURI, const OriginAttributes& aAttrs) { +#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. + nsAutoString readerSpec; + if (URLParams::Extract(query, u"url"_ns, readerSpec)) { + nsCOMPtr<nsIURI> readerUri; + if (NS_SUCCEEDED(NS_NewURI(getter_AddRefs(readerUri), readerSpec))) { + return BasePrincipal::CreateContentPrincipal(readerUri, aAttrs); + } + } + return nullptr; +} + +/** + * 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<nsCString, nsresult> SpecialBehaviorRemoteType( + IsolationBehavior aBehavior, const nsACString& aCurrentRemoteType, + WindowGlobalParent* aParentWindow) { + switch (aBehavior) { + case IsolationBehavior::ForceWebRemoteType: + return {WEB_REMOTE_TYPE}; + case IsolationBehavior::PrivilegedAbout: + // The privileged about: content process cannot be disabled, as it + // causes various actors to break. + return {PRIVILEGEDABOUT_REMOTE_TYPE}; + case IsolationBehavior::Extension: + if (ExtensionPolicyService::GetSingleton().UseRemoteExtensions()) { + return {EXTENSION_REMOTE_TYPE}; + } + return {NOT_REMOTE_TYPE}; + case IsolationBehavior::File: + if (StaticPrefs::browser_tabs_remote_separateFileUriProcess()) { + return {FILE_REMOTE_TYPE}; + } + return {WEB_REMOTE_TYPE}; + case IsolationBehavior::PrivilegedMozilla: + return {PRIVILEGEDMOZILLA_REMOTE_TYPE}; + case IsolationBehavior::Parent: + return {NOT_REMOTE_TYPE}; + case IsolationBehavior::Anywhere: + return {nsCString(aCurrentRemoteType)}; + case IsolationBehavior::Inherit: + MOZ_DIAGNOSTIC_ASSERT(aParentWindow); + return {nsCString(aParentWindow->GetRemoteType())}; + + case IsolationBehavior::Error: + return Err(NS_ERROR_UNEXPECTED); + + default: + MOZ_ASSERT_UNREACHABLE(); + return Err(NS_ERROR_UNEXPECTED); + } +} + +enum class WebProcessType { + Web, + WebIsolated, + WebCoopCoep, +}; + +} // namespace + +Result<NavigationIsolationOptions, nsresult> IsolationOptionsForNavigation( + CanonicalBrowsingContext* aTopBC, WindowGlobalParent* aParentWindow, + nsIURI* aChannelCreationURI, nsIChannel* aChannel, + const nsACString& aCurrentRemoteType, bool aHasCOOPMismatch, + bool aForNewTab, uint32_t aLoadStateLoadType, + const Maybe<uint64_t>& aChannelId, + const Maybe<nsCString>& 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; + + // 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->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); + 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, but otherwise + // do it at least where there are opener/group relationships. + if (mozilla::SessionHistoryInParent() || + aTopBC->Group()->Toplevels().Length() > 1) { + 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.IsEmpty() && + (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); + } + } + + // 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 == EXTENSION_REMOTE_TYPE && + 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())); + + // 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.IsEmpty() && + 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) { + MOZ_TRY_VAR( + options.mRemoteType, + SpecialBehaviorRemoteType(behavior, aCurrentRemoteType, aParentWindow)); + + if (options.mRemoteType != aCurrentRemoteType && + (options.mRemoteType.IsEmpty() || aCurrentRemoteType.IsEmpty())) { + options.mReplaceBrowsingContext = true; + } + + MOZ_LOG( + gProcessIsolationLog, LogLevel::Debug, + ("Selecting specific remote type (%s) due to a special case isolation " + "behavior %s", + options.mRemoteType.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.IsEmpty()) { + MOZ_ASSERT(!aParentWindow); + options.mReplaceBrowsingContext = true; + } + + nsAutoCString siteOriginNoSuffix; + MOZ_TRY(resultOrPrecursor->GetSiteOriginNoSuffix(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().IsEmpty() && + principalIsSameSite(wgp->DocumentPrincipal())) { + MOZ_LOG(gProcessIsolationLog, LogLevel::Debug, + ("Found existing frame with matching principal " + "(remoteType:(%s), origin:%s)", + PromiseFlatCString(wgp->GetRemoteType()).get(), + OriginString(wgp->DocumentPrincipal()).get())); + options.mRemoteType = wgp->GetRemoteType(); + return options; + } + + // Also enumerate over this WindowContexts' subframes. + contexts.AppendElements(wc->Children()); + } + } + } + + nsAutoCString originSuffix = OriginSuffixForRemoteType(resultOrPrecursor); + + WebProcessType webProcessType = WebProcessType::Web; + if (ShouldIsolateSite(resultOrPrecursor, aTopBC->UseRemoteSubframes())) { + webProcessType = WebProcessType::WebIsolated; + } + + // Check if we should be loading in a webCOOP+COEP remote type due to our COOP + // status. + nsILoadInfo::CrossOriginOpenerPolicy coop = + nsILoadInfo::OPENER_POLICY_UNSAFE_NONE; + if (aParentWindow) { + coop = aTopBC->GetOpenerPolicy(); + } else if (nsCOMPtr<nsIHttpChannelInternal> httpChannel = + do_QueryInterface(aChannel)) { + MOZ_ALWAYS_SUCCEEDS(httpChannel->GetCrossOriginOpenerPolicy(&coop)); + } + if (coop == + nsILoadInfo::OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP) { + webProcessType = WebProcessType::WebCoopCoep; + + // If we're changing BrowsingContext, and are going to end up within a + // webCOOP+COEP group, ensure we use a cross-origin isolated BCG ID. + if (options.mReplaceBrowsingContext) { + MOZ_ASSERT(!options.mSpecificGroupId, + "overriding previously-specified BCG ID"); + options.mSpecificGroupId = BrowsingContextGroup::CreateId( + /* aPotentiallyCrossOriginIsolated */ true); + } + } + + switch (webProcessType) { + case WebProcessType::Web: + options.mRemoteType = WEB_REMOTE_TYPE; + break; + case WebProcessType::WebIsolated: + options.mRemoteType = + FISSION_WEB_REMOTE_TYPE "="_ns + siteOriginNoSuffix + originSuffix; + break; + case WebProcessType::WebCoopCoep: + options.mRemoteType = + WITH_COOP_COEP_REMOTE_TYPE "="_ns + siteOriginNoSuffix + originSuffix; + break; + } + return options; +} + +Result<WorkerIsolationOptions, nsresult> IsolationOptionsForWorker( + nsIPrincipal* aPrincipal, WorkerKind aWorkerKind, + const nsACString& aCurrentRemoteType, bool aUseRemoteSubframes) { + MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose, + ("IsolationOptionsForWorker principal:%s, kind:%s, current:%s", + OriginString(aPrincipal).get(), WorkerKindName(aWorkerKind), + PromiseFlatCString(aCurrentRemoteType).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. + nsCString preferredRemoteType = DEFAULT_REMOTE_TYPE; + if (aWorkerKind == WorkerKind::WorkerKindShared && + !StringBeginsWith(aCurrentRemoteType, + WITH_COOP_COEP_REMOTE_TYPE_PREFIX)) { + 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); + } else if (resultOrPrecursor->IsSystemPrincipal()) { + MOZ_ASSERT(aWorkerKind == WorkerKindShared); + + // Allow system principal shared workers to load within either the + // parent process or privilegedabout process, depending on the + // responsible process. + if (preferredRemoteType == NOT_REMOTE_TYPE) { + MOZ_LOG(gProcessIsolationLog, LogLevel::Debug, + ("Loading system principal shared worker in parent process")); + behavior = IsolationBehavior::Parent; + } else if (preferredRemoteType == PRIVILEGEDABOUT_REMOTE_TYPE) { + MOZ_LOG(gProcessIsolationLog, LogLevel::Debug, + ("Loading system principal shared worker in privilegedabout " + "process")); + behavior = IsolationBehavior::PrivilegedAbout; + } else { + MOZ_LOG(gProcessIsolationLog, LogLevel::Warning, + ("Cannot load system-principal shared worker in " + "non-privilegedabout content process")); + return Err(NS_ERROR_UNEXPECTED); + } + } else { + MOZ_ASSERT(resultOrPrecursor->GetIsNullPrincipal()); + MOZ_ASSERT(aWorkerKind == WorkerKindShared); + + if (preferredRemoteType == NOT_REMOTE_TYPE) { + 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.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 (behavior != IsolationBehavior::WebContent) { + MOZ_TRY_VAR( + options.mRemoteType, + SpecialBehaviorRemoteType(behavior, preferredRemoteType, nullptr)); + + MOZ_LOG( + gProcessIsolationLog, LogLevel::Debug, + ("Selecting specific %s worker remote type (%s) due to a special case " + "isolation behavior %s", + WorkerKindName(aWorkerKind), options.mRemoteType.get(), + IsolationBehaviorName(behavior))); + return options; + } + + // 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)) { + nsAutoCString siteOriginNoSuffix; + MOZ_TRY(resultOrPrecursor->GetSiteOriginNoSuffix(siteOriginNoSuffix)); + nsAutoCString originSuffix = OriginSuffixForRemoteType(resultOrPrecursor); + + nsCString prefix = aWorkerKind == WorkerKindService + ? SERVICEWORKER_REMOTE_TYPE + : FISSION_WEB_REMOTE_TYPE; + options.mRemoteType = prefix + "="_ns + siteOriginNoSuffix + originSuffix; + + MOZ_LOG(gProcessIsolationLog, LogLevel::Debug, + ("Isolating web content %s worker in remote type (%s)", + WorkerKindName(aWorkerKind), options.mRemoteType.get())); + } else { + options.mRemoteType = WEB_REMOTE_TYPE; + + MOZ_LOG(gProcessIsolationLog, LogLevel::Debug, + ("Loading web content %s worker in shared web remote type", + WorkerKindName(aWorkerKind))); + } + return options; +} + +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'", aPermissionType.BeginReading(), + 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); + Unused << 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; +} + +} // namespace mozilla::dom |