diff options
Diffstat (limited to 'security/sandbox/common/test')
-rw-r--r-- | security/sandbox/common/test/PSandboxTesting.ipdl | 21 | ||||
-rw-r--r-- | security/sandbox/common/test/SandboxTest.cpp | 362 | ||||
-rw-r--r-- | security/sandbox/common/test/SandboxTest.h | 44 | ||||
-rw-r--r-- | security/sandbox/common/test/SandboxTestingChild.cpp | 191 | ||||
-rw-r--r-- | security/sandbox/common/test/SandboxTestingChild.h | 81 | ||||
-rw-r--r-- | security/sandbox/common/test/SandboxTestingChildTests.h | 824 | ||||
-rw-r--r-- | security/sandbox/common/test/SandboxTestingParent.cpp | 126 | ||||
-rw-r--r-- | security/sandbox/common/test/SandboxTestingParent.h | 52 | ||||
-rw-r--r-- | security/sandbox/common/test/SandboxTestingThread.h | 53 | ||||
-rw-r--r-- | security/sandbox/common/test/mozISandboxTest.idl | 28 |
10 files changed, 1782 insertions, 0 deletions
diff --git a/security/sandbox/common/test/PSandboxTesting.ipdl b/security/sandbox/common/test/PSandboxTesting.ipdl new file mode 100644 index 0000000000..cd3d0db3f2 --- /dev/null +++ b/security/sandbox/common/test/PSandboxTesting.ipdl @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 https://mozilla.org/MPL/2.0/. */ + +namespace mozilla { + +[ManualDealloc] +sync protocol PSandboxTesting { +parent: + async ReportTestResults(nsCString testName, bool passed, nsCString message); + async TestCompleted(); + + sync GetSpecialDirectory(nsCString aSpecialDirName) returns (nsString aDirPath); + +child: + async ShutDown(); +}; + +} //namespace mozilla diff --git a/security/sandbox/common/test/SandboxTest.cpp b/security/sandbox/common/test/SandboxTest.cpp new file mode 100644 index 0000000000..29cb1798e8 --- /dev/null +++ b/security/sandbox/common/test/SandboxTest.cpp @@ -0,0 +1,362 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 https://mozilla.org/MPL/2.0/. */ + +#include "SandboxTest.h" + +#include "mozilla/Components.h" +#include "mozilla/Preferences.h" +#include "SandboxTestingParent.h" +#include "SandboxTestingChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/gfx/GPUProcessManager.h" +#include "mozilla/gfx/GPUChild.h" +#include "mozilla/net/SocketProcessParent.h" +#include "mozilla/RDDProcessManager.h" +#include "mozilla/RDDChild.h" +#include "mozilla/ipc/UtilityProcessManager.h" +#include "mozilla/ipc/UtilityProcessParent.h" +#include "mozilla/ipc/UtilityProcessSandboxing.h" +#include "GMPService.h" +#include "mozilla/gmp/GMPTypes.h" +#include "mozilla/ipc/Endpoint.h" +#include "nsIOService.h" + +#ifdef XP_WIN +# include "nsAppDirectoryServiceDefs.h" +#endif + +using namespace mozilla; +using namespace mozilla::ipc; +using namespace mozilla::dom; + +namespace mozilla { + +NS_IMPL_ISUPPORTS(SandboxTest, mozISandboxTest) + +inline void UnsetEnvVariable(const nsCString& aEnvVarName) { + nsCString aEnvVarNameFull = aEnvVarName + "="_ns; + int rv_unset = +#ifdef XP_UNIX + unsetenv(aEnvVarName.get()); +#endif // XP_UNIX +#ifdef XP_WIN + _putenv(aEnvVarNameFull.get()); +#endif // XP_WIN + MOZ_ASSERT(rv_unset == 0, "Error unsetting env var"); +} + +GeckoProcessType GeckoProcessStringToType(const nsCString& aString) { + for (GeckoProcessType type = GeckoProcessType(0); + type < GeckoProcessType::GeckoProcessType_End; + type = GeckoProcessType(type + 1)) { + if (aString == XRE_GeckoProcessTypeToString(type)) { + return type; + } + } + return GeckoProcessType::GeckoProcessType_Invalid; +} + +// Set up tests on remote process connected to the given actor. +// The actor must handle the InitSandboxTesting message. +template <typename Actor> +void InitializeSandboxTestingActors( + Actor* aActor, + const RefPtr<SandboxTest::ProcessPromise::Private>& aProcessPromise) { + MOZ_ASSERT(aActor, "Should have provided an IPC actor"); + Endpoint<PSandboxTestingParent> sandboxTestingParentEnd; + Endpoint<PSandboxTestingChild> sandboxTestingChildEnd; + nsresult rv = PSandboxTesting::CreateEndpoints(&sandboxTestingParentEnd, + &sandboxTestingChildEnd); + if (NS_FAILED(rv)) { + aProcessPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + + // GMPlugin binds us to the GMP Thread, so we need IPC's Send to be done on + // the same thread + Unused << aActor->SendInitSandboxTesting(std::move(sandboxTestingChildEnd)); + // But then the SandboxTestingParent::Create() call needs to be on the main + // thread + NS_DispatchToMainThread(NS_NewRunnableFunction( + "SandboxTestingParent::Create", + [stpE = std::move(sandboxTestingParentEnd), aProcessPromise]() mutable { + return aProcessPromise->Resolve( + SandboxTestingParent::Create(std::move(stpE)), __func__); + })); +} + +NS_IMETHODIMP +SandboxTest::StartTests(const nsTArray<nsCString>& aProcessesList) { + MOZ_ASSERT(NS_IsMainThread()); + +#if defined(XP_WIN) + nsCOMPtr<nsIFile> testFile; + NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(testFile)); + MOZ_ASSERT(testFile); + nsCOMPtr<nsIFile> testChromeFile; + testFile->Clone(getter_AddRefs(testChromeFile)); + testChromeFile->Append(u"chrome"_ns); + testChromeFile->Exists(&mChromeDirExisted); + testFile->Append(u"sandboxTest.txt"_ns); + testChromeFile->Append(u"sandboxTest.txt"_ns); + MOZ_ALWAYS_SUCCEEDS(testFile->Create(nsIFile::NORMAL_FILE_TYPE, 0666)); + MOZ_ALWAYS_SUCCEEDS(testChromeFile->Create(nsIFile::NORMAL_FILE_TYPE, 0666)); +#endif + + for (const auto& processTypeName : aProcessesList) { + SandboxingKind sandboxingKind = SandboxingKind::COUNT; + GeckoProcessType type = GeckoProcessType::GeckoProcessType_Invalid; + if (processTypeName.Find(":") != kNotFound) { + int32_t pos = processTypeName.Find(":"); + nsCString processType = nsCString(Substring(processTypeName, 0, pos)); + nsCString sandboxKindStr = nsCString( + Substring(processTypeName, pos + 1, processTypeName.Length())); + + nsresult err; + uint64_t sbVal = (uint64_t)(sandboxKindStr.ToDouble(&err)); + if (NS_FAILED(err)) { + NS_WARNING("Unable to get SandboxingKind"); + return NS_ERROR_ILLEGAL_VALUE; + } + + if (sbVal >= SandboxingKind::COUNT) { + NS_WARNING("Invalid sandboxing kind"); + return NS_ERROR_ILLEGAL_VALUE; + } + + if (!processType.Equals( + XRE_GeckoProcessTypeToString(GeckoProcessType_Utility))) { + NS_WARNING("Expected utility process type"); + return NS_ERROR_ILLEGAL_VALUE; + } + + sandboxingKind = (SandboxingKind)sbVal; + type = GeckoProcessType_Utility; + } else { + type = GeckoProcessStringToType(processTypeName); + + if (type == GeckoProcessType::GeckoProcessType_Invalid) { + NS_WARNING("Invalid process type"); + return NS_ERROR_ILLEGAL_VALUE; + } + } + + RefPtr<ProcessPromise::Private> processPromise = + MakeRefPtr<ProcessPromise::Private>(__func__); + + switch (type) { + case GeckoProcessType_Content: { + nsTArray<ContentParent*> parents; + ContentParent::GetAll(parents); + if (parents[0]) { + InitializeSandboxTestingActors(parents[0], processPromise); + } else { + processPromise->Reject(NS_ERROR_FAILURE, __func__); + MOZ_ASSERT_UNREACHABLE("SandboxTest; failure to get Content process"); + } + break; + } + + case GeckoProcessType_GPU: { + gfx::GPUProcessManager* gpuProc = gfx::GPUProcessManager::Get(); + gfx::GPUChild* gpuChild = gpuProc ? gpuProc->GetGPUChild() : nullptr; + if (gpuChild) { + InitializeSandboxTestingActors(gpuChild, processPromise); + } else { + processPromise->Reject(NS_OK, __func__); + } + break; + } + + case GeckoProcessType_RDD: { + RDDProcessManager* rddProc = RDDProcessManager::Get(); + rddProc->LaunchRDDProcess()->Then( + GetMainThreadSerialEventTarget(), __func__, + [processPromise, rddProc]() { + RDDChild* rddChild = rddProc ? rddProc->GetRDDChild() : nullptr; + if (rddChild) { + return InitializeSandboxTestingActors(rddChild, processPromise); + } + return processPromise->Reject(NS_ERROR_FAILURE, __func__); + }, + [processPromise](nsresult aError) { + MOZ_ASSERT_UNREACHABLE("SandboxTest; failure to get RDD process"); + return processPromise->Reject(aError, __func__); + }); + break; + } + + case GeckoProcessType_GMPlugin: { + UnsetEnvVariable("MOZ_DISABLE_GMP_SANDBOX"_ns); + RefPtr<gmp::GeckoMediaPluginService> service = + gmp::GeckoMediaPluginService::GetGeckoMediaPluginService(); + MOZ_ASSERT(service, "We have a GeckoMediaPluginService"); + + RefPtr<SandboxTest> self = this; + nsCOMPtr<nsISerialEventTarget> thread = service->GetGMPThread(); + nsresult rv = thread->Dispatch(NS_NewRunnableFunction( + "SandboxTest::GMPlugin", [self, processPromise, service, thread]() { + service->GetContentParentForTest()->Then( + thread, __func__, + [self, processPromise]( + const RefPtr<gmp::GMPContentParent::CloseBlocker>& + wrapper) { + RefPtr<gmp::GMPContentParent> parent = wrapper->mParent; + MOZ_ASSERT(parent, + "Wrapper should wrap a valid parent if we're in " + "this path."); + if (!parent) { + return processPromise->Reject(NS_ERROR_ILLEGAL_VALUE, + __func__); + } + NS_DispatchToMainThread(NS_NewRunnableFunction( + "SandboxTesting::Wrapper", [self, wrapper]() { + self->mGMPContentParentWrapper = wrapper; + })); + return InitializeSandboxTestingActors(parent.get(), + processPromise); + }, + [processPromise](const MediaResult& rv) { + return processPromise->Reject(NS_ERROR_FAILURE, __func__); + }); + })); + NS_ENSURE_SUCCESS(rv, rv); + break; + } + + case GeckoProcessType_Socket: { + // mochitest harness force this variable, but we actually do not want + // that + UnsetEnvVariable("MOZ_DISABLE_SOCKET_PROCESS"_ns); + + nsresult rv_pref = + Preferences::SetBool("network.process.enabled", true); + MOZ_ASSERT(rv_pref == NS_OK, "Error enforcing pref"); + + MOZ_ASSERT(net::gIOService, "No gIOService?"); + + net::gIOService->CallOrWaitForSocketProcess([processPromise]() { + // If socket process was previously disabled by env, + // nsIOService code will take some time before it creates the new + // process and it triggers this callback + net::SocketProcessParent* parent = + net::SocketProcessParent::GetSingleton(); + if (parent) { + return InitializeSandboxTestingActors(parent, processPromise); + } + return processPromise->Reject(NS_ERROR_FAILURE, __func__); + }); + break; + } + + case GeckoProcessType_Utility: { + RefPtr<UtilityProcessManager> utilityProc = + UtilityProcessManager::GetSingleton(); + utilityProc->LaunchProcess(sandboxingKind) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [processPromise, utilityProc, sandboxingKind]() { + RefPtr<UtilityProcessParent> utilityParent = + utilityProc + ? utilityProc->GetProcessParent(sandboxingKind) + : nullptr; + if (utilityParent) { + return InitializeSandboxTestingActors(utilityParent.get(), + processPromise); + } + return processPromise->Reject(NS_ERROR_FAILURE, __func__); + }, + [processPromise](nsresult aError) { + MOZ_ASSERT_UNREACHABLE( + "SandboxTest; failure to get Utility process"); + return processPromise->Reject(aError, __func__); + }); + break; + } + + default: + MOZ_ASSERT_UNREACHABLE( + "SandboxTest does not yet support this process type"); + return NS_ERROR_NOT_IMPLEMENTED; + } + + RefPtr<SandboxTest> self = this; + RefPtr<ProcessPromise> aPromise(processPromise); + aPromise->Then( + GetMainThreadSerialEventTarget(), __func__, + [self](SandboxTestingParent* aValue) { + self->mSandboxTestingParents.AppendElement(aValue); + return NS_OK; + }, + [](nsresult aError) { + if (aError == NS_OK) { + // There is no such process for this OS. Report test done. + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + MOZ_RELEASE_ASSERT(observerService); + observerService->NotifyObservers(nullptr, "sandbox-test-done", + nullptr); + return NS_OK; + } + MOZ_ASSERT_UNREACHABLE("SandboxTest; failure to get a process"); + return NS_ERROR_FAILURE; + }); + } + return NS_OK; +} + +NS_IMETHODIMP +SandboxTest::FinishTests() { + if (mGMPContentParentWrapper) { + RefPtr<gmp::GeckoMediaPluginService> service = + gmp::GeckoMediaPluginService::GetGeckoMediaPluginService(); + MOZ_ASSERT(service, "We have a GeckoMediaPluginService"); + + nsCOMPtr<nsISerialEventTarget> thread = service->GetGMPThread(); + nsresult rv = thread->Dispatch(NS_NewRunnableFunction( + "SandboxTest::FinishTests", + [wrapper = std::move(mGMPContentParentWrapper)]() { + // Release mGMPContentWrapper's reference. We hold this to keep an + // active reference on the CloseBlocker produced by GMPService, + // otherwise it would automatically shutdown the GMPlugin thread we + // started. + // If somehow it does not work as expected, then tests will fail + // because of leaks happening on GMPService and others. + })); + NS_ENSURE_SUCCESS(rv, rv); + } + + for (SandboxTestingParent* stp : mSandboxTestingParents) { + SandboxTestingParent::Destroy(stp); + } + + // Make sure there is no leftover for test --verify to run without failure + mSandboxTestingParents.Clear(); + +#if defined(XP_WIN) + nsCOMPtr<nsIFile> testFile; + NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(testFile)); + MOZ_ASSERT(testFile); + nsCOMPtr<nsIFile> testChromeFile; + testFile->Clone(getter_AddRefs(testChromeFile)); + testChromeFile->Append(u"chrome"_ns); + testFile->Append(u"sandboxTest.txt"_ns); + if (mChromeDirExisted) { + // Chrome dir existed, just delete test file. + testChromeFile->Append(u"sandboxTest.txt"_ns); + } + testFile->Remove(false); + testChromeFile->Remove(true); +#endif + + return NS_OK; +} + +} // namespace mozilla + +NS_IMPL_COMPONENT_FACTORY(mozISandboxTest) { + return MakeAndAddRef<SandboxTest>().downcast<nsISupports>(); +} diff --git a/security/sandbox/common/test/SandboxTest.h b/security/sandbox/common/test/SandboxTest.h new file mode 100644 index 0000000000..955a104e43 --- /dev/null +++ b/security/sandbox/common/test/SandboxTest.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 https://mozilla.org/MPL/2.0/. */ +#ifndef mozilla_SandboxTest_h +#define mozilla_SandboxTest_h + +#include "SandboxTestingParent.h" +#include "mozISandboxTest.h" +#include "mozilla/GfxMessageUtils.h" +#include "mozilla/MozPromise.h" +#include "GMPService.h" +#include "nsTArray.h" + +#if !defined(MOZ_DEBUG) || !defined(ENABLE_TESTS) +# error "This file should not be used outside of debug with tests" +#endif + +namespace mozilla { + +class SandboxTest : public mozISandboxTest { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISANDBOXTEST + + SandboxTest() : mSandboxTestingParents{nullptr} {}; + + // We allow nsresult to be rejected with values: + // - NS_ERROR_FAILURE in obvious case of error + // - NS_OK in case of success to complete the code but missing process (GPU) + using ProcessPromise = MozPromise<SandboxTestingParent*, nsresult, true>; + + private: + virtual ~SandboxTest() = default; + nsTArray<SandboxTestingParent*> mSandboxTestingParents; + RefPtr<gmp::GMPContentParent::CloseBlocker> mGMPContentParentWrapper; +#if defined(XP_WIN) + bool mChromeDirExisted = false; +#endif +}; + +} // namespace mozilla +#endif // mozilla_SandboxTest_h diff --git a/security/sandbox/common/test/SandboxTestingChild.cpp b/security/sandbox/common/test/SandboxTestingChild.cpp new file mode 100644 index 0000000000..64e347b958 --- /dev/null +++ b/security/sandbox/common/test/SandboxTestingChild.cpp @@ -0,0 +1,191 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 https://mozilla.org/MPL/2.0/. */ + +#include "SandboxTestingChild.h" +#include "SandboxTestingChildTests.h" +#include "SandboxTestingThread.h" +#include "mozilla/ipc/Endpoint.h" +#include "mozilla/ipc/UtilityProcessSandboxing.h" +#include "mozilla/ipc/UtilityProcessChild.h" + +#ifdef XP_LINUX +# include "mozilla/Sandbox.h" +#endif + +#include "nsXULAppAPI.h" + +namespace mozilla { + +SandboxTestingChild* SandboxTestingChild::sInstance = nullptr; + +bool SandboxTestingChild::IsTestThread() { return mThread->IsOnThread(); } + +void SandboxTestingChild::PostToTestThread( + already_AddRefed<nsIRunnable>&& runnable) { + mThread->Dispatch(std::move(runnable)); +} + +/* static */ +bool SandboxTestingChild::Initialize( + Endpoint<PSandboxTestingChild>&& aSandboxTestingEndpoint) { + MOZ_ASSERT(!sInstance); + SandboxTestingThread* thread = SandboxTestingThread::Create(); + if (!thread) { + return false; + } + sInstance = + new SandboxTestingChild(thread, std::move(aSandboxTestingEndpoint)); + return true; +} + +/* static */ +SandboxTestingChild* SandboxTestingChild::GetInstance() { + MOZ_ASSERT(sInstance, "Must initialize SandboxTestingChild before using it"); + return sInstance; +} + +SandboxTestingChild::SandboxTestingChild( + SandboxTestingThread* aThread, Endpoint<PSandboxTestingChild>&& aEndpoint) + : mThread(aThread) { + MOZ_ASSERT(aThread); + PostToTestThread(NewNonOwningRunnableMethod<Endpoint<PSandboxTestingChild>&&>( + "SandboxTestingChild::Bind", this, &SandboxTestingChild::Bind, + std::move(aEndpoint))); +} + +void SandboxTestingChild::Bind(Endpoint<PSandboxTestingChild>&& aEndpoint) { + MOZ_RELEASE_ASSERT(mThread->IsOnThread()); + DebugOnly<bool> ok = aEndpoint.Bind(this); + MOZ_ASSERT(ok); + +#ifdef XP_LINUX + bool sandboxCrashOnError = SetSandboxCrashOnError(false); +#endif + + if (XRE_IsContentProcess()) { + RunTestsContent(this); + } + + if (XRE_IsRDDProcess()) { + RunTestsRDD(this); + } + + if (XRE_IsGMPluginProcess()) { + RunTestsGMPlugin(this); + } + + if (XRE_IsSocketProcess()) { + RunTestsSocket(this); + } + + if (XRE_IsUtilityProcess()) { + RefPtr<ipc::UtilityProcessChild> s = ipc::UtilityProcessChild::Get(); + MOZ_ASSERT(s, "Unable to grab a UtilityProcessChild"); + switch (s->mSandbox) { + case ipc::SandboxingKind::GENERIC_UTILITY: + RunTestsGenericUtility(this); +#ifdef MOZ_APPLEMEDIA + [[fallthrough]]; + case ipc::SandboxingKind::UTILITY_AUDIO_DECODING_APPLE_MEDIA: +#endif +#ifdef XP_WIN + [[fallthrough]]; + case ipc::SandboxingKind::UTILITY_AUDIO_DECODING_WMF: +#endif + RunTestsUtilityAudioDecoder(this, s->mSandbox); + break; + + default: + MOZ_ASSERT(false, "Invalid SandboxingKind"); + break; + } + } + +#ifdef XP_LINUX + SetSandboxCrashOnError(sandboxCrashOnError); +#endif + + // Tell SandboxTest that this process is done with all tests. + SendTestCompleted(); +} + +void SandboxTestingChild::ActorDestroy(ActorDestroyReason aWhy) { + MOZ_ASSERT(mThread->IsOnThread()); + NS_DispatchToMainThread(NS_NewRunnableFunction( + "SandboxChildDestroyer", []() { SandboxTestingChild::Destroy(); })); +} + +void SandboxTestingChild::Destroy() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(sInstance); + delete sInstance; + sInstance = nullptr; +} + +ipc::IPCResult SandboxTestingChild::RecvShutDown() { + Close(); + return IPC_OK(); +} + +void SandboxTestingChild::ReportNoTests() { + SendReportTestResults("dummy_test"_ns, /* passed */ true, + "The test framework fails if there are no cases."_ns); +} + +template <typename F> +void SandboxTestingChild::ErrnoTest(const nsCString& aName, bool aExpectSuccess, + F&& aFunction) { + int status = aFunction() >= 0 ? 0 : errno; + PosixTest(aName, aExpectSuccess, status); +} + +template <typename F> +void SandboxTestingChild::ErrnoValueTest(const nsCString& aName, + int aExpectedErrno, F&& aFunction) { + int status = aFunction() >= 0 ? 0 : errno; + PosixTest(aName, aExpectedErrno == 0, status, Some(aExpectedErrno)); +} + +void SandboxTestingChild::PosixTest(const nsCString& aName, bool aExpectSuccess, + int aStatus, Maybe<int> aExpectedError) { + nsAutoCString message; + bool passed; + + // The "expected" arguments are a little redundant. + MOZ_ASSERT(!aExpectedError || aExpectSuccess == (*aExpectedError == 0)); + + // Decide whether the test passed, and stringify the actual result. + if (aStatus == 0) { + message = "Succeeded"_ns; + passed = aExpectSuccess; + } else { + message = "Error: "_ns; + message += strerror(aStatus); + if (aExpectedError) { + passed = aStatus == *aExpectedError; + } else { + passed = !aExpectSuccess; + } + } + + // If something unexpected happened, mention the expected result. + if (!passed) { + message += "; expected "; + if (aExpectSuccess) { + message += "success"; + } else { + message += "error"; + if (aExpectedError) { + message += ": "; + message += strerror(*aExpectedError); + } + } + } + + SendReportTestResults(aName, passed, message); +} + +} // namespace mozilla diff --git a/security/sandbox/common/test/SandboxTestingChild.h b/security/sandbox/common/test/SandboxTestingChild.h new file mode 100644 index 0000000000..b8259c5707 --- /dev/null +++ b/security/sandbox/common/test/SandboxTestingChild.h @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_SandboxTestingChild_h +#define mozilla_SandboxTestingChild_h + +#include "mozilla/PSandboxTestingChild.h" +#include "mozilla/Maybe.h" +#include "mozilla/Monitor.h" +#include "mozilla/UniquePtr.h" + +#ifdef XP_UNIX +# include "nsString.h" +#endif + +#if !defined(MOZ_SANDBOX) || !defined(MOZ_DEBUG) || !defined(ENABLE_TESTS) +# error "This file should not be used outside of debug with tests" +#endif + +namespace mozilla { + +class SandboxTestingThread; + +/** + * Runs tests that check sandbox in child process, depending on process type. + */ +class SandboxTestingChild : public PSandboxTestingChild { + public: + static bool Initialize( + Endpoint<PSandboxTestingChild>&& aSandboxTestingEndpoint); + static SandboxTestingChild* GetInstance(); + static void Destroy(); + + bool IsTestThread(); + void PostToTestThread(already_AddRefed<nsIRunnable>&& runnable); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + virtual ipc::IPCResult RecvShutDown(); + + // Helper to return that no test have been executed. Tests should make sure + // they have some fallback through that otherwise the framework will consider + // absence of test report as a failure. + inline void ReportNoTests(); + + // For test cases that return an error number or 0, like newer POSIX + // APIs. If `aExpectSuccess` is true, the test passes if the status is + // 0; otherwise, the test requires a specific error if `aExpectedError` + // is `Some(n)` or any nonzero status if it's `Nothing()`. + void PosixTest(const nsCString& aName, bool aExpectSuccess, int aStatus, + Maybe<int> aExpectedError = Nothing()); + + // For test cases that return a negative number and set `errno` to + // indicate error, like classical Unix APIs; takes a callable, which + // is used only in this function call (so `[&]` captures are safe). + template <typename F> + void ErrnoTest(const nsCString& aName, bool aExpectSuccess, F&& aFunction); + + // Similar to ErrnoTest, except that we want to compare a specific `errno` + // being returned. + template <typename F> + void ErrnoValueTest(const nsCString& aName, int aExpectedErrno, + F&& aFunction); + + private: + explicit SandboxTestingChild(SandboxTestingThread* aThread, + Endpoint<PSandboxTestingChild>&& aEndpoint); + + void Bind(Endpoint<PSandboxTestingChild>&& aEndpoint); + + UniquePtr<SandboxTestingThread> mThread; + + static SandboxTestingChild* sInstance; +}; + +} // namespace mozilla + +#endif // mozilla_SandboxTestingChild_h diff --git a/security/sandbox/common/test/SandboxTestingChildTests.h b/security/sandbox/common/test/SandboxTestingChildTests.h new file mode 100644 index 0000000000..f26bc68af0 --- /dev/null +++ b/security/sandbox/common/test/SandboxTestingChildTests.h @@ -0,0 +1,824 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 https://mozilla.org/MPL/2.0/. */ + +#include "SandboxTestingChild.h" + +#include "mozilla/StaticPrefs_security.h" +#include "mozilla/ipc/UtilityProcessSandboxing.h" +#ifdef XP_MACOSX +# include "nsCocoaFeatures.h" +#endif +#include "nsXULAppAPI.h" + +#ifdef XP_UNIX +# include <fcntl.h> +# include <netdb.h> +# ifdef XP_LINUX +# include <linux/mempolicy.h> +# include <sched.h> +# include <sys/ioctl.h> +# include <sys/prctl.h> +# include <sys/resource.h> +# include <sys/socket.h> +# include <sys/statfs.h> +# include <sys/syscall.h> +# include <sys/sysmacros.h> +# include <sys/time.h> +# include <sys/un.h> +# include <sys/utsname.h> +# include <termios.h> +# include "mozilla/ProcInfo_linux.h" +# include "mozilla/UniquePtrExtensions.h" +# ifdef MOZ_X11 +# include "X11/Xlib.h" +# include "X11UndefineNone.h" +# endif // MOZ_X11 +# endif // XP_LINUX +# include <sys/socket.h> +# include <sys/stat.h> +# include <sys/types.h> +# include <time.h> +# include <unistd.h> +#endif + +#ifdef XP_MACOSX +# if defined(__SSE2__) || defined(_M_X64) || \ + (defined(_M_IX86_FP) && _M_IX86_FP >= 2) +# include "emmintrin.h" +# endif +# include <spawn.h> +# include <CoreFoundation/CoreFoundation.h> +# include <CoreGraphics/CoreGraphics.h> +# include <AudioToolbox/AudioToolbox.h> +namespace ApplicationServices { +# include <ApplicationServices/ApplicationServices.h> +} +#endif + +#ifdef XP_WIN +# include <stdio.h> +# include <winternl.h> + +# include "mozilla/DynamicallyLinkedFunctionPtr.h" +# include "nsAppDirectoryServiceDefs.h" +# include "mozilla/WindowsProcessMitigations.h" +#endif + +constexpr bool kIsDebug = +#ifdef DEBUG + true; +#else + false; +#endif + +namespace mozilla { + +#ifdef XP_LINUX +static void RunTestsSched(SandboxTestingChild* child) { + struct sched_param param_pid_0 = {}; + child->ErrnoTest("sched_getparam(0)"_ns, true, + [&] { return sched_getparam(0, ¶m_pid_0); }); + + struct sched_param param_pid_tid = {}; + child->ErrnoTest("sched_getparam(tid)"_ns, true, [&] { + return sched_getparam((pid_t)syscall(__NR_gettid), ¶m_pid_tid); + }); + + struct sched_param param_pid_Ntid = {}; + child->ErrnoValueTest("sched_getparam(Ntid)"_ns, EPERM, [&] { + return sched_getparam((pid_t)(syscall(__NR_gettid) - 1), ¶m_pid_Ntid); + }); +} +#endif + +// Tests that apply to every process type (more or less) +static void RunGenericTests(SandboxTestingChild* child, bool aIsGMP = false) { +#ifdef XP_LINUX + // Check ABI issues with 32-bit arguments on 64-bit platforms. + if (sizeof(void*) == 8) { + static constexpr uint64_t kHighBits = 0xDEADBEEF00000000; + + struct timespec ts0, ts1; + child->ErrnoTest("high_bits_gettime"_ns, true, [&] { + return syscall(__NR_clock_gettime, kHighBits | CLOCK_MONOTONIC, &ts0); + }); + // Try to make sure we got the correct clock by reading it again and + // comparing to see if the times are vaguely similar. + int rv = clock_gettime(CLOCK_MONOTONIC, &ts1); + MOZ_RELEASE_ASSERT(rv == 0); + MOZ_RELEASE_ASSERT(ts0.tv_sec <= ts1.tv_sec + 1); + MOZ_RELEASE_ASSERT(ts1.tv_sec <= ts0.tv_sec + 60); + + // Check some non-zeroth arguments. (fcntl is convenient for + // this, but GMP has a stricter policy, so skip it there.) + if (!aIsGMP) { + int flags; + child->ErrnoTest("high_bits_fcntl_getfl"_ns, true, [&] { + flags = syscall(__NR_fcntl, 0, kHighBits | F_GETFL); + return flags; + }); + MOZ_RELEASE_ASSERT(flags == fcntl(0, F_GETFL)); + + int fds[2]; + rv = pipe(fds); + MOZ_RELEASE_ASSERT(rv >= 0); + child->ErrnoTest("high_bits_fcntl_setfl"_ns, true, [&] { + return syscall(__NR_fcntl, fds[0], kHighBits | F_SETFL, + kHighBits | O_NONBLOCK); + }); + flags = fcntl(fds[0], F_GETFL); + MOZ_RELEASE_ASSERT(flags >= 0); + MOZ_RELEASE_ASSERT(flags & O_NONBLOCK); + } + } +#endif // XP_LINUX +} + +#ifdef XP_WIN +/** + * Uses NtCreateFile directly to test file system brokering. + * + */ +static void FileTest(const nsCString& aName, const char* aSpecialDirName, + const nsString& aRelativeFilePath, ACCESS_MASK aAccess, + bool aExpectSuccess, SandboxTestingChild* aChild) { + static const StaticDynamicallyLinkedFunctionPtr<decltype(&NtCreateFile)> + pNtCreateFile(L"ntdll.dll", "NtCreateFile"); + static const StaticDynamicallyLinkedFunctionPtr<decltype(&NtClose)> pNtClose( + L"ntdll.dll", "NtClose"); + + // Start the filename with the NT namespace + nsString testFilename(u"\\??\\"_ns); + nsString dirPath; + aChild->SendGetSpecialDirectory(nsDependentCString(aSpecialDirName), + &dirPath); + testFilename.Append(dirPath); + testFilename.AppendLiteral("\\"); + testFilename.Append(aRelativeFilePath); + + UNICODE_STRING uniFileName; + ::RtlInitUnicodeString(&uniFileName, testFilename.get()); + + OBJECT_ATTRIBUTES objectAttributes; + InitializeObjectAttributes(&objectAttributes, &uniFileName, + OBJ_CASE_INSENSITIVE, nullptr, nullptr); + + HANDLE fileHandle = INVALID_HANDLE_VALUE; + IO_STATUS_BLOCK ioStatusBlock = {}; + + ULONG createOptions = StringEndsWith(testFilename, u"\\"_ns) || + StringEndsWith(testFilename, u"/"_ns) + ? FILE_DIRECTORY_FILE + : FILE_NON_DIRECTORY_FILE; + NTSTATUS status = pNtCreateFile( + &fileHandle, aAccess, &objectAttributes, &ioStatusBlock, nullptr, 0, 0, + FILE_OPEN_IF, createOptions | FILE_SYNCHRONOUS_IO_NONALERT, nullptr, 0); + + if (fileHandle != INVALID_HANDLE_VALUE) { + pNtClose(fileHandle); + } + + nsCString accessString; + if ((aAccess & FILE_GENERIC_READ) == FILE_GENERIC_READ) { + accessString.AppendLiteral("r"); + } + if ((aAccess & FILE_GENERIC_WRITE) == FILE_GENERIC_WRITE) { + accessString.AppendLiteral("w"); + } + if ((aAccess & FILE_GENERIC_EXECUTE) == FILE_GENERIC_EXECUTE) { + accessString.AppendLiteral("e"); + } + + nsCString msgRelPath = NS_ConvertUTF16toUTF8(aRelativeFilePath); + for (size_t i = 0, j = 0; i < aRelativeFilePath.Length(); ++i, ++j) { + if (aRelativeFilePath[i] == u'\\') { + msgRelPath.Insert('\\', j++); + } + } + + nsCString message; + message.AppendPrintf( + "Special dir: %s, file: %s, access: %s , returned status: %lx", + aSpecialDirName, msgRelPath.get(), accessString.get(), status); + + aChild->SendReportTestResults(aName, aExpectSuccess == NT_SUCCESS(status), + message); +} +#endif + +#ifdef XP_MACOSX +/* + * Test if this process can launch another process with posix_spawnp, + * exec, and LSOpenCFURLRef. All launches are expected to fail. In processes + * where the sandbox permits reading of file metadata (content processes at + * this time), we expect the posix_spawnp error to be EPERM. In processes + * without that permission, we expect ENOENT. Changing the sandbox policy + * may break this assumption, but the important aspect to test for is that the + * launch is not permitted. + */ +void RunMacTestLaunchProcess(SandboxTestingChild* child, + int aPosixSpawnExpectedError = ENOENT) { + // Test that posix_spawnp fails + char* argv[2]; + argv[0] = const_cast<char*>("bash"); + argv[1] = NULL; + int rv = posix_spawnp(NULL, "/bin/bash", NULL, NULL, argv, NULL); + nsPrintfCString posixSpawnMessage("posix_spawnp returned %d, expected %d", rv, + aPosixSpawnExpectedError); + child->SendReportTestResults("posix_spawnp test"_ns, + rv == aPosixSpawnExpectedError, + posixSpawnMessage); + + // Test that exec fails + child->ErrnoTest("execv /bin/bash test"_ns, false, [&] { + char* argvp = NULL; + return execv("/bin/bash", &argvp); + }); + + // Test that launching an application using LSOpenCFURLRef fails + char* uri; + if (nsCocoaFeatures::OnCatalinaOrLater()) { + uri = const_cast<char*>("/System/Applications/Utilities/Console.app"); + } else { + uri = const_cast<char*>("/Applications/Utilities/Console.app"); + } + CFStringRef filePath = ::CFStringCreateWithCString(kCFAllocatorDefault, uri, + kCFStringEncodingUTF8); + CFURLRef urlRef = ::CFURLCreateWithFileSystemPath( + kCFAllocatorDefault, filePath, kCFURLPOSIXPathStyle, false); + if (!urlRef) { + child->SendReportTestResults("LSOpenCFURLRef"_ns, false, + "CFURLCreateWithFileSystemPath failed"_ns); + return; + } + + OSStatus status = ApplicationServices::LSOpenCFURLRef(urlRef, NULL); + ::CFRelease(urlRef); + nsPrintfCString lsMessage( + "LSOpenCFURLRef returned %d, " + "expected kLSServerCommunicationErr (%d)", + status, ApplicationServices::kLSServerCommunicationErr); + child->SendReportTestResults( + "LSOpenCFURLRef"_ns, + status == ApplicationServices::kLSServerCommunicationErr, lsMessage); +} + +/* + * Test if this process can connect to the macOS window server. + * When |aShouldHaveAccess| is true, the test passes if access is __permitted__. + * When |aShouldHaveAccess| is false, the test passes if access is __blocked__. + */ +void RunMacTestWindowServer(SandboxTestingChild* child, + bool aShouldHaveAccess = false) { + // CGSessionCopyCurrentDictionary() returns NULL when a + // connection to the window server is not available. + CFDictionaryRef windowServerDict = CGSessionCopyCurrentDictionary(); + bool gotWindowServerDetails = (windowServerDict != nullptr); + bool testPassed = (gotWindowServerDetails == aShouldHaveAccess); + child->SendReportTestResults( + "CGSessionCopyCurrentDictionary"_ns, testPassed, + gotWindowServerDetails + ? "dictionary returned, access is permitted"_ns + : "no dictionary returned, access appears blocked"_ns); + if (windowServerDict != nullptr) { + CFRelease(windowServerDict); + } +} + +/* + * Test if this process can get access to audio components on macOS. + * When |aShouldHaveAccess| is true, the test passes if access is __permitted__. + * When |aShouldHaveAccess| is false, the test passes if access is __blocked__. + */ +void RunMacTestAudioAPI(SandboxTestingChild* child, + bool aShouldHaveAccess = false) { + AudioStreamBasicDescription inputFormat; + inputFormat.mFormatID = kAudioFormatMPEG4AAC; + inputFormat.mSampleRate = 48000.0; + inputFormat.mChannelsPerFrame = 2; + inputFormat.mBitsPerChannel = 0; + inputFormat.mFormatFlags = 0; + inputFormat.mFramesPerPacket = 1024; + inputFormat.mBytesPerPacket = 0; + + UInt32 inputFormatSize = sizeof(inputFormat); + OSStatus status = AudioFormatGetProperty( + kAudioFormatProperty_FormatInfo, 0, NULL, &inputFormatSize, &inputFormat); + + bool gotAudioFormat = (status == 0); + bool testPassed = (gotAudioFormat == aShouldHaveAccess); + child->SendReportTestResults( + "AudioFormatGetProperty"_ns, testPassed, + gotAudioFormat ? "got audio format, access is permitted"_ns + : "no audio format, access appears blocked"_ns); +} +#endif /* XP_MACOSX */ + +#ifdef XP_WIN +void RunWinTestWin32k(SandboxTestingChild* child, + bool aShouldHaveAccess = true) { + bool isLockedDown = (IsWin32kLockedDown() == true); + bool testPassed = (isLockedDown == aShouldHaveAccess); + child->SendReportTestResults( + "Win32kLockdown"_ns, testPassed, + isLockedDown ? "got lockdown, access is blocked"_ns + : "no lockdown, access appears permitted"_ns); +} +#endif // XP_WIN + +void RunTestsContent(SandboxTestingChild* child) { + MOZ_ASSERT(child, "No SandboxTestingChild*?"); + + RunGenericTests(child); + +#ifdef XP_UNIX + struct stat st; + static const char kAllowedPath[] = "/usr/lib"; + + child->ErrnoTest("fstatat_as_stat"_ns, true, + [&] { return fstatat(AT_FDCWD, kAllowedPath, &st, 0); }); + child->ErrnoTest("fstatat_as_lstat"_ns, true, [&] { + return fstatat(AT_FDCWD, kAllowedPath, &st, AT_SYMLINK_NOFOLLOW); + }); + +# ifdef XP_LINUX + child->ErrnoTest("fstatat_as_fstat"_ns, true, + [&] { return fstatat(0, "", &st, AT_EMPTY_PATH); }); + + const struct timespec usec = {0, 1000}; + child->ErrnoTest("nanosleep"_ns, true, + [&] { return nanosleep(&usec, nullptr); }); + + struct timespec res = {0, 0}; + child->ErrnoTest("clock_getres"_ns, true, + [&] { return clock_getres(CLOCK_REALTIME, &res); }); + + // same process is allowed + struct timespec tproc = {0, 0}; + clockid_t same_process = MAKE_PROCESS_CPUCLOCK(getpid(), CPUCLOCK_SCHED); + child->ErrnoTest("clock_gettime_same_process"_ns, true, + [&] { return clock_gettime(same_process, &tproc); }); + + // different process is blocked by sandbox (SIGSYS, kernel would return + // EINVAL) + struct timespec tprocd = {0, 0}; + clockid_t diff_process = MAKE_PROCESS_CPUCLOCK(1, CPUCLOCK_SCHED); + child->ErrnoValueTest("clock_gettime_diff_process"_ns, ENOSYS, + [&] { return clock_gettime(diff_process, &tprocd); }); + + // thread is allowed + struct timespec tthread = {0, 0}; + clockid_t thread = + MAKE_THREAD_CPUCLOCK((pid_t)syscall(__NR_gettid), CPUCLOCK_SCHED); + child->ErrnoTest("clock_gettime_thread"_ns, true, + [&] { return clock_gettime(thread, &tthread); }); + + // getcpu is allowed + // We're using syscall directly because: + // - sched_getcpu uses vdso and as a result doesn't go through the sandbox. + // - getcpu isn't defined in the header files we're using yet. + int c; + child->ErrnoTest("getcpu"_ns, true, + [&] { return syscall(SYS_getcpu, &c, NULL, NULL); }); + + // An abstract socket that does not starts with '/', so we don't want it to + // work. + // Checking ENETUNREACH should be thrown by SandboxBrokerClient::Connect() + // when it detects it does not starts with a '/' + child->ErrnoValueTest("connect_abstract_blocked"_ns, ENETUNREACH, [&] { + int sockfd; + struct sockaddr_un addr; + char str[] = "\0xyz"; // Abstract socket requires first byte to be NULL + size_t str_size = 4; + + memset(&addr, 0, sizeof(struct sockaddr_un)); + addr.sun_family = AF_UNIX; + memcpy(&addr.sun_path, str, str_size); + + sockfd = socket(AF_UNIX, SOCK_STREAM, 0); + if (sockfd == -1) { + return -1; + } + + int con_st = connect(sockfd, (struct sockaddr*)&addr, + sizeof(sa_family_t) + str_size); + return con_st; + }); + + // An abstract socket that does starts with /, so we do want it to work. + // Checking ECONNREFUSED because this is what the broker should get + // when trying to establish the connect call for us if it's allowed; + // otherwise we get EACCES, meaning that it was passed to the broker + // (unlike the previous test) but rejected. + const int errorForX = + StaticPrefs::security_sandbox_content_headless_AtStartup() ? EACCES + : ECONNREFUSED; + child->ErrnoValueTest("connect_abstract_permit"_ns, errorForX, [&] { + int sockfd; + struct sockaddr_un addr; + // we re-use actual X path, because this is what is allowed within + // SandboxBrokerPolicyFactory::InitContentPolicy() + // We can't just use any random path allowed, but one with CONNECT allowed. + + // (Note that the real X11 sockets have names like `X0` for + // display `:0`; there shouldn't be anything named just `X`.) + + // Abstract socket requires first byte to be NULL + char str[] = "\0/tmp/.X11-unix/X"; + size_t str_size = 17; + + memset(&addr, 0, sizeof(struct sockaddr_un)); + addr.sun_family = AF_UNIX; + memcpy(&addr.sun_path, str, str_size); + + sockfd = socket(AF_UNIX, SOCK_STREAM, 0); + if (sockfd == -1) { + return -1; + } + + int con_st = connect(sockfd, (struct sockaddr*)&addr, + sizeof(sa_family_t) + str_size); + return con_st; + }); + + // Testing FIPS-relevant files, which need to be accessible + std::vector<std::pair<const char*, bool>> open_tests = { + {"/dev/random", true}}; + // Not all systems have that file, so we only test access, if it exists + // in the first place + if (stat("/proc/sys/crypto/fips_enabled", &st) == 0) { + open_tests.push_back({"/proc/sys/crypto/fips_enabled", true}); + } + + for (const std::pair<const char*, bool>& to_open : open_tests) { + child->ErrnoTest("open("_ns + nsCString(to_open.first) + ")"_ns, + to_open.second, [&] { + int fd = open(to_open.first, O_RDONLY); + if (to_open.second && fd > 0) { + close(fd); + } + return fd; + }); + } + + child->ErrnoTest("statfs"_ns, true, [] { + struct statfs sf; + return statfs("/usr/share", &sf); + }); + +# ifdef MOZ_X11 + // Check that X11 access is blocked (bug 1129492). + // This will fail if security.sandbox.content.headless is turned off. + if (PR_GetEnv("DISPLAY")) { + Display* disp = XOpenDisplay(nullptr); + + child->SendReportTestResults( + "x11_access"_ns, !disp, + disp ? "XOpenDisplay succeeded"_ns : "XOpenDisplay failed"_ns); + if (disp) { + XCloseDisplay(disp); + } + } +# endif // MOZ_X11 +# endif // XP_LINUX + +# ifdef XP_MACOSX + RunMacTestLaunchProcess(child, EPERM); + RunMacTestWindowServer(child); + RunMacTestAudioAPI(child, true); +# endif + +#elif XP_WIN + FileTest("read from chrome"_ns, NS_APP_USER_CHROME_DIR, u"sandboxTest.txt"_ns, + FILE_GENERIC_READ, true, child); + FileTest("read from profile via relative path"_ns, NS_APP_USER_CHROME_DIR, + u"..\\sandboxTest.txt"_ns, FILE_GENERIC_READ, false, child); + // The profile dir is the parent of the chrome dir. + FileTest("read from chrome using forward slash"_ns, + NS_APP_USER_PROFILE_50_DIR, u"chrome/sandboxTest.txt"_ns, + FILE_GENERIC_READ, false, child); + + // Note: these only pass in DEBUG builds because we allow write access to the + // temp dir for certain test logs and that is where the profile is created. + FileTest("read from profile"_ns, NS_APP_USER_PROFILE_50_DIR, + u"sandboxTest.txt"_ns, FILE_GENERIC_READ, kIsDebug, child); + FileTest("read/write from chrome"_ns, NS_APP_USER_CHROME_DIR, + u"sandboxTest.txt"_ns, FILE_GENERIC_READ | FILE_GENERIC_WRITE, + kIsDebug, child); +#else + child->ReportNoTests(); +#endif +} + +void RunTestsSocket(SandboxTestingChild* child) { + MOZ_ASSERT(child, "No SandboxTestingChild*?"); + + RunGenericTests(child); + +#ifdef XP_UNIX + child->ErrnoTest("getaddrinfo"_ns, true, [&] { + struct addrinfo* res; + int rv = getaddrinfo("localhost", nullptr, nullptr, &res); + if (res != nullptr) { + freeaddrinfo(res); + } + return rv; + }); + +# ifdef XP_LINUX + child->ErrnoTest("prctl_allowed"_ns, true, [&] { + int rv = prctl(PR_SET_DUMPABLE, 0, 0, 0, 0); + return rv; + }); + + child->ErrnoTest("prctl_blocked"_ns, false, [&] { + int rv = prctl(PR_GET_SECCOMP, 0, 0, 0, 0); + return rv; + }); + + // Testing FIPS-relevant files, which need to be accessible + std::vector<std::pair<const char*, bool>> open_tests = { + {"/dev/random", true}}; + // Not all systems have that file, so we only test access, if it exists + // in the first place + struct stat st; + if (stat("/proc/sys/crypto/fips_enabled", &st) == 0) { + open_tests.push_back({"/proc/sys/crypto/fips_enabled", true}); + } + + for (const std::pair<const char*, bool>& to_open : open_tests) { + child->ErrnoTest("open("_ns + nsCString(to_open.first) + ")"_ns, + to_open.second, [&] { + int fd = open(to_open.first, O_RDONLY); + if (to_open.second && fd > 0) { + close(fd); + } + return fd; + }); + } + + // getcpu is allowed + // We're using syscall directly because: + // - sched_getcpu uses vdso and as a result doesn't go through the sandbox. + // - getcpu isn't defined in the header files we're using yet. + int c; + child->ErrnoTest("getcpu"_ns, true, + [&] { return syscall(SYS_getcpu, &c, NULL, NULL); }); +# endif // XP_LINUX +#elif XP_MACOSX + RunMacTestLaunchProcess(child); + RunMacTestWindowServer(child); + RunMacTestAudioAPI(child); +#else // XP_UNIX + child->ReportNoTests(); +#endif // XP_UNIX +} + +void RunTestsRDD(SandboxTestingChild* child) { + MOZ_ASSERT(child, "No SandboxTestingChild*?"); + + RunGenericTests(child); + +#ifdef XP_UNIX +# ifdef XP_LINUX + child->ErrnoValueTest("ioctl_tiocsti"_ns, ENOSYS, [&] { + int rv = ioctl(1, TIOCSTI, "x"); + return rv; + }); + + struct rusage res = {}; + child->ErrnoTest("getrusage"_ns, true, [&] { + int rv = getrusage(RUSAGE_SELF, &res); + return rv; + }); + + child->ErrnoValueTest("unlink"_ns, ENOENT, [&] { + int rv = unlink(""); + return rv; + }); + + child->ErrnoValueTest("unlinkat"_ns, ENOENT, [&] { + int rv = unlinkat(AT_FDCWD, "", 0); + return rv; + }); + + RunTestsSched(child); + + child->ErrnoTest("socket_inet"_ns, false, + [] { return socket(AF_INET, SOCK_STREAM, 0); }); + + child->ErrnoTest("socket_unix"_ns, false, + [] { return socket(AF_UNIX, SOCK_STREAM, 0); }); + + child->ErrnoTest("uname"_ns, true, [] { + struct utsname uts; + return uname(&uts); + }); + + child->ErrnoValueTest("ioctl_dma_buf"_ns, ENOTTY, [] { + // Apply the ioctl to the wrong kind of fd; it should fail with + // ENOTTY (rather than ENOSYS if it were blocked). + return ioctl(0, _IOW('b', 0, uint64_t), nullptr); + }); + + // getcpu is allowed + // We're using syscall directly because: + // - sched_getcpu uses vdso and as a result doesn't go through the sandbox. + // - getcpu isn't defined in the header files we're using yet. + int c; + child->ErrnoTest("getcpu"_ns, true, + [&] { return syscall(SYS_getcpu, &c, NULL, NULL); }); + + // The nvidia proprietary drivers will, in some cases, try to + // mknod their device files; we reject this politely. + child->ErrnoValueTest("mknod"_ns, EPERM, [] { + return mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3)); + }); + + // nvidia defines some ioctls with the type 0x46 ('F', otherwise + // used by fbdev) and numbers starting from 200 (0xc8). + child->ErrnoValueTest("ioctl_nvidia"_ns, ENOTTY, + [] { return ioctl(0, 0x46c8, nullptr); }); + + child->ErrnoTest("statfs"_ns, true, [] { + struct statfs sf; + return statfs("/usr/share", &sf); + }); + +# elif XP_MACOSX + RunMacTestLaunchProcess(child); + RunMacTestWindowServer(child); + RunMacTestAudioAPI(child, true); +# endif +#else // XP_UNIX +# ifdef XP_WIN + RunWinTestWin32k(child, false); +# endif // XP_WIN + child->ReportNoTests(); +#endif +} + +void RunTestsGMPlugin(SandboxTestingChild* child) { + MOZ_ASSERT(child, "No SandboxTestingChild*?"); + + RunGenericTests(child, /* aIsGMP = */ true); + +#ifdef XP_UNIX +# ifdef XP_LINUX + struct utsname utsname_res = {}; + child->ErrnoTest("uname_restricted"_ns, true, [&] { + int rv = uname(&utsname_res); + + nsCString expectedSysname("Linux"_ns); + nsCString sysname(utsname_res.sysname); + nsCString expectedVersion("3"_ns); + nsCString version(utsname_res.version); + if ((sysname != expectedSysname) || (version != expectedVersion)) { + return -1; + } + + return rv; + }); + + child->ErrnoTest("getuid"_ns, true, [&] { return getuid(); }); + child->ErrnoTest("getgid"_ns, true, [&] { return getgid(); }); + child->ErrnoTest("geteuid"_ns, true, [&] { return geteuid(); }); + child->ErrnoTest("getegid"_ns, true, [&] { return getegid(); }); + + RunTestsSched(child); + + std::vector<std::pair<const char*, bool>> open_tests = { + {"/etc/ld.so.cache", true}, + {"/proc/cpuinfo", true}, + {"/etc/hostname", false}}; + + for (const std::pair<const char*, bool>& to_open : open_tests) { + child->ErrnoTest("open("_ns + nsCString(to_open.first) + ")"_ns, + to_open.second, [&] { + int fd = open(to_open.first, O_RDONLY); + if (to_open.second && fd > 0) { + close(fd); + } + return fd; + }); + } + + child->ErrnoValueTest("readlink_exe"_ns, EINVAL, [] { + char pathBuf[PATH_MAX]; + return readlink("/proc/self/exe", pathBuf, sizeof(pathBuf)); + }); + + child->ErrnoTest("memfd_sizing"_ns, true, [] { + int fd = syscall(__NR_memfd_create, "sandbox-test", 0); + if (fd < 0) { + if (errno == ENOSYS) { + // Don't fail the test if the kernel is old. + return 0; + } + return -1; + } + + int rv = ftruncate(fd, 4096); + int savedErrno = errno; + close(fd); + errno = savedErrno; + return rv; + }); + +# elif XP_MACOSX // XP_LINUX + RunMacTestLaunchProcess(child); + /* The Mac GMP process requires access to the window server */ + RunMacTestWindowServer(child, true /* aShouldHaveAccess */); + RunMacTestAudioAPI(child); +# endif // XP_MACOSX +#else // XP_UNIX + child->ReportNoTests(); +#endif +} + +void RunTestsGenericUtility(SandboxTestingChild* child) { + MOZ_ASSERT(child, "No SandboxTestingChild*?"); + + RunGenericTests(child); + +#ifdef XP_UNIX +# ifdef XP_LINUX + child->ErrnoValueTest("ioctl_tiocsti"_ns, ENOSYS, [&] { + int rv = ioctl(1, TIOCSTI, "x"); + return rv; + }); + + struct rusage res; + child->ErrnoTest("getrusage"_ns, true, [&] { + int rv = getrusage(RUSAGE_SELF, &res); + return rv; + }); +# elif XP_MACOSX // XP_LINUX + RunMacTestLaunchProcess(child); + RunMacTestWindowServer(child); + RunMacTestAudioAPI(child); +# endif // XP_MACOSX +#elif XP_WIN // XP_UNIX + child->ErrnoValueTest("write_only"_ns, EACCES, [&] { + FILE* rv = fopen("test_sandbox.txt", "w"); + if (rv != nullptr) { + fclose(rv); + return 0; + } + return -1; + }); + RunWinTestWin32k(child); +#else // XP_UNIX + child->ReportNoTests(); +#endif // XP_MACOSX +} + +void RunTestsUtilityAudioDecoder(SandboxTestingChild* child, + ipc::SandboxingKind aSandbox) { + MOZ_ASSERT(child, "No SandboxTestingChild*?"); + + RunGenericTests(child); + +#ifdef XP_UNIX +# ifdef XP_LINUX + // getrusage is allowed in Generic Utility and on AudioDecoder + struct rusage res; + child->ErrnoTest("getrusage"_ns, true, [&] { + int rv = getrusage(RUSAGE_SELF, &res); + return rv; + }); + + // get_mempolicy is not allowed in Generic Utility but is on AudioDecoder + child->ErrnoTest("get_mempolicy"_ns, true, [&] { + int numa_node; + int test_val = 0; + // <numaif.h> not installed by default, let's call directly the syscall + long rv = syscall(SYS_get_mempolicy, &numa_node, NULL, 0, (void*)&test_val, + MPOL_F_NODE | MPOL_F_ADDR); + return rv; + }); + // set_mempolicy is not allowed in Generic Utility but is on AudioDecoder + child->ErrnoValueTest("set_mempolicy"_ns, ENOSYS, [&] { + // <numaif.h> not installed by default, let's call directly the syscall + long rv = syscall(SYS_set_mempolicy, 0, NULL, 0); + return rv; + }); +# elif XP_MACOSX // XP_LINUX + RunMacTestLaunchProcess(child); + RunMacTestWindowServer(child); + RunMacTestAudioAPI( + child, + aSandbox == ipc::SandboxingKind::UTILITY_AUDIO_DECODING_APPLE_MEDIA); +# endif // XP_MACOSX +#else // XP_UNIX +# ifdef XP_WIN + RunWinTestWin32k(child); +# endif // XP_WIN + child->ReportNoTests(); +#endif // XP_UNIX +} + +} // namespace mozilla diff --git a/security/sandbox/common/test/SandboxTestingParent.cpp b/security/sandbox/common/test/SandboxTestingParent.cpp new file mode 100644 index 0000000000..d4939fc5b5 --- /dev/null +++ b/security/sandbox/common/test/SandboxTestingParent.cpp @@ -0,0 +1,126 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 https://mozilla.org/MPL/2.0/. */ + +#include "SandboxTestingParent.h" +#include "SandboxTestingThread.h" +#include "nsIObserverService.h" +#include "mozilla/ipc/Endpoint.h" +#include "mozilla/Services.h" +#include "mozilla/SyncRunnable.h" +#include "nsDirectoryServiceUtils.h" + +namespace mozilla { + +/* static */ +SandboxTestingParent* SandboxTestingParent::Create( + Endpoint<PSandboxTestingParent>&& aParentEnd) { + SandboxTestingThread* thread = SandboxTestingThread::Create(); + if (!thread) { + return nullptr; + } + return new SandboxTestingParent(thread, std::move(aParentEnd)); +} + +SandboxTestingParent::SandboxTestingParent( + SandboxTestingThread* aThread, Endpoint<PSandboxTestingParent>&& aParentEnd) + : mThread(aThread), + mMonitor("SandboxTestingParent Lock"), + mShutdownDone(false) { + MOZ_ASSERT(mThread); + mThread->Dispatch( + NewNonOwningRunnableMethod<Endpoint<PSandboxTestingParent>&&>( + "SandboxTestingParent::Bind", this, &SandboxTestingParent::Bind, + std::move(aParentEnd))); +} + +void SandboxTestingParent::Bind(Endpoint<PSandboxTestingParent>&& aEnd) { + MOZ_RELEASE_ASSERT(mThread->IsOnThread()); + DebugOnly<bool> ok = aEnd.Bind(this); + MOZ_ASSERT(ok); +} + +void SandboxTestingParent::ShutdownSandboxTestThread() { + MOZ_ASSERT(mThread->IsOnThread()); + Close(); + // Notify waiting thread that we are done. + MonitorAutoLock lock(mMonitor); + mShutdownDone = true; + mMonitor.Notify(); +} + +void SandboxTestingParent::Destroy(SandboxTestingParent* aInstance) { + MOZ_ASSERT(NS_IsMainThread()); + if (!aInstance) { + return; + } + + { + // Hold the lock while we destroy the actor on the test thread. + MonitorAutoLock lock(aInstance->mMonitor); + aInstance->mThread->Dispatch(NewNonOwningRunnableMethod( + "SandboxTestingParent::ShutdownSandboxTestThread", aInstance, + &SandboxTestingParent::ShutdownSandboxTestThread)); + + // Wait for test thread to complete destruction. + while (!aInstance->mShutdownDone) { + aInstance->mMonitor.Wait(); + } + } + + delete aInstance; +} + +void SandboxTestingParent::ActorDestroy(ActorDestroyReason aWhy) { + MOZ_RELEASE_ASSERT(mThread->IsOnThread()); +} + +mozilla::ipc::IPCResult SandboxTestingParent::RecvReportTestResults( + const nsCString& testName, bool passed, const nsCString& resultMessage) { + NS_DispatchToMainThread( + NS_NewRunnableFunction("SandboxReportTestResults", [=]() { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + MOZ_RELEASE_ASSERT(observerService); + nsCString passedStr(passed ? "true"_ns : "false"_ns); + nsString json; + json += u"{ \"testid\" : \""_ns + NS_ConvertUTF8toUTF16(testName) + + u"\", \"passed\" : "_ns + NS_ConvertUTF8toUTF16(passedStr) + + u", \"message\" : \""_ns + + NS_ConvertUTF8toUTF16(resultMessage) + u"\" }"_ns; + observerService->NotifyObservers(nullptr, "sandbox-test-result", + json.BeginReading()); + })); + return IPC_OK(); +} + +mozilla::ipc::IPCResult SandboxTestingParent::RecvTestCompleted() { + Unused << SendShutDown(); + NS_DispatchToMainThread( + NS_NewRunnableFunction("SandboxReportTestResults", []() { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + MOZ_RELEASE_ASSERT(observerService); + observerService->NotifyObservers(nullptr, "sandbox-test-done", 0); + })); + return IPC_OK(); +} + +mozilla::ipc::IPCResult SandboxTestingParent::RecvGetSpecialDirectory( + const nsCString& aSpecialDirName, nsString* aDirPath) { + RefPtr<Runnable> runnable = NS_NewRunnableFunction( + "SandboxTestingParent::RecvGetSpecialDirectory", [&]() { + nsCOMPtr<nsIFile> dir; + NS_GetSpecialDirectory(aSpecialDirName.get(), getter_AddRefs(dir)); + if (dir) { + dir->GetPath(*aDirPath); + } + }); + SyncRunnable::DispatchToThread(GetMainThreadEventTarget(), runnable, + /*aForceDispatch*/ true); + return IPC_OK(); +} + +} // namespace mozilla diff --git a/security/sandbox/common/test/SandboxTestingParent.h b/security/sandbox/common/test/SandboxTestingParent.h new file mode 100644 index 0000000000..1899b11119 --- /dev/null +++ b/security/sandbox/common/test/SandboxTestingParent.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_SandboxTestingParent_h +#define mozilla_SandboxTestingParent_h + +#include "mozilla/PSandboxTestingParent.h" +#include "mozilla/Monitor.h" +#include "mozilla/UniquePtr.h" + +#if !defined(MOZ_SANDBOX) || !defined(MOZ_DEBUG) || !defined(ENABLE_TESTS) +# error "This file should not be used outside of debug with tests" +#endif + +namespace mozilla { + +class SandboxTestingThread; + +class SandboxTestingParent : public PSandboxTestingParent { + public: + static SandboxTestingParent* Create( + Endpoint<PSandboxTestingParent>&& aParentEnd); + static void Destroy(SandboxTestingParent* aInstance); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvReportTestResults(const nsCString& testName, + bool passed, + const nsCString& resultMessage); + mozilla::ipc::IPCResult RecvTestCompleted(); + + mozilla::ipc::IPCResult RecvGetSpecialDirectory( + const nsCString& aSpecialDirName, nsString* aDirPath); + + private: + explicit SandboxTestingParent(SandboxTestingThread* aThread, + Endpoint<PSandboxTestingParent>&& aParentEnd); + virtual ~SandboxTestingParent() = default; + void ShutdownSandboxTestThread(); + void Bind(Endpoint<PSandboxTestingParent>&& aEnd); + + UniquePtr<SandboxTestingThread> mThread; + Monitor mMonitor MOZ_UNANNOTATED; + bool mShutdownDone; +}; + +} // namespace mozilla + +#endif // mozilla_SandboxTestingParent_h diff --git a/security/sandbox/common/test/SandboxTestingThread.h b/security/sandbox/common/test/SandboxTestingThread.h new file mode 100644 index 0000000000..f0ddfa5573 --- /dev/null +++ b/security/sandbox/common/test/SandboxTestingThread.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_SandboxTestingThread_h +#define mozilla_SandboxTestingThread_h + +#include "nsThreadManager.h" + +#if !defined(MOZ_SANDBOX) || !defined(MOZ_DEBUG) || !defined(ENABLE_TESTS) +# error "This file should not be used outside of debug with tests" +#endif + +namespace mozilla { + +class SandboxTestingThread { + public: + void Dispatch(already_AddRefed<nsIRunnable>&& aRunnable) { + mThread->Dispatch(std::move(aRunnable), nsIEventTarget::NS_DISPATCH_NORMAL); + } + + bool IsOnThread() { + bool on; + return NS_SUCCEEDED(mThread->IsOnCurrentThread(&on)) && on; + } + + static SandboxTestingThread* Create() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + nsCOMPtr<nsIThread> thread; + if (NS_FAILED( + NS_NewNamedThread("Sandbox Testing", getter_AddRefs(thread)))) { + return nullptr; + } + return new SandboxTestingThread(thread); + } + + ~SandboxTestingThread() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + mThread->Shutdown(); + } + + private: + explicit SandboxTestingThread(nsIThread* aThread) : mThread(aThread) { + MOZ_ASSERT(mThread); + } + + nsCOMPtr<nsIThread> mThread; +}; +} // namespace mozilla + +#endif // mozilla_SandboxTestingThread_h diff --git a/security/sandbox/common/test/mozISandboxTest.idl b/security/sandbox/common/test/mozISandboxTest.idl new file mode 100644 index 0000000000..7cde1defcf --- /dev/null +++ b/security/sandbox/common/test/mozISandboxTest.idl @@ -0,0 +1,28 @@ +/* -*- Mode: IDL; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 "nsISupports.idl" + +// This interface is only for testing Sandbox. + +[scriptable, builtinclass, uuid(2306c118-3544-4674-9222-670b88dc07a9)] +interface mozISandboxTest : nsISupports +{ + void startTests(in Array<ACString> aProcessesList); + void finishTests(); +}; + +%{ C++ + +#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS) +#define MOZ_SANDBOX_TEST_CID \ + {0x989dda27, 0xb144, 0x45f9, {0x90, 0x39, 0x69, 0x74, 0x4e, 0xc6, dd0xd9, 0x12}} +#define MOZ_SANDBOX_TEST_CONTRACTID \ + "@mozilla.org/sandbox/sandbox-test;1" +#else +#error "This file should not be used outside of debug with tests" +#endif +%} |