fune/toolkit/components/telemetry/other/UntrustedModules.cpp

648 lines
20 KiB
C++

/* -*- 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 http://mozilla.org/MPL/2.0/. */
#include "UntrustedModules.h"
#include "core/TelemetryCommon.h"
#include "js/Array.h" // JS::NewArrayObject
#include "mozilla/dom/ContentParent.h"
#include "mozilla/MozPromise.h"
#include "mozilla/RDDChild.h"
#include "mozilla/RDDProcessManager.h"
#include "mozilla/UntrustedModulesProcessor.h"
#include "mozilla/WinDllServices.h"
#include "nsCOMPtr.h"
#include "nsDataHashtable.h"
#include "nsISupportsImpl.h"
#include "nsLocalFile.h"
#include "nsProxyRelease.h"
#include "nsUnicharUtils.h"
#include "nsXULAppAPI.h"
namespace {
using IndexMap = nsDataHashtable<nsStringHashKey, uint32_t>;
} // anonymous namespace
namespace mozilla {
namespace Telemetry {
static const uint32_t kThirdPartyModulesPingVersion = 1;
static const uint32_t kMaxModulesArrayLen = 100;
/**
* Limits the length of a string by removing the middle of the string, replacing
* with ellipsis.
* e.g. LimitStringLength("hello world", 6) would result in "he...d"
*
* @param aStr [in,out] The string to transform
* @param aMaxFieldLength [in] The maximum length of the resulting string.
*/
static void LimitStringLength(nsAString& aStr, size_t aMaxFieldLength) {
if (aStr.Length() <= aMaxFieldLength) {
return;
}
constexpr auto kEllipsis = u"..."_ns;
if (aMaxFieldLength <= (kEllipsis.Length() + 3)) {
// An ellipsis is useless in this case, as it would obscure the string to
// the point that we cannot even determine the string's contents. We might
// as well just truncate.
aStr.Truncate(aMaxFieldLength);
return;
}
size_t cutPos = (aMaxFieldLength - kEllipsis.Length()) / 2;
size_t rightLen = aMaxFieldLength - kEllipsis.Length() - cutPos;
size_t cutLen = aStr.Length() - (cutPos + rightLen);
aStr.Replace(cutPos, cutLen, kEllipsis);
}
/**
* Adds a string property to a JS object, that's limited in length using
* LimitStringLength().
*
* @param cx [in] The JS context
* @param aObj [in] The object to add the property to
* @param aName [in] The name of the property to add
* @param aVal [in] The JS value of the resulting property.
* @param aMaxFieldLength [in] The maximum length of the value
* (see LimitStringLength())
* @return true upon success
*/
static bool AddLengthLimitedStringProp(JSContext* cx, JS::HandleObject aObj,
const char* aName, const nsAString& aVal,
size_t aMaxFieldLength = MAX_PATH) {
JS::RootedValue jsval(cx);
nsAutoString shortVal(aVal);
LimitStringLength(shortVal, aMaxFieldLength);
jsval.setString(Common::ToJSString(cx, shortVal));
return JS_DefineProperty(cx, aObj, aName, jsval, JSPROP_ENUMERATE);
};
static JSString* ModuleVersionToJSString(JSContext* aCx,
const ModuleVersion& aVersion) {
uint16_t major, minor, patch, build;
Tie(major, minor, patch, build) = aVersion.AsTuple();
constexpr auto dot = u"."_ns;
nsAutoString strVer;
strVer.AppendInt(major);
strVer.Append(dot);
strVer.AppendInt(minor);
strVer.Append(dot);
strVer.AppendInt(patch);
strVer.Append(dot);
strVer.AppendInt(build);
return Common::ToJSString(aCx, strVer);
}
/**
* Convert the given mozilla::Vector to a JavaScript array.
*
* @param cx [in] The JS context.
* @param aRet [out] This gets assigned to the newly created
* array object.
* @param aContainer [in] The source array to convert.
* @param aElementConverter [in] A callable used to convert each array element
* to a JS element. The form of this function is:
* bool(JSContext *cx,
* JS::MutableHandleValue aRet,
* const ArrayElementT& aElement)
* @return true if aRet was successfully assigned to the new array object.
*/
template <typename T, size_t N, typename AllocPolicy, typename Converter,
typename... Args>
static bool VectorToJSArray(JSContext* cx, JS::MutableHandleObject aRet,
const Vector<T, N, AllocPolicy>& aContainer,
Converter&& aElementConverter, Args&&... aArgs) {
JS::RootedObject arr(cx, JS::NewArrayObject(cx, 0));
if (!arr) {
return false;
}
for (size_t i = 0, l = aContainer.length(); i < l; ++i) {
JS::RootedValue jsel(cx);
if (!aElementConverter(cx, &jsel, aContainer[i],
std::forward<Args>(aArgs)...)) {
return false;
}
if (!JS_DefineElement(cx, arr, i, jsel, JSPROP_ENUMERATE)) {
return false;
}
}
aRet.set(arr);
return true;
}
static bool SerializeModule(JSContext* aCx, JS::MutableHandleValue aElement,
const RefPtr<ModuleRecord>& aModule) {
if (!aModule) {
return false;
}
JS::RootedObject obj(aCx, JS_NewPlainObject(aCx));
if (!obj) {
return false;
}
if (!AddLengthLimitedStringProp(aCx, obj, "resolvedDllName",
aModule->mSanitizedDllName)) {
return false;
}
if (aModule->mVersion.isSome()) {
JS::RootedValue jsModuleVersion(aCx);
jsModuleVersion.setString(
ModuleVersionToJSString(aCx, aModule->mVersion.ref()));
if (!JS_DefineProperty(aCx, obj, "fileVersion", jsModuleVersion,
JSPROP_ENUMERATE)) {
return false;
}
}
if (aModule->mVendorInfo.isSome()) {
const char* propName;
const VendorInfo& vendorInfo = aModule->mVendorInfo.ref();
switch (vendorInfo.mSource) {
case VendorInfo::Source::Signature:
propName = "signedBy";
break;
case VendorInfo::Source::VersionInfo:
propName = "companyName";
break;
default:
MOZ_ASSERT_UNREACHABLE("Unknown VendorInfo Source!");
return false;
}
MOZ_ASSERT(!vendorInfo.mVendor.IsEmpty());
if (vendorInfo.mVendor.IsEmpty()) {
return false;
}
if (!AddLengthLimitedStringProp(aCx, obj, propName, vendorInfo.mVendor)) {
return false;
}
}
JS::RootedValue jsTrustFlags(aCx);
jsTrustFlags.setNumber(static_cast<uint32_t>(aModule->mTrustFlags));
if (!JS_DefineProperty(aCx, obj, "trustFlags", jsTrustFlags,
JSPROP_ENUMERATE)) {
return false;
}
aElement.setObject(*obj);
return true;
}
static bool SerializeEvent(JSContext* aCx, JS::MutableHandleValue aElement,
const ProcessedModuleLoadEvent& aEvent,
const IndexMap& aModuleIndices) {
MOZ_ASSERT(NS_IsMainThread());
if (!aEvent) {
return false;
}
JS::RootedObject obj(aCx, JS_NewPlainObject(aCx));
if (!obj) {
return false;
}
JS::RootedValue jsProcessUptimeMS(aCx);
// Javascript doesn't like 64-bit integers; convert to double.
jsProcessUptimeMS.setNumber(static_cast<double>(aEvent.mProcessUptimeMS));
if (!JS_DefineProperty(aCx, obj, "processUptimeMS", jsProcessUptimeMS,
JSPROP_ENUMERATE)) {
return false;
}
if (aEvent.mLoadDurationMS) {
JS::RootedValue jsLoadDurationMS(aCx);
jsLoadDurationMS.setNumber(aEvent.mLoadDurationMS.value());
if (!JS_DefineProperty(aCx, obj, "loadDurationMS", jsLoadDurationMS,
JSPROP_ENUMERATE)) {
return false;
}
}
JS::RootedValue jsThreadId(aCx);
jsThreadId.setNumber(static_cast<uint32_t>(aEvent.mThreadId));
if (!JS_DefineProperty(aCx, obj, "threadID", jsThreadId, JSPROP_ENUMERATE)) {
return false;
}
nsDependentCString effectiveThreadName;
if (aEvent.mThreadId == ::GetCurrentThreadId()) {
effectiveThreadName.Rebind("Main Thread"_ns, 0);
} else {
effectiveThreadName.Rebind(aEvent.mThreadName, 0);
}
if (!effectiveThreadName.IsEmpty()) {
JS::RootedValue jsThreadName(aCx);
jsThreadName.setString(Common::ToJSString(aCx, effectiveThreadName));
if (!JS_DefineProperty(aCx, obj, "threadName", jsThreadName,
JSPROP_ENUMERATE)) {
return false;
}
}
// Don't add this property unless mRequestedDllName differs from
// the associated module's mSanitizedDllName
if (!aEvent.mRequestedDllName.IsEmpty() &&
!aEvent.mRequestedDllName.Equals(aEvent.mModule->mSanitizedDllName,
nsCaseInsensitiveStringComparator)) {
if (!AddLengthLimitedStringProp(aCx, obj, "requestedDllName",
aEvent.mRequestedDllName)) {
return false;
}
}
nsAutoString strBaseAddress;
strBaseAddress.AppendLiteral(u"0x");
strBaseAddress.AppendInt(aEvent.mBaseAddress, 16);
JS::RootedValue jsBaseAddress(aCx);
jsBaseAddress.setString(Common::ToJSString(aCx, strBaseAddress));
if (!JS_DefineProperty(aCx, obj, "baseAddress", jsBaseAddress,
JSPROP_ENUMERATE)) {
return false;
}
uint32_t index;
if (!aModuleIndices.Get(aEvent.mModule->mResolvedNtName, &index)) {
return false;
}
JS::RootedValue jsModuleIndex(aCx);
jsModuleIndex.setNumber(index);
if (!JS_DefineProperty(aCx, obj, "moduleIndex", jsModuleIndex,
JSPROP_ENUMERATE)) {
return false;
}
JS::RootedValue jsIsDependent(aCx);
jsIsDependent.setBoolean(aEvent.mIsDependent);
if (!JS_DefineProperty(aCx, obj, "isDependent", jsIsDependent,
JSPROP_ENUMERATE)) {
return false;
}
aElement.setObject(*obj);
return true;
}
static nsresult GetPerProcObject(JSContext* aCx, const IndexMap& aModuleIndices,
const UntrustedModulesData& aData,
JS::MutableHandleObject aObj) {
nsDependentCString strProcType;
if (aData.mProcessType == GeckoProcessType_Default) {
strProcType.Rebind("browser"_ns, 0);
} else {
strProcType.Rebind(XRE_GeckoProcessTypeToString(aData.mProcessType));
}
JS::RootedValue jsProcType(aCx);
jsProcType.setString(Common::ToJSString(aCx, strProcType));
if (!JS_DefineProperty(aCx, aObj, "processType", jsProcType,
JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
JS::RootedValue jsElapsed(aCx);
jsElapsed.setNumber(aData.mElapsed.ToSecondsSigDigits());
if (!JS_DefineProperty(aCx, aObj, "elapsed", jsElapsed, JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
if (aData.mXULLoadDurationMS.isSome()) {
JS::RootedValue jsXulLoadDurationMS(aCx);
jsXulLoadDurationMS.setNumber(aData.mXULLoadDurationMS.value());
if (!JS_DefineProperty(aCx, aObj, "xulLoadDurationMS", jsXulLoadDurationMS,
JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
}
JS::RootedValue jsSanitizationFailures(aCx);
jsSanitizationFailures.setNumber(aData.mSanitizationFailures);
if (!JS_DefineProperty(aCx, aObj, "sanitizationFailures",
jsSanitizationFailures, JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
JS::RootedValue jsTrustTestFailures(aCx);
jsTrustTestFailures.setNumber(aData.mTrustTestFailures);
if (!JS_DefineProperty(aCx, aObj, "trustTestFailures", jsTrustTestFailures,
JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
JS::RootedObject eventsArray(aCx);
if (!VectorToJSArray(aCx, &eventsArray, aData.mEvents, &SerializeEvent,
aModuleIndices)) {
return NS_ERROR_FAILURE;
}
if (!JS_DefineProperty(aCx, aObj, "events", eventsArray, JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
JS::RootedObject combinedStacksObj(aCx,
CreateJSStackObject(aCx, aData.mStacks));
if (!combinedStacksObj) {
return NS_ERROR_FAILURE;
}
if (!JS_DefineProperty(aCx, aObj, "combinedStacks", combinedStacksObj,
JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
return NS_OK;
}
/**
* Converts a UntrustedModulesData to a JS object.
*
* @param aData [in] The source objects to convert.
* @param aCx [in] The JS context.
* @param aRet [out] This gets assigned to the newly created object.
* @return nsresult
*/
static nsresult GetUntrustedModuleLoadEventsJSValue(
const Vector<UntrustedModulesData>& aData, JSContext* aCx,
JS::MutableHandleValue aRet) {
if (aData.empty()) {
return NS_ERROR_NOT_AVAILABLE;
}
JS::RootedObject mainObj(aCx, JS_NewPlainObject(aCx));
if (!mainObj) {
return NS_ERROR_FAILURE;
}
JS::RootedValue jsVersion(aCx);
jsVersion.setNumber(kThirdPartyModulesPingVersion);
if (!JS_DefineProperty(aCx, mainObj, "structVersion", jsVersion,
JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
IndexMap indexMap;
uint32_t curModulesArrayIdx = 0;
JS::RootedObject modulesArray(aCx, JS::NewArrayObject(aCx, 0));
if (!modulesArray) {
return NS_ERROR_FAILURE;
}
JS::RootedObject perProcObjContainer(aCx, JS_NewPlainObject(aCx));
if (!perProcObjContainer) {
return NS_ERROR_FAILURE;
}
for (auto&& procData : aData) {
// Serialize each entry in the modules hashtable out to the "modules" array
// and store the indices in |indexMap|
for (auto iter = procData.mModules.ConstIter(); !iter.Done(); iter.Next()) {
auto addPtr = indexMap.LookupForAdd(iter.Key());
if (!addPtr) {
addPtr.OrInsert([curModulesArrayIdx]() { return curModulesArrayIdx; });
JS::RootedValue jsModule(aCx);
if (!SerializeModule(aCx, &jsModule, iter.Data()) ||
!JS_DefineElement(aCx, modulesArray, curModulesArrayIdx, jsModule,
JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
++curModulesArrayIdx;
}
}
if (curModulesArrayIdx >= kMaxModulesArrayLen) {
return NS_ERROR_CANNOT_CONVERT_DATA;
}
JS::RootedObject perProcObj(aCx, JS_NewPlainObject(aCx));
if (!perProcObj) {
return NS_ERROR_FAILURE;
}
nsresult rv = GetPerProcObject(aCx, indexMap, procData, &perProcObj);
if (NS_FAILED(rv)) {
return rv;
}
nsAutoCString strPid;
strPid.AppendLiteral("0x");
strPid.AppendInt(static_cast<uint32_t>(procData.mPid), 16);
JS::RootedValue jsPerProcObjValue(aCx);
jsPerProcObjValue.setObject(*perProcObj);
if (!JS_DefineProperty(aCx, perProcObjContainer, strPid.get(),
jsPerProcObjValue, JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
}
JS::RootedValue jsModulesArrayValue(aCx);
jsModulesArrayValue.setObject(*modulesArray);
if (!JS_DefineProperty(aCx, mainObj, "modules", jsModulesArrayValue,
JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
JS::RootedValue jsPerProcObjContainerValue(aCx);
jsPerProcObjContainerValue.setObject(*perProcObjContainer);
if (!JS_DefineProperty(aCx, mainObj, "processes", jsPerProcObjContainerValue,
JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
aRet.setObject(*mainObj);
return NS_OK;
}
static void Serialize(Vector<UntrustedModulesData>&& aData,
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;
}
if (aData.empty()) {
aPromise->MaybeReject(NS_ERROR_NOT_AVAILABLE);
return;
}
JSContext* cx = jsapi.cx();
JS::RootedValue jsval(cx);
nsresult rv = GetUntrustedModuleLoadEventsJSValue(aData, cx, &jsval);
if (NS_WARN_IF(NS_FAILED(rv))) {
aPromise->MaybeReject(rv);
return;
}
aPromise->MaybeResolve(jsval);
}
using UntrustedModulesIpcPromise =
MozPromise<Maybe<UntrustedModulesData>, ipc::ResponseRejectReason, true>;
using MultiGetUntrustedModulesPromise =
MozPromise<Vector<UntrustedModulesData>, nsresult, true>;
class MOZ_HEAP_CLASS MultiGetUntrustedModulesData final {
public:
MultiGetUntrustedModulesData()
: mPromise(new MultiGetUntrustedModulesPromise::Private(__func__)),
mNumPending(0) {}
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MultiGetUntrustedModulesData)
RefPtr<MultiGetUntrustedModulesPromise> GetUntrustedModuleLoadEvents();
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;
}
if (mResults.empty()) {
mPromise->Reject(NS_ERROR_NOT_AVAILABLE, __func__);
return;
}
mPromise->Resolve(std::move(mResults), __func__);
}
void OnCompletion(Maybe<UntrustedModulesData>&& aResult) {
MOZ_ASSERT(NS_IsMainThread());
if (aResult.isSome()) {
Unused << mResults.emplaceBack(std::move(aResult.ref()));
}
OnCompletion();
}
private:
RefPtr<MultiGetUntrustedModulesPromise::Private> mPromise;
Vector<UntrustedModulesData> mResults;
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 (RDDProcessManager* rddMgr = RDDProcessManager::Get()) {
if (RDDChild* rddChild = rddMgr->GetRDDChild()) {
AddPending(rddChild->SendGetUntrustedModulesData());
}
}
Unused << mResults.reserve(mNumPending);
return mPromise;
}
nsresult GetUntrustedModuleLoadEvents(JSContext* cx, dom::Promise** aPromise) {
// Create a promise using global context.
nsIGlobalObject* global = xpc::CurrentNativeGlobal(cx);
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();
}
RefPtr<MultiGetUntrustedModulesData> multi(
new MultiGetUntrustedModulesData());
multi->GetUntrustedModuleLoadEvents()->Then(
GetMainThreadSerialEventTarget(), __func__,
[promise](Vector<UntrustedModulesData>&& aData) mutable {
Serialize(std::move(aData), std::move(promise));
},
[promise](nsresult aRv) { promise->MaybeReject(aRv); });
promise.forget(aPromise);
return NS_OK;
}
} // namespace Telemetry
} // namespace mozilla