Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
#include "UntrustedModules.h"
#include "GMPServiceParent.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/glean/GleanPings.h"
#include "mozilla/glean/TelemetryMetrics.h"
#include "mozilla/MozPromise.h"
#include "mozilla/net/SocketProcessParent.h"
#include "mozilla/ipc/UtilityProcessParent.h"
#include "mozilla/ipc/UtilityProcessManager.h"
#include "mozilla/RDDChild.h"
#include "mozilla/RDDProcessManager.h"
#include "mozilla/WinDllServices.h"
#include "nsISupportsImpl.h"
#include "nsProxyRelease.h"
#include "nsXULAppAPI.h"
#include "UntrustedModulesDataSerializer.h"
namespace mozilla {
namespace Telemetry {
static const uint32_t kMaxModulesArrayLen = 100;
using UntrustedModulesIpcPromise =
MozPromise<Maybe<UntrustedModulesData>, ipc::ResponseRejectReason, true>;
using MultiGetUntrustedModulesPromise =
MozPromise<bool /*aIgnored*/, nsresult, true>;
class MOZ_HEAP_CLASS MultiGetUntrustedModulesData final {
public:
/**
* @param aFlags [in] Combinations of the flags defined under nsITelemetry.
* (See "Flags for getUntrustedModuleLoadEvents"
* in nsITelemetry.idl)
*/
explicit MultiGetUntrustedModulesData(uint32_t aFlags)
: mFlags(aFlags),
mBackupSvc(UntrustedModulesBackupService::Get()),
mPromise(new MultiGetUntrustedModulesPromise::Private(__func__)),
mNumPending(0) {}
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MultiGetUntrustedModulesData)
RefPtr<MultiGetUntrustedModulesPromise> GetUntrustedModuleLoadEvents();
void Serialize(RefPtr<dom::Promise>&& aPromise);
/**
* Submit a "third-party-modules" ping with any already-gotten data.
*/
nsresult SubmitToGlean();
MultiGetUntrustedModulesData(const MultiGetUntrustedModulesData&) = delete;
MultiGetUntrustedModulesData(MultiGetUntrustedModulesData&&) = delete;
MultiGetUntrustedModulesData& operator=(const MultiGetUntrustedModulesData&) =
delete;
MultiGetUntrustedModulesData& operator=(MultiGetUntrustedModulesData&&) =
delete;
private:
~MultiGetUntrustedModulesData() = default;
void AddPending(RefPtr<UntrustedModulesPromise>&& aNewPending) {
MOZ_ASSERT(NS_IsMainThread());
++mNumPending;
RefPtr<MultiGetUntrustedModulesData> self(this);
aNewPending->Then(
GetMainThreadSerialEventTarget(), __func__,
[self](Maybe<UntrustedModulesData>&& aResult) {
self->OnCompletion(std::move(aResult));
},
[self](nsresult aReason) { self->OnCompletion(); });
}
void AddPending(RefPtr<UntrustedModulesIpcPromise>&& aNewPending) {
MOZ_ASSERT(NS_IsMainThread());
++mNumPending;
RefPtr<MultiGetUntrustedModulesData> self(this);
aNewPending->Then(
GetMainThreadSerialEventTarget(), __func__,
[self](Maybe<UntrustedModulesData>&& aResult) {
self->OnCompletion(std::move(aResult));
},
[self](ipc::ResponseRejectReason&& aReason) { self->OnCompletion(); });
}
void OnCompletion() {
MOZ_ASSERT(NS_IsMainThread() && mNumPending > 0);
--mNumPending;
if (mNumPending) {
return;
}
mPromise->Resolve(true, __func__);
}
void OnCompletion(Maybe<UntrustedModulesData>&& aResult) {
MOZ_ASSERT(NS_IsMainThread());
if (aResult.isSome()) {
mBackupSvc->Backup(std::move(aResult.ref()));
}
OnCompletion();
}
private:
// Combinations of the flags defined under nsITelemetry.
// (See "Flags for getUntrustedModuleLoadEvents" in nsITelemetry.idl)
uint32_t mFlags;
RefPtr<UntrustedModulesBackupService> mBackupSvc;
RefPtr<MultiGetUntrustedModulesPromise::Private> mPromise;
size_t mNumPending;
};
RefPtr<MultiGetUntrustedModulesPromise>
MultiGetUntrustedModulesData::GetUntrustedModuleLoadEvents() {
MOZ_ASSERT(XRE_IsParentProcess() && NS_IsMainThread());
// Parent process
RefPtr<DllServices> dllSvc(DllServices::Get());
AddPending(dllSvc->GetUntrustedModulesData());
// Child processes
nsTArray<dom::ContentParent*> contentParents;
dom::ContentParent::GetAll(contentParents);
for (auto&& contentParent : contentParents) {
AddPending(contentParent->SendGetUntrustedModulesData());
}
if (RefPtr<net::SocketProcessParent> socketActor =
net::SocketProcessParent::GetSingleton()) {
AddPending(socketActor->SendGetUntrustedModulesData());
}
if (RDDProcessManager* rddMgr = RDDProcessManager::Get()) {
if (RDDChild* rddChild = rddMgr->GetRDDChild()) {
AddPending(rddChild->SendGetUntrustedModulesData());
}
}
if (RefPtr<ipc::UtilityProcessManager> utilityManager =
ipc::UtilityProcessManager::GetIfExists()) {
for (RefPtr<ipc::UtilityProcessParent>& parent :
utilityManager->GetAllProcessesProcessParent()) {
AddPending(parent->SendGetUntrustedModulesData());
}
}
if (RefPtr<gmp::GeckoMediaPluginServiceParent> gmps =
gmp::GeckoMediaPluginServiceParent::GetSingleton()) {
nsTArray<RefPtr<
gmp::GeckoMediaPluginServiceParent::GetUntrustedModulesDataPromise>>
promises;
gmps->SendGetUntrustedModulesData(promises);
for (auto& promise : promises) {
AddPending(std::move(promise));
}
}
return mPromise;
}
#if defined(XP_WIN)
nsTArray<nsDependentSubstring> GetBlockedModules() {
nsTArray<nsDependentSubstring> blockedModules;
RefPtr<DllServices> dllSvc(DllServices::Get());
nt::SharedSection* sharedSection = dllSvc->GetSharedSection();
if (!sharedSection) {
return blockedModules;
}
auto dynamicBlocklist = sharedSection->GetDynamicBlocklist();
for (const auto& blockedEntry : dynamicBlocklist) {
if (!blockedEntry.IsValidDynamicBlocklistEntry()) {
break;
}
blockedModules.AppendElement(
nsDependentSubstring(blockedEntry.mName.Buffer,
blockedEntry.mName.Length / sizeof(wchar_t)));
}
return blockedModules;
}
#endif
nsresult MultiGetUntrustedModulesData::SubmitToGlean() {
MOZ_ASSERT(mFlags == 0,
"The Glean 'third-party-modules' ping doesn't know how to handle "
"nsITelemetry's flags for getUntrustedModuleLoadEvents.");
const UntrustedModulesBackupData& stagingRef = mBackupSvc->Staging();
if (stagingRef.IsEmpty()) {
return NS_OK;
}
glean::third_party_modules::ModulesObject modules;
glean::third_party_modules::ProcessesObject processes;
uint32_t modulesArrayIdx = 0;
for (const auto& container : stagingRef.Values()) {
if (!container) {
continue;
}
const auto& data = container->mData;
// We are duplicating the module mapping that
// UntrustedModulesDataSerializer::AddSingleData does because
// 1) Accessing its mIndexMap at the correct time would be fragile.
// 2) Decoupling data submission from JS serialization is arguably good.
nsTHashMap<nsStringHashKey, uint32_t> indexMap;
for (const auto& entry : data.mModules) {
nsresult rv = NS_OK;
(void)indexMap.LookupOrInsertWith(entry.GetKey(), [&]() -> uint32_t {
const auto record = entry.GetData();
if (!record) {
rv = NS_ERROR_FAILURE;
return 0;
}
glean::third_party_modules::ModulesObjectItem item;
item.resolvedDllName =
Some(NS_ConvertUTF16toUTF8(record->mSanitizedDllName));
if (record->mVersion.isSome()) {
auto [major, minor, patch, build] = record->mVersion->AsTuple();
nsCString version;
version.AppendPrintf("%" PRIu16 ".%" PRIu16 ".%" PRIu16 ".%" PRIu16,
major, minor, patch, build);
item.fileVersion = Some(version);
}
if (record->mVendorInfo.isSome()) {
MOZ_ASSERT(!record->mVendorInfo->mVendor.IsEmpty());
if (record->mVendorInfo->mVendor.IsEmpty()) {
// Per `SerializeModule`, this is an error condition severe enough
// to cease further processing.
rv = NS_ERROR_FAILURE;
return 0;
}
switch (record->mVendorInfo->mSource) {
case VendorInfo::Source::Signature:
item.signedBy =
Some(NS_ConvertUTF16toUTF8(record->mVendorInfo->mVendor));
break;
case VendorInfo::Source::VersionInfo:
item.companyName =
Some(NS_ConvertUTF16toUTF8(record->mVendorInfo->mVendor));
break;
default:
MOZ_ASSERT_UNREACHABLE("Unknown VendorInfo Source!");
rv = NS_ERROR_FAILURE;
return 0;
}
}
item.trustFlags = Some(static_cast<int64_t>(record->mTrustFlags));
modules.EmplaceBack(item);
return modulesArrayIdx++;
});
if (modulesArrayIdx > kMaxModulesArrayLen) {
rv = NS_ERROR_CANNOT_CONVERT_DATA;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
nsDependentCString processType;
if (data.mProcessType == GeckoProcessType_Default) {
processType.Rebind("browser"_ns, 0);
} else {
processType.Rebind(XRE_GeckoProcessTypeToString(data.mProcessType));
}
// UntrusteModulesDataSerializer::Add allows for multiple containers' data
// to be for the same process (type and pid).
// When that happens, the latest containers' data would take precedence.
// By intuition, I don't think that happens so we don't need to worry.
// And if it does, we'll get two items in the `processes` array:
// analyses may merely take the latest one.
glean::third_party_modules::ProcessesObjectItem process{
.processType = Some(processType),
.sanitizationFailures = Some(data.mSanitizationFailures),
.trustTestFailures = Some(data.mTrustTestFailures),
};
nsCString strPid(processType);
strPid.AppendLiteral(".0x");
strPid.AppendInt(static_cast<uint32_t>(data.mPid), 16);
process.processName = Some(strPid);
nsCString elapsed;
elapsed.AppendFloat(data.mElapsed.ToSecondsSigDigits());
process.elapsed = Some(elapsed);
if (data.mXULLoadDurationMS.isSome()) {
nsCString xulLoadDuration;
xulLoadDuration.AppendFloat(*(data.mXULLoadDurationMS));
process.xulLoadDurationMS = Some(xulLoadDuration);
}
glean::third_party_modules::ProcessesObjectItemEvents eventsArray;
for (const auto& eventContainer : data.mEvents) {
const ProcessedModuleLoadEvent& event = eventContainer->mEvent;
if (!event) {
// Event has no module.
continue;
}
glean::third_party_modules::ProcessesObjectItemEventsItem eventItem{
.processUptimeMS = Some(static_cast<int64_t>(event.mProcessUptimeMS)),
// TODO: Document the narrowing conversion (2^64ms is ~584MY)
.threadID = Some(event.mThreadId),
.requestedDllName =
Some(NS_ConvertUTF16toUTF8(event.mRequestedDllName)),
.isDependent = Some(event.mIsDependent),
.loadStatus = Some(event.mLoadStatus),
};
if (event.mLoadDurationMS.isSome()) {
nsCString loadDuration;
loadDuration.AppendFloat(*(event.mLoadDurationMS));
eventItem.loadDurationMS = Some(loadDuration);
}
nsDependentCString effectiveThreadName;
if (event.mThreadId == ::GetCurrentThreadId()) {
effectiveThreadName.Rebind("Main Thread"_ns, 0);
} else {
effectiveThreadName.Rebind(event.mThreadName, 0);
}
if (!effectiveThreadName.IsEmpty()) {
eventItem.threadName = Some(event.mThreadName);
}
if (!event.mRequestedDllName.IsEmpty() &&
!event.mRequestedDllName.Equals(event.mModule->mSanitizedDllName,
nsCaseInsensitiveStringComparator)) {
// TODO: Truncate to MAX_PATH - 3 + "..."
eventItem.requestedDllName =
Some(NS_ConvertUTF16toUTF8(event.mRequestedDllName));
}
nsAutoCString baseAddress("0x");
baseAddress.AppendInt(event.mBaseAddress, 16);
eventItem.baseAddress = Some(baseAddress);
uint32_t moduleIndex;
if (!indexMap.Get(event.mModule->mResolvedNtName, &moduleIndex)) {
// TODO: Should we instrument this somehow?
continue;
}
eventItem.moduleIndex = Some(moduleIndex);
eventsArray.AppendElement(eventItem);
}
if (eventsArray.Length()) {
process.events = Some(std::move(eventsArray));
}
glean::third_party_modules::ProcessesObjectItemCombinedstacks
combinedStacks;
glean::third_party_modules::ProcessesObjectItemCombinedstacksMemorymap
memoryMap;
const size_t moduleCount = data.mStacks.GetModuleCount();
for (size_t moduleIndex = 0; moduleIndex < moduleCount; ++moduleIndex) {
const auto& module = data.mStacks.GetModule(moduleIndex);
memoryMap.EmplaceBack(nsTArray<nsCString>{
NS_ConvertUTF16toUTF8(module.mName),
module.mBreakpadId,
});
}
combinedStacks.memoryMap = Some(std::move(memoryMap));
const size_t stackCount = data.mStacks.GetStackCount();
glean::third_party_modules::ProcessesObjectItemCombinedstacksStacks
stackArray;
for (size_t stackIndex = 0; stackIndex < stackCount; ++stackIndex) {
nsTArray<nsTArray<int64_t>> pcArray;
const CombinedStacks::Stack& stack = data.mStacks.GetStack(stackIndex);
const uint32_t pcCount = stack.size();
for (size_t pcIndex = 0; pcIndex < pcCount; ++pcIndex) {
const ProcessedStack::Frame& frame = stack[pcIndex];
const int64_t modIndex =
(std::numeric_limits<uint16_t>::max() == frame.mModIndex)
? -1
: frame.mModIndex;
pcArray.EmplaceBack(nsTArray<int64_t>{
modIndex,
static_cast<int64_t>(frame.mOffset),
});
}
stackArray.AppendElement(std::move(pcArray));
}
combinedStacks.stacks = Some(std::move(stackArray));
process.combinedStacks = Some(std::move(combinedStacks));
processes.AppendElement(std::move(process));
}
glean::third_party_modules::modules.Set(std::move(modules));
glean::third_party_modules::processes.Set(std::move(processes));
#if defined(XP_WIN)
nsTArray<nsDependentSubstring> uBlockedModules = GetBlockedModules();
nsTArray<nsCString> blockedModules;
for (const auto& blockedModule : uBlockedModules) {
blockedModules.EmplaceBack(NS_ConvertUTF16toUTF8(blockedModule));
}
glean::third_party_modules::blocked_modules.Set(blockedModules);
#endif
glean_pings::ThirdPartyModules.Submit();
return NS_OK;
}
void MultiGetUntrustedModulesData::Serialize(RefPtr<dom::Promise>&& aPromise) {
MOZ_ASSERT(NS_IsMainThread());
dom::AutoJSAPI jsapi;
if (NS_WARN_IF(!jsapi.Init(aPromise->GetGlobalObject()))) {
aPromise->MaybeReject(NS_ERROR_FAILURE);
return;
}
JSContext* cx = jsapi.cx();
UntrustedModulesDataSerializer serializer(cx, kMaxModulesArrayLen, mFlags);
if (!serializer) {
aPromise->MaybeReject(NS_ERROR_FAILURE);
return;
}
nsresult rv;
if (mFlags & nsITelemetry::INCLUDE_OLD_LOADEVENTS) {
// When INCLUDE_OLD_LOADEVENTS is set, we need to return instances
// from both "Staging" and "Settled" backup.
if (mFlags & nsITelemetry::KEEP_LOADEVENTS_NEW) {
// When INCLUDE_OLD_LOADEVENTS and KEEP_LOADEVENTS_NEW are set, we need to
// return a JS object consisting of all instances from both "Staging" and
// "Settled" backups, keeping instances in those backups as is.
if (mFlags & nsITelemetry::EXCLUDE_STACKINFO_FROM_LOADEVENTS) {
// Without the stack info, we can add multiple UntrustedModulesData to
// the serializer directly.
rv = serializer.Add(mBackupSvc->Staging());
if (NS_WARN_IF(NS_FAILED(rv))) {
aPromise->MaybeReject(rv);
return;
}
rv = serializer.Add(mBackupSvc->Settled());
if (NS_WARN_IF(NS_FAILED(rv))) {
aPromise->MaybeReject(rv);
return;
}
} else {
// Currently we don't have a method to merge UntrustedModulesData into
// a serialized JS object because merging CombinedStack will be tricky.
// Thus we return an error on this flag combination.
aPromise->MaybeReject(NS_ERROR_INVALID_ARG);
return;
}
} else {
// When KEEP_LOADEVENTS_NEW is not set, we can move data from "Staging"
// to "Settled" first, then add "Settled" to the serializer.
mBackupSvc->SettleAllStagingData();
const UntrustedModulesBackupData& settledRef = mBackupSvc->Settled();
if (settledRef.IsEmpty()) {
aPromise->MaybeReject(NS_ERROR_NOT_AVAILABLE);
return;
}
rv = serializer.Add(settledRef);
if (NS_WARN_IF(NS_FAILED(rv))) {
aPromise->MaybeReject(rv);
return;
}
}
} else {
// When INCLUDE_OLD_LOADEVENTS is not set, we serialize only the "Staging"
// into a JS object.
const UntrustedModulesBackupData& stagingRef = mBackupSvc->Staging();
if (stagingRef.IsEmpty()) {
aPromise->MaybeReject(NS_ERROR_NOT_AVAILABLE);
return;
}
rv = serializer.Add(stagingRef);
if (NS_WARN_IF(NS_FAILED(rv))) {
aPromise->MaybeReject(rv);
return;
}
// When KEEP_LOADEVENTS_NEW is not set, we move all "Staging" instances
// to the "Settled".
if (!(mFlags & nsITelemetry::KEEP_LOADEVENTS_NEW)) {
mBackupSvc->SettleAllStagingData();
}
}
#if defined(XP_WIN)
nsTArray<nsDependentSubstring> blockedModules = GetBlockedModules();
if (!blockedModules.IsEmpty()) {
rv = serializer.AddBlockedModules(blockedModules);
if (NS_WARN_IF(NS_FAILED(rv))) {
aPromise->MaybeReject(rv);
return;
}
}
#endif
JS::Rooted<JS::Value> jsval(cx);
serializer.GetObject(&jsval);
aPromise->MaybeResolve(jsval);
}
nsresult MaybeSubmitAndGetUntrustedModulePayload(JSContext* aCx,
dom::Promise** aPromise,
uint32_t aFlags,
bool aSubmitGleanPing) {
// Create a promise using global context.
nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
if (NS_WARN_IF(!global)) {
return NS_ERROR_FAILURE;
}
ErrorResult result;
RefPtr<dom::Promise> promise(dom::Promise::Create(global, result));
if (NS_WARN_IF(result.Failed())) {
return result.StealNSResult();
}
auto multi = MakeRefPtr<MultiGetUntrustedModulesData>(aFlags);
multi->GetUntrustedModuleLoadEvents()->Then(
GetMainThreadSerialEventTarget(), __func__,
[promise, multi, aSubmitGleanPing](bool) mutable {
if (aSubmitGleanPing) {
nsresult rv = multi->SubmitToGlean();
if (NS_FAILED(rv)) {
promise->MaybeReject(rv);
return;
}
}
multi->Serialize(std::move(promise));
},
[promise](nsresult aRv) { promise->MaybeReject(aRv); });
promise.forget(aPromise);
return NS_OK;
}
nsresult SubmitAndGetUntrustedModulePayload(JSContext* aCx,
dom::Promise** aPromise) {
return MaybeSubmitAndGetUntrustedModulePayload(aCx, aPromise, 0, true);
}
nsresult GetUntrustedModuleLoadEvents(uint32_t aFlags, JSContext* aCx,
dom::Promise** aPromise) {
return MaybeSubmitAndGetUntrustedModulePayload(aCx, aPromise, aFlags, false);
}
} // namespace Telemetry
} // namespace mozilla