Files
tubestation/toolkit/components/telemetry/other/UntrustedModules.cpp
Aaron Klotz eaf969ed51 Bug 1603714: Do not treat third-party module data integrity error as IPC error; r=mhowell
I still do not yet have a good grasp of the root cause of this data integrity
issue, however I also think that having the browser process crash because of it
is far worse.

This also adds a check in our telemetry processing that the deserialized event
is valid.

We'll probably need to add more diagnostic information in the future, but I'd
like to get rid of the IPC errors in Nightly in time for holidays.

Depends on D58046

Differential Revision: https://phabricator.services.mozilla.com/D58047
2019-12-20 21:09:37 +00:00

642 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;
}
NS_NAMED_LITERAL_STRING(kEllipsis, "...");
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();
NS_NAMED_LITERAL_STRING(dot, ".");
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(NS_LITERAL_CSTRING("Main Thread"), 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;
}
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(NS_LITERAL_CSTRING("browser"), 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