Files
sousa-gecko/dom/ipc/gtest/ProcessIsolationTest.cpp
T
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

447 lines
19 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 "gtest/gtest.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/ExpandedPrincipal.h"
#include "mozilla/ExtensionPolicyService.h"
#include "mozilla/GenericFactory.h"
#include "mozilla/NullPrincipal.h"
#include "mozilla/StaticPrefs_browser.h"
#include "mozilla/SystemPrincipal.h"
#include "mozilla/dom/ProcessIsolation.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/gtest/MozAssertions.h"
#include "mozilla/gtest/MozHelpers.h"
#include "nsComponentManager.h"
#include "nsIEnterprisePolicies.h"
using namespace mozilla;
using namespace mozilla::dom;
static nsCOMPtr<nsIPrincipal> MakeTestPrincipal(const char* aURI) {
nsCOMPtr<nsIURI> uri;
MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), aURI));
return BasePrincipal::CreateContentPrincipal(uri, {});
}
namespace {
static bool gJitDisabled = false;
struct RemoteTypes {
RemoteType mIsolated;
RemoteType mUnisolated;
};
struct WorkerExpectation {
nsCOMPtr<nsIPrincipal> mPrincipal;
WorkerKind mWorkerKind = WorkerKindShared;
bool mJitDisabled = false;
Result<RemoteTypes, nsresult> mExpected = Err(NS_ERROR_FAILURE);
RemoteType mCurrentRemoteType = RemoteType(RemoteType::Kind::WebContent);
void Check(bool aUseRemoteSubframes) {
nsAutoCString origin;
ASSERT_NS_SUCCEEDED(mPrincipal->GetOrigin(origin));
nsPrintfCString describe(
"origin: %s, workerKind: %s, currentRemoteType: %s, "
"useRemoteSubframes: %d",
origin.get(), mWorkerKind == WorkerKindShared ? "shared" : "service",
mCurrentRemoteType.Stringify().get(), aUseRemoteSubframes);
gJitDisabled = mJitDisabled;
auto result = IsolationOptionsForWorker(
mPrincipal, mWorkerKind, mCurrentRemoteType, aUseRemoteSubframes);
ASSERT_EQ(result.isOk(), mExpected.isOk())
<< "Unexpected status (expected " << (mExpected.isOk() ? "ok" : "err")
<< ") for " << describe;
if (mExpected.isOk()) {
const RemoteType& expected = aUseRemoteSubframes
? mExpected.inspect().mIsolated
: mExpected.inspect().mUnisolated;
EXPECT_TRUE(expected.IsKnown())
<< "invalid remote type expectation in test";
ASSERT_EQ(result.inspect().mRemoteType, expected)
<< "Unexpected remote type (expected " << expected.Stringify()
<< ") for " << describe;
}
}
};
#define MOCK_ENTERPRISE_POLICIES_CID \
{0xaabbc001, 0xdd00, 0x1234, {0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89}}
NS_DEFINE_NAMED_CID(MOCK_ENTERPRISE_POLICIES_CID);
class MockEnterprisePoliciesService final : public nsIEnterprisePolicies {
~MockEnterprisePoliciesService() = default;
public:
NS_DECL_ISUPPORTS
NS_IMETHOD GetStatus(int16_t* aStatus) override {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHOD GetIsEnterprise(bool* aIsEnterprise) override {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHOD IsAllowed(const nsACString&, bool* aRetVal) override {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHOD IsAllowedForURI(const nsACString&, nsIURI*,
bool* aRetVal) override {
*aRetVal = !gJitDisabled;
return NS_OK;
}
NS_IMETHOD GetActivePolicies(JS::MutableHandle<JS::Value>) override {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHOD GetSupportMenu(JS::MutableHandle<JS::Value>) override {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHOD GetExtensionPolicy(const nsACString&,
JS::MutableHandle<JS::Value>) override {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHOD GetExtensionSettings(const nsACString&,
JS::MutableHandle<JS::Value>) override {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHOD MayInstallAddon(JS::Handle<JS::Value>, bool*) override {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHOD IsAddonRequiredByPolicy(const nsACString&, bool*) override {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHOD AllowedInstallSource(nsIURI*, bool*) override {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHOD IsExemptExecutableExtension(const nsACString&, const nsACString&,
bool*) override {
return NS_ERROR_NOT_IMPLEMENTED;
}
};
NS_IMPL_ISUPPORTS(MockEnterprisePoliciesService, nsIEnterprisePolicies)
static nsresult ConstructMockEnterprisePolicies(const nsIID& aIID,
void** aResult) {
RefPtr<MockEnterprisePoliciesService> service =
new MockEnterprisePoliciesService();
return service->QueryInterface(aIID, aResult);
}
StaticRefPtr<nsIFactory> gMockPolicyFactory;
static void RegisterMockPolicyService() {
MOZ_ASSERT(!gMockPolicyFactory);
gMockPolicyFactory =
new mozilla::GenericFactory(ConstructMockEnterprisePolicies);
MOZ_ALWAYS_SUCCEEDS(
nsComponentManagerImpl::gComponentManager->RegisterFactory(
kMOCK_ENTERPRISE_POLICIES_CID, "MockEnterprisePolicies",
"@mozilla.org/enterprisepolicies;1", gMockPolicyFactory));
}
static void UnregisterMockPolicyService() {
MOZ_ASSERT(gMockPolicyFactory);
MOZ_ALWAYS_SUCCEEDS(
nsComponentManagerImpl::gComponentManager->UnregisterFactory(
kMOCK_ENTERPRISE_POLICIES_CID, gMockPolicyFactory));
gMockPolicyFactory = nullptr;
}
} // namespace
// When file URI process separation is disabled (as is the default on
// Android), a file: shared worker is allowed to load in any remote type,
// rather than being rejected when it isn't already in a file: process.
static Result<RemoteTypes, nsresult> FileWorkerOutsideFileProcessExpected(
const RemoteType& aFileRemoteType) {
if (StaticPrefs::browser_tabs_remote_separateFileUriProcess()) {
return Err(NS_ERROR_UNEXPECTED);
}
return RemoteTypes{aFileRemoteType, aFileRemoteType};
}
TEST(ProcessIsolationTest, WorkerOptions)
{
// Forcibly enable the privileged mozilla content process for the duration of
// the test.
MOZ_ALWAYS_SUCCEEDS(Preferences::SetCString(
"browser.tabs.remote.separatedMozillaDomains", "addons.mozilla.org"));
MOZ_ALWAYS_SUCCEEDS(Preferences::SetBool(
"browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", true));
auto cleanup = MakeScopeExit([&] {
MOZ_ALWAYS_SUCCEEDS(
Preferences::ClearUser("browser.tabs.remote.separatedMozillaDomains"));
MOZ_ALWAYS_SUCCEEDS(Preferences::ClearUser(
"browser.tabs.remote.separatePrivilegedMozillaWebContentProcess"));
});
OriginAttributes containerOA;
containerOA.mUserContextId = 1;
nsCOMPtr<nsIPrincipal> systemPrincipal = SystemPrincipal::Get();
nsCOMPtr<nsIPrincipal> nullPrincipal =
NullPrincipal::CreateWithoutOriginAttributes();
nsCOMPtr<nsIPrincipal> nullContainerPrincipal =
NullPrincipal::Create(containerOA);
nsCOMPtr<nsIPrincipal> secureComPrincipal =
MakeTestPrincipal("https://example.com");
nsCOMPtr<nsIPrincipal> secureOrgPrincipal =
MakeTestPrincipal("https://example.org");
nsCOMPtr<nsIPrincipal> insecureOrgPrincipal =
MakeTestPrincipal("http://example.org");
nsCOMPtr<nsIPrincipal> filePrincipal =
MakeTestPrincipal("file:///path/to/dir");
nsCOMPtr<nsIPrincipal> extensionPrincipal =
MakeTestPrincipal("moz-extension://fake-uuid");
nsCOMPtr<nsIPrincipal> privilegedMozillaPrincipal =
MakeTestPrincipal("https://addons.mozilla.org");
nsCOMPtr<nsIPrincipal> expandedPrincipal = ExpandedPrincipal::Create(
nsTArray{secureComPrincipal, extensionPrincipal}, {});
nsCOMPtr<nsIPrincipal> nullSecureComPrecursorPrincipal =
NullPrincipal::CreateWithInheritedAttributes(secureComPrincipal);
RemoteType extensionRemoteType =
ExtensionPolicyService::GetSingleton().UseRemoteExtensions()
? RemoteType(RemoteType::Kind::Extension)
: RemoteType::NotRemote();
RemoteType fileRemoteType =
StaticPrefs::browser_tabs_remote_separateFileUriProcess()
? RemoteType(RemoteType::Kind::File)
: RemoteType(RemoteType::Kind::WebContent);
WorkerExpectation expectations[] = {
// Neither service not shared workers can have expanded principals
{.mPrincipal = expandedPrincipal,
.mWorkerKind = WorkerKindService,
.mExpected = Err(NS_ERROR_UNEXPECTED)},
{.mPrincipal = expandedPrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected = Err(NS_ERROR_UNEXPECTED)},
// Service workers cannot have system or null principals
{.mPrincipal = systemPrincipal,
.mWorkerKind = WorkerKindService,
.mExpected = Err(NS_ERROR_UNEXPECTED)},
{.mPrincipal = nullPrincipal,
.mWorkerKind = WorkerKindService,
.mExpected = Err(NS_ERROR_UNEXPECTED)},
{.mPrincipal = nullContainerPrincipal,
.mWorkerKind = WorkerKindService,
.mExpected = Err(NS_ERROR_UNEXPECTED)},
{.mPrincipal = nullSecureComPrecursorPrincipal,
.mWorkerKind = WorkerKindService,
.mExpected = Err(NS_ERROR_UNEXPECTED)},
// Service workers with various content principals
{.mPrincipal = secureComPrincipal,
.mWorkerKind = WorkerKindService,
.mExpected = RemoteTypes{RemoteType::Parse(
"webServiceWorker=https://example.com"_ns),
RemoteType(RemoteType::Kind::WebContent)}},
{.mPrincipal = secureOrgPrincipal,
.mWorkerKind = WorkerKindService,
.mExpected = RemoteTypes{RemoteType::Parse(
"webServiceWorker=https://example.org"_ns),
RemoteType(RemoteType::Kind::WebContent)}},
{.mPrincipal = extensionPrincipal,
.mWorkerKind = WorkerKindService,
.mExpected = RemoteTypes{extensionRemoteType, extensionRemoteType},
.mCurrentRemoteType = RemoteType(RemoteType::Kind::Extension)},
{.mPrincipal = privilegedMozillaPrincipal,
.mWorkerKind = WorkerKindService,
.mExpected = Err(NS_ERROR_UNEXPECTED)},
{.mPrincipal = privilegedMozillaPrincipal,
.mWorkerKind = WorkerKindService,
.mExpected =
RemoteTypes{RemoteType(RemoteType::Kind::PrivilegedMozilla),
RemoteType(RemoteType::Kind::PrivilegedMozilla)},
.mCurrentRemoteType = RemoteType(RemoteType::Kind::PrivilegedMozilla)},
// Shared Worker loaded from within a webCOOP+COEP remote type process,
// should load elsewhere.
{.mPrincipal = secureComPrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected =
RemoteTypes{RemoteType::Parse("webIsolated=https://example.com"_ns),
RemoteType(RemoteType::Kind::WebContent)},
.mCurrentRemoteType =
RemoteType::Parse("webCOOP+COEP=https://example.com"_ns)},
// Even precursorless null principal should load elsewhere.
{.mPrincipal = nullPrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected = RemoteTypes{RemoteType(RemoteType::Kind::WebContent),
RemoteType(RemoteType::Kind::WebContent)},
.mCurrentRemoteType =
RemoteType::Parse("webCOOP+COEP=https://example.com"_ns)},
{.mPrincipal = nullContainerPrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected = RemoteTypes{RemoteType::Parse("web=^userContextId=1"_ns),
RemoteType::Parse("web=^userContextId=1"_ns)},
.mCurrentRemoteType =
RemoteType::Parse("webCOOP+COEP=https://example.com"_ns)},
// System principal shared workers can only load in the parent process.
{.mPrincipal = systemPrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected =
RemoteTypes{RemoteType::NotRemote(), RemoteType::NotRemote()},
.mCurrentRemoteType = RemoteType::NotRemote()},
{.mPrincipal = systemPrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected = Err(NS_ERROR_UNEXPECTED),
.mCurrentRemoteType = RemoteType(RemoteType::Kind::PrivilegedAbout)},
{.mPrincipal = systemPrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected = Err(NS_ERROR_UNEXPECTED)},
{.mPrincipal = systemPrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected = Err(NS_ERROR_UNEXPECTED)},
// Content principals should load in the appropriate remote types.
{.mPrincipal = secureComPrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected =
RemoteTypes{RemoteType::Parse("webIsolated=https://example.com"_ns),
RemoteType(RemoteType::Kind::WebContent)}},
{.mPrincipal = secureOrgPrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected =
RemoteTypes{RemoteType::Parse("webIsolated=https://example.org"_ns),
RemoteType(RemoteType::Kind::WebContent)}},
{.mPrincipal = insecureOrgPrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected =
RemoteTypes{RemoteType::Parse("webIsolated=http://example.org"_ns),
RemoteType(RemoteType::Kind::WebContent)}},
{.mPrincipal = filePrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected = FileWorkerOutsideFileProcessExpected(fileRemoteType)},
{.mPrincipal = filePrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected = RemoteTypes{fileRemoteType, fileRemoteType},
.mCurrentRemoteType = RemoteType(RemoteType::Kind::File)},
{.mPrincipal = extensionPrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected = RemoteTypes{extensionRemoteType, extensionRemoteType},
.mCurrentRemoteType = RemoteType(RemoteType::Kind::Extension)},
{.mPrincipal = privilegedMozillaPrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected = Err(NS_ERROR_UNEXPECTED)},
{.mPrincipal = privilegedMozillaPrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected =
RemoteTypes{RemoteType(RemoteType::Kind::PrivilegedMozilla),
RemoteType(RemoteType::Kind::PrivilegedMozilla)},
.mCurrentRemoteType = RemoteType(RemoteType::Kind::PrivilegedMozilla)},
{.mPrincipal = nullSecureComPrecursorPrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected =
RemoteTypes{RemoteType::Parse("webIsolated=https://example.com"_ns),
RemoteType(RemoteType::Kind::WebContent)}},
// When the policy service calls for the JIT to be disabled the remote
// type should reflect that.
{.mPrincipal = secureComPrincipal,
.mWorkerKind = WorkerKindShared,
.mJitDisabled = true,
.mExpected =
RemoteTypes{RemoteType::Parse(
"webIsolated=https://example.com^disableJit=1"_ns),
RemoteType::Parse("web=^disableJit=1"_ns)}},
{.mPrincipal = secureComPrincipal,
.mWorkerKind = WorkerKindService,
.mJitDisabled = true,
.mExpected =
RemoteTypes{
RemoteType::Parse(
"webServiceWorker=https://example.com^disableJit=1"_ns),
RemoteType::Parse("web=^disableJit=1"_ns)}},
};
RegisterMockPolicyService();
for (auto& expectation : expectations) {
expectation.Check(true);
expectation.Check(false);
}
UnregisterMockPolicyService();
}
// The file:// URI allowlist is populated from the `capability.policy.*` prefs,
// which in practice are written by the LocalFileLinks enterprise policy so that
// an intranet origin may link to files on a network share. Historically that
// allowlist only made documents load in the file: content process; workers were
// explicitly excluded (see the `!aIsWorker` guard that used to live in
// E10SUtils). Since bug 1850589 dropped that exclusion, a service worker on an
// allowlisted origin resolves to IsolationBehavior::File and is then rejected
// outright by ValidateBehaviorForWorker, because ServiceWorkerPrivate passes
// the shared "web" remote type rather than the file remote type.
TEST(ProcessIsolationTest, FileURIAllowlistedWorkerOptions)
{
MOZ_ALWAYS_SUCCEEDS(Preferences::SetCString("capability.policy.policynames",
"localfilelinks_policy"));
MOZ_ALWAYS_SUCCEEDS(Preferences::SetCString(
"capability.policy.localfilelinks_policy.checkloaduri.enabled",
"allAccess"));
MOZ_ALWAYS_SUCCEEDS(Preferences::SetCString(
"capability.policy.localfilelinks_policy.sites", "https://example.com"));
auto cleanup = MakeScopeExit([&] {
MOZ_ALWAYS_SUCCEEDS(
Preferences::ClearUser("capability.policy.policynames"));
MOZ_ALWAYS_SUCCEEDS(Preferences::ClearUser(
"capability.policy.localfilelinks_policy.checkloaduri.enabled"));
MOZ_ALWAYS_SUCCEEDS(Preferences::ClearUser(
"capability.policy.localfilelinks_policy.sites"));
});
nsCOMPtr<nsIPrincipal> allowlistedPrincipal =
MakeTestPrincipal("https://example.com");
nsCOMPtr<nsIPrincipal> filePrincipal =
MakeTestPrincipal("file:///path/to/dir");
RemoteType fileRemoteType(
StaticPrefs::browser_tabs_remote_separateFileUriProcess()
? RemoteType::Kind::File
: RemoteType::Kind::WebContent);
WorkerExpectation expectations[] = {
// Being in the file:// URI allowlist must not change worker process
// selection: these are the same expectations as for a principal which
// isn't allowlisted at all. The current remote type mirrors what
// ServiceWorkerPrivate::Initialize() passes for a service worker.
{.mPrincipal = allowlistedPrincipal,
.mWorkerKind = WorkerKindService,
.mExpected = RemoteTypes{RemoteType::Parse(
"webServiceWorker=https://example.com"_ns),
RemoteType(RemoteType::Kind::WebContent)},
.mCurrentRemoteType = RemoteType(RemoteType::Kind::WebContent)},
{.mPrincipal = allowlistedPrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected =
RemoteTypes{RemoteType::Parse("webIsolated=https://example.com"_ns),
RemoteType(RemoteType::Kind::WebContent)},
.mCurrentRemoteType = RemoteType(RemoteType::Kind::WebContent)},
// An actual file: principal is still confined to the file process,
// regardless of the allowlist.
{.mPrincipal = filePrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected = FileWorkerOutsideFileProcessExpected(fileRemoteType)},
{.mPrincipal = filePrincipal,
.mWorkerKind = WorkerKindShared,
.mExpected = RemoteTypes{fileRemoteType, fileRemoteType},
.mCurrentRemoteType = RemoteType(RemoteType::Kind::File)},
};
for (auto& expectation : expectations) {
expectation.Check(true);
expectation.Check(false);
}
}