/* vim:set ts=2 sw=2 sts=2 et: */ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ #include "gtest/gtest.h" #include "js/RegExp.h" #include "mozilla/SpinEventLoopUntil.h" #include "mozilla/UntrustedModulesProcessor.h" #include "mozilla/WinDllServices.h" #include "nsContentUtils.h" #include "nsDirectoryServiceDefs.h" #include "TelemetryFixture.h" #include "UntrustedModulesBackupService.h" #include "UntrustedModulesDataSerializer.h" class ModuleLoadCounter final { nsDataHashtable mCounters; public: template ModuleLoadCounter(const nsString (&aNames)[N], const int (&aCounts)[N]) : mCounters(N) { for (int i = 0; i < N; ++i) { mCounters.Put(aNames[i], aCounts[i]); } } template bool Remains(const nsString (&aNames)[N], const int (&aCounts)[N]) { EXPECT_EQ(mCounters.Count(), N); if (mCounters.Count() != N) { return false; } bool result = true; for (int i = 0; i < N; ++i) { int* entry = mCounters.GetValue(aNames[i]); if (!entry) { wprintf(L"%s is not registered.\n", aNames[i].get()); result = false; } else if (*entry != aCounts[i]) { // We can return false, but let's print out all unmet modules // which may be helpful to investigate test failures. wprintf(L"%s:%4d\n", aNames[i].get(), *entry); result = false; } } return result; } bool IsDone() const { bool allZero = true; for (auto iter = mCounters.ConstIter(); !iter.Done(); iter.Next()) { if (iter.Data() < 0) { // If any counter is negative, we know the test fails. // No need to continue. return true; } if (iter.Data() > 0) { allZero = false; } } // If all counters are zero, the test finished nicely. Otherwise, those // counters are expected to be decremented later. Let's continue. return allZero; } void Decrement(const nsString& aName) { if (int* entry = mCounters.GetValue(aName)) { --(*entry); } } }; class UntrustedModulesCollector { static constexpr int kMaximumPendingQueries = 200; Vector mData; public: Vector& Data() { return mData; } nsresult Collect(ModuleLoadCounter& aChecker) { nsresult rv = NS_OK; mData.clear(); int pendingQueries = 0; EXPECT_TRUE(SpinEventLoopUntil([this, &pendingQueries, &aChecker, &rv]() { // Some of expected loaded modules are still missing // after kMaximumPendingQueries queries were submitted. // Giving up here to avoid an infinite loop. if (pendingQueries >= kMaximumPendingQueries) { rv = NS_ERROR_ABORT; return true; } ++pendingQueries; RefPtr dllSvc(DllServices::Get()); dllSvc->GetUntrustedModulesData()->Then( GetMainThreadSerialEventTarget(), __func__, [this, &pendingQueries, &aChecker](Maybe&& aResult) { EXPECT_GT(pendingQueries, 0); --pendingQueries; if (aResult.isSome()) { wprintf(L"Received data. (pendingQueries=%d)\n", pendingQueries); for (const auto& evt : aResult.ref().mEvents) { aChecker.Decrement(evt.mRequestedDllName); } EXPECT_TRUE(mData.emplaceBack(std::move(aResult.ref()))); } }, [&pendingQueries, &rv](nsresult aReason) { EXPECT_GT(pendingQueries, 0); --pendingQueries; wprintf(L"GetUntrustedModulesData() failed - %08x\n", aReason); EXPECT_TRUE(false); rv = aReason; }); // Keep calling GetUntrustedModulesData() until we meet the condition. return aChecker.IsDone(); })); EXPECT_TRUE(SpinEventLoopUntil( [&pendingQueries]() { return pendingQueries <= 0; })); return rv; } }; static void ValidateUntrustedModules(const UntrustedModulesData& aData) { EXPECT_EQ(aData.mProcessType, GeckoProcessType_Default); EXPECT_EQ(aData.mPid, ::GetCurrentProcessId()); nsTHashtable> moduleSet; for (auto iter = aData.mModules.ConstIter(); !iter.Done(); iter.Next()) { const RefPtr& module = iter.Data(); moduleSet.PutEntry(module); } for (const auto& evt : aData.mEvents) { EXPECT_EQ(evt.mThreadId, ::GetCurrentThreadId()); // Make sure mModule is pointing to an entry of mModules. EXPECT_TRUE(moduleSet.Contains(evt.mModule)); EXPECT_FALSE(evt.mIsDependent); EXPECT_EQ(evt.mLoadStatus, 0); } // No check for the mXULLoadDurationMS field because the field has a value // in CCov build GTest, but it is empty in non-CCov build (bug 1681936). EXPECT_GT(aData.mEvents.length(), 0); EXPECT_GT(aData.mStacks.GetModuleCount(), 0); EXPECT_EQ(aData.mSanitizationFailures, 0); EXPECT_EQ(aData.mTrustTestFailures, 0); } class UntrustedModulesFixture : public TelemetryTestFixture { static constexpr int kLoadCountBeforeDllServices = 5; static constexpr int kLoadCountAfterDllServices = 5; static constexpr uint32_t kMaxModulesArrayLen = 10; // One of the important test scenarios is to load modules before DllServices // is initialized and to make sure those loading events are forwarded when // DllServices is initialized. // However, GTest instantiates a Fixture class every testcase and there is // no way to re-enable DllServices and UntrustedModulesProcessor once it's // disabled, which means no matter how many testcases we have, only the // first testcase exercises that scenario. That's why we implement that // test scenario in InitialModuleLoadOnce as a static member and runs it // in the first testcase to be executed. static INIT_ONCE sInitLoadOnce; static UntrustedModulesCollector sInitLoadDataCollector; static nsString PrependWorkingDir(const nsAString& aLeaf) { nsCOMPtr file; EXPECT_TRUE(NS_SUCCEEDED(NS_GetSpecialDirectory(NS_OS_CURRENT_WORKING_DIR, getter_AddRefs(file)))); EXPECT_TRUE(NS_SUCCEEDED(file->Append(aLeaf))); bool exists; EXPECT_TRUE(NS_SUCCEEDED(file->Exists(&exists)) && exists); nsString fullPath; EXPECT_TRUE(NS_SUCCEEDED(file->GetPath(fullPath))); return fullPath; } static BOOL CALLBACK InitialModuleLoadOnce(PINIT_ONCE, void*, void**); protected: static constexpr int kInitLoadCount = kLoadCountBeforeDllServices + kLoadCountAfterDllServices; static const nsString kTestModules[]; static void LoadAndFree(const nsAString& aLeaf) { nsModuleHandle dll(::LoadLibraryW(PrependWorkingDir(aLeaf).get())); EXPECT_TRUE(!!dll); } virtual void SetUp() override { TelemetryTestFixture::SetUp(); ::InitOnceExecuteOnce(&sInitLoadOnce, InitialModuleLoadOnce, nullptr, nullptr); } static const Vector& GetInitLoadData() { return sInitLoadDataCollector.Data(); } // This method is useful if we want a new instance of UntrustedModulesData // which is not copyable. static UntrustedModulesData CollectSingleData() { // If we call LoadAndFree more than once, those loading events are // likely to be merged into an instance of UntrustedModulesData, // meaning the length of the collector's vector is at least one but // the exact number is unknown. LoadAndFree(kTestModules[0]); UntrustedModulesCollector collector; ModuleLoadCounter waitForOne({kTestModules[0]}, {1}); EXPECT_TRUE(NS_SUCCEEDED(collector.Collect(waitForOne))); EXPECT_TRUE(waitForOne.Remains({kTestModules[0]}, {0})); EXPECT_EQ(collector.Data().length(), 1); // Cannot "return collector.Data()[0]" as copy ctor is deleted. return UntrustedModulesData(std::move(collector.Data()[0])); } template void ValidateJSValue(const char16_t* aPattern, size_t aPatternLength, DataFetcherT&& aDataFetcher) { AutoJSContextWithGlobal cx(mCleanGlobal); mozilla::Telemetry::UntrustedModulesDataSerializer serializer( cx.GetJSContext(), kMaxModulesArrayLen); EXPECT_TRUE(!!serializer); aDataFetcher(serializer); JS::RootedValue jsval(cx.GetJSContext()); serializer.GetObject(&jsval); nsAutoString json; EXPECT_TRUE(nsContentUtils::StringifyJSON(cx.GetJSContext(), &jsval, json)); JS::RootedObject re( cx.GetJSContext(), JS::NewUCRegExpObject(cx.GetJSContext(), aPattern, aPatternLength, JS::RegExpFlag::Global)); EXPECT_TRUE(!!re); JS::RootedValue matchResult(cx.GetJSContext(), JS::NullValue()); size_t idx = 0; EXPECT_TRUE(JS::ExecuteRegExpNoStatics(cx.GetJSContext(), re, json.get(), json.Length(), &idx, true, &matchResult)); // On match, with aOnlyMatch = true, ExecuteRegExpNoStatics returns boolean // true. If no match, ExecuteRegExpNoStatics returns Null. EXPECT_TRUE(matchResult.isBoolean() && matchResult.toBoolean()); if (!matchResult.toBoolean()) { // If match failed, print out the actual JSON kindly. wprintf(L"JSON: %s\n", json.get()); wprintf(L"RE: %s\n", aPattern); } } }; const nsString UntrustedModulesFixture::kTestModules[] = { u"TestUntrustedModules_Dll1.dll"_ns, u"TestUntrustedModules_Dll2.dll"_ns}; INIT_ONCE UntrustedModulesFixture::sInitLoadOnce = INIT_ONCE_STATIC_INIT; UntrustedModulesCollector UntrustedModulesFixture::sInitLoadDataCollector; BOOL CALLBACK UntrustedModulesFixture::InitialModuleLoadOnce(PINIT_ONCE, void*, void**) { for (int i = 0; i < kLoadCountBeforeDllServices; ++i) { for (const auto& mod : kTestModules) { LoadAndFree(mod); } } RefPtr dllSvc(DllServices::Get()); dllSvc->StartUntrustedModulesProcessor(); for (int i = 0; i < kLoadCountAfterDllServices; ++i) { for (const auto& mod : kTestModules) { LoadAndFree(mod); } } ModuleLoadCounter waitForTwo(kTestModules, {kInitLoadCount, kInitLoadCount}); EXPECT_EQ(sInitLoadDataCollector.Collect(waitForTwo), NS_OK); EXPECT_TRUE(waitForTwo.Remains(kTestModules, {0, 0})); for (const auto& event : GetInitLoadData()) { ValidateUntrustedModules(event); } // Data was removed when retrieved. No data is retrieved again. UntrustedModulesCollector collector; ModuleLoadCounter waitOnceForEach(kTestModules, {1, 1}); EXPECT_EQ(collector.Collect(waitOnceForEach), NS_ERROR_ABORT); EXPECT_TRUE(waitOnceForEach.Remains(kTestModules, {1, 1})); return TRUE; } #define PROCESS_OBJ(TYPE, PID) \ u"\"" TYPE u"\\." PID u"\":{" \ u"\"processType\":\"" TYPE u"\",\"elapsed\":\\d+\\.\\d+," \ u"\"sanitizationFailures\":0,\"trustTestFailures\":0," \ u"\"events\":\\[{" \ u"\"processUptimeMS\":\\d+,\"loadDurationMS\":\\d+\\.\\d+," \ u"\"threadID\":\\d+,\"threadName\":\"Main Thread\"," \ u"\"baseAddress\":\"0x[0-9a-f]+\",\"moduleIndex\":0," \ u"\"isDependent\":false,\"loadStatus\":0}\\]," \ u"\"combinedStacks\":{" \ u"\"memoryMap\":\\[\\[\"\\w+\\.\\w+\",\"[0-9A-Z]+\"\\]" \ u"(,\\[\"\\w+\\.\\w+\",\"[0-9A-Z]+\\\"\\])*\\]," \ u"\"stacks\":\\[\\[\\[\\d+,\\d+\\]" \ u"(,\\[\\d+,\\d+\\])*\\]\\]}}" TEST_F(UntrustedModulesFixture, Serialize) { // clang-format off const char16_t kPattern[] = u"{\"structVersion\":1," u"\"modules\":\\[{" u"\"resolvedDllName\":\"TestUntrustedModules_Dll1\\.dll\"," u"\"fileVersion\":\"1\\.2\\.3\\.4\"," u"\"companyName\":\"Mozilla Corporation\",\"trustFlags\":0}\\]," u"\"processes\":{" PROCESS_OBJ(u"browser", u"0xabc") u"," PROCESS_OBJ(u"browser", u"0x4") u"," PROCESS_OBJ(u"rdd", u"0x4") u"}}"; // clang-format on UntrustedModulesBackupData backup1, backup2; { UntrustedModulesData data1 = CollectSingleData(); UntrustedModulesData data2 = CollectSingleData(); UntrustedModulesData data3 = CollectSingleData(); data1.mPid = 0xabc; data2.mPid = 0x4; data2.mProcessType = GeckoProcessType_RDD; data3.mPid = 0x4; backup1.Add(std::move(data1)); backup2.Add(std::move(data2)); backup1.Add(std::move(data3)); } ValidateJSValue(kPattern, ArrayLength(kPattern) - 1, [&backup1, &backup2]( Telemetry::UntrustedModulesDataSerializer& aSerializer) { EXPECT_TRUE(NS_SUCCEEDED(aSerializer.Add(backup1))); EXPECT_TRUE(NS_SUCCEEDED(aSerializer.Add(backup2))); }); } TEST_F(UntrustedModulesFixture, Backup) { using BackupType = UntrustedModulesBackupService::BackupType; RefPtr backupSvc( UntrustedModulesBackupService::Get()); for (int i = 0; i < 5; ++i) { backupSvc->Backup(BackupType::Staging, CollectSingleData()); } backupSvc->SettleAllStagingData(); EXPECT_TRUE(backupSvc->Ref(BackupType::Staging).IsEmpty()); for (auto iter = backupSvc->Ref(BackupType::Settled).ConstIter(); !iter.Done(); iter.Next()) { const RefPtr& container = iter.Data(); EXPECT_TRUE(!!container); const UntrustedModulesData& data = container->mData; EXPECT_EQ(iter.Key(), ProcessHashKey(data.mProcessType, data.mPid)); ValidateUntrustedModules(data); } }