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
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "UntrustedModulesDataSerializer.h"
#include "core/TelemetryCommon.h"
#include "js/Array.h" // JS::NewArrayObject
#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineProperty, JS_GetProperty
#include "jsapi.h"
#include "mozilla/dom/ToJSValue.h"
#include "nsITelemetry.h"
#include "nsUnicharUtils.h"
#include "nsXULAppAPI.h"
#include "SharedLibraries.h"
namespace mozilla {
namespace Telemetry {
static const uint32_t kThirdPartyModulesPingVersion = 1;
/**
* 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::Handle<JSObject*> aObj,
const char* aName, const nsAString& aVal,
size_t aMaxFieldLength = MAX_PATH) {
JS::Rooted<JS::Value> 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) {
auto [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 container object 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 container to convert.
* @param aElementConverter [in] A callable used to convert each element
* to a JS element. The form of this function is:
* bool(JSContext *cx,
* JS::MutableHandleValue aRet,
* const ElementT& aElement)
* @return true if aRet was successfully assigned to the new array object.
*/
template <typename T, typename Converter, typename... Args>
static bool ContainerToJSArray(JSContext* cx, JS::MutableHandle<JSObject*> aRet,
const T& aContainer,
Converter&& aElementConverter, Args&&... aArgs) {
JS::Rooted<JSObject*> arr(cx, JS::NewArrayObject(cx, 0));
if (!arr) {
return false;
}
size_t i = 0;
for (auto&& item : aContainer) {
JS::Rooted<JS::Value> jsel(cx);
if (!aElementConverter(cx, &jsel, *item, std::forward<Args>(aArgs)...)) {
return false;
}
if (!JS_DefineElement(cx, arr, i, jsel, JSPROP_ENUMERATE)) {
return false;
}
++i;
}
aRet.set(arr);
return true;
}
static bool SerializeModule(JSContext* aCx,
JS::MutableHandle<JS::Value> aElement,
const RefPtr<ModuleRecord>& aModule,
uint32_t aFlags) {
if (!aModule) {
return false;
}
JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
if (!obj) {
return false;
}
if (aFlags & nsITelemetry::INCLUDE_PRIVATE_FIELDS_IN_LOADEVENTS) {
JS::Rooted<JS::Value> jsFileObj(aCx);
if (!dom::ToJSValue(aCx, aModule->mResolvedDosName, &jsFileObj) ||
!JS_DefineProperty(aCx, obj, "dllFile", jsFileObj, JSPROP_ENUMERATE)) {
return false;
}
} else {
if (!AddLengthLimitedStringProp(aCx, obj, "resolvedDllName",
aModule->mSanitizedDllName)) {
return false;
}
}
if (aModule->mVersion.isSome()) {
JS::Rooted<JS::Value> jsModuleVersion(aCx);
jsModuleVersion.setString(
ModuleVersionToJSString(aCx, aModule->mVersion.ref()));
if (!JS_DefineProperty(aCx, obj, "fileVersion", jsModuleVersion,
JSPROP_ENUMERATE)) {
return false;
}
}
#if defined(MOZ_GECKO_PROFILER)
if (aModule->mResolvedDosName) {
nsAutoString path;
if (aModule->mResolvedDosName->GetPath(path) == NS_OK) {
SharedLibraryInfo info = SharedLibraryInfo::GetInfoFromPath(path.Data());
if (info.GetSize() > 0) {
nsString breakpadId =
NS_ConvertUTF8toUTF16(info.GetEntry(0).GetBreakpadId());
if (!AddLengthLimitedStringProp(aCx, obj, "debugID", breakpadId)) {
return false;
}
}
}
}
#endif // MOZ_GECKO_PROFILER
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::Rooted<JS::Value> 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 UntrustedModulesDataSerializer::SerializeEvent(
JSContext* aCx, JS::MutableHandle<JS::Value> aElement,
const ProcessedModuleLoadEventContainer& aEventContainer,
const IndexMap& aModuleIndices) {
MOZ_ASSERT(NS_IsMainThread());
const ProcessedModuleLoadEvent& event = aEventContainer.mEvent;
if (!event) {
return false;
}
JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
if (!obj) {
return false;
}
JS::Rooted<JS::Value> jsProcessUptimeMS(aCx);
// Javascript doesn't like 64-bit integers; convert to double.
jsProcessUptimeMS.setNumber(static_cast<double>(event.mProcessUptimeMS));
if (!JS_DefineProperty(aCx, obj, "processUptimeMS", jsProcessUptimeMS,
JSPROP_ENUMERATE)) {
return false;
}
if (event.mLoadDurationMS) {
JS::Rooted<JS::Value> jsLoadDurationMS(aCx);
jsLoadDurationMS.setNumber(event.mLoadDurationMS.value());
if (!JS_DefineProperty(aCx, obj, "loadDurationMS", jsLoadDurationMS,
JSPROP_ENUMERATE)) {
return false;
}
}
JS::Rooted<JS::Value> jsThreadId(aCx);
jsThreadId.setNumber(static_cast<uint32_t>(event.mThreadId));
if (!JS_DefineProperty(aCx, obj, "threadID", jsThreadId, JSPROP_ENUMERATE)) {
return false;
}
nsDependentCString effectiveThreadName;
if (event.mThreadId == ::GetCurrentThreadId()) {
effectiveThreadName.Rebind("Main Thread"_ns, 0);
} else {
effectiveThreadName.Rebind(event.mThreadName, 0);
}
if (!effectiveThreadName.IsEmpty()) {
JS::Rooted<JS::Value> 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 (!event.mRequestedDllName.IsEmpty() &&
!event.mRequestedDllName.Equals(event.mModule->mSanitizedDllName,
nsCaseInsensitiveStringComparator)) {
if (!AddLengthLimitedStringProp(aCx, obj, "requestedDllName",
event.mRequestedDllName)) {
return false;
}
}
nsAutoString strBaseAddress;
strBaseAddress.AppendLiteral(u"0x");
strBaseAddress.AppendInt(event.mBaseAddress, 16);
JS::Rooted<JS::Value> 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(event.mModule->mResolvedNtName, &index)) {
return false;
}
JS::Rooted<JS::Value> jsModuleIndex(aCx);
jsModuleIndex.setNumber(index);
if (!JS_DefineProperty(aCx, obj, "moduleIndex", jsModuleIndex,
JSPROP_ENUMERATE)) {
return false;
}
JS::Rooted<JS::Value> jsIsDependent(aCx);
jsIsDependent.setBoolean(event.mIsDependent);
if (!JS_DefineProperty(aCx, obj, "isDependent", jsIsDependent,
JSPROP_ENUMERATE)) {
return false;
}
JS::Rooted<JS::Value> jsLoadStatus(aCx);
jsLoadStatus.setNumber(event.mLoadStatus);
if (!JS_DefineProperty(aCx, obj, "loadStatus", jsLoadStatus,
JSPROP_ENUMERATE)) {
return false;
}
aElement.setObject(*obj);
return true;
}
static nsDependentCString GetProcessTypeString(GeckoProcessType aType) {
nsDependentCString strProcType;
if (aType == GeckoProcessType_Default) {
strProcType.Rebind("browser"_ns, 0);
} else {
strProcType.Rebind(XRE_GeckoProcessTypeToString(aType));
}
return strProcType;
}
nsresult UntrustedModulesDataSerializer::GetPerProcObject(
const UntrustedModulesData& aData, JS::MutableHandle<JSObject*> aObj) {
JS::Rooted<JS::Value> jsProcType(mCx);
jsProcType.setString(
Common::ToJSString(mCx, GetProcessTypeString(aData.mProcessType)));
if (!JS_DefineProperty(mCx, aObj, "processType", jsProcType,
JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
JS::Rooted<JS::Value> jsElapsed(mCx);
jsElapsed.setNumber(aData.mElapsed.ToSecondsSigDigits());
if (!JS_DefineProperty(mCx, aObj, "elapsed", jsElapsed, JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
if (aData.mXULLoadDurationMS.isSome()) {
JS::Rooted<JS::Value> jsXulLoadDurationMS(mCx);
jsXulLoadDurationMS.setNumber(aData.mXULLoadDurationMS.value());
if (!JS_DefineProperty(mCx, aObj, "xulLoadDurationMS", jsXulLoadDurationMS,
JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
}
JS::Rooted<JS::Value> jsSanitizationFailures(mCx);
jsSanitizationFailures.setNumber(aData.mSanitizationFailures);
if (!JS_DefineProperty(mCx, aObj, "sanitizationFailures",
jsSanitizationFailures, JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
JS::Rooted<JS::Value> jsTrustTestFailures(mCx);
jsTrustTestFailures.setNumber(aData.mTrustTestFailures);
if (!JS_DefineProperty(mCx, aObj, "trustTestFailures", jsTrustTestFailures,
JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
JS::Rooted<JSObject*> eventsArray(mCx);
if (!ContainerToJSArray(mCx, &eventsArray, aData.mEvents, &SerializeEvent,
mIndexMap)) {
return NS_ERROR_FAILURE;
}
if (!JS_DefineProperty(mCx, aObj, "events", eventsArray, JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
if (!(mFlags & nsITelemetry::EXCLUDE_STACKINFO_FROM_LOADEVENTS)) {
JS::Rooted<JSObject*> combinedStacksObj(
mCx, CreateJSStackObject(mCx, aData.mStacks));
if (!combinedStacksObj) {
return NS_ERROR_FAILURE;
}
if (!JS_DefineProperty(mCx, aObj, "combinedStacks", combinedStacksObj,
JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
}
return NS_OK;
}
nsresult UntrustedModulesDataSerializer::AddLoadEvents(
const UntrustedModuleLoadingEvents& aEvents,
JS::MutableHandle<JSObject*> aPerProcObj) {
JS::Rooted<JS::Value> eventsArrayVal(mCx);
if (!JS_GetProperty(mCx, aPerProcObj, "events", &eventsArrayVal) ||
!eventsArrayVal.isObject()) {
return NS_ERROR_FAILURE;
}
JS::Rooted<JSObject*> eventsArray(mCx, &eventsArrayVal.toObject());
bool isArray;
if (!JS::IsArrayObject(mCx, eventsArray, &isArray) && !isArray) {
return NS_ERROR_FAILURE;
}
uint32_t currentPos;
if (!GetArrayLength(mCx, eventsArray, &currentPos)) {
return NS_ERROR_FAILURE;
}
for (auto item : aEvents) {
JS::Rooted<JS::Value> jsel(mCx);
if (!SerializeEvent(mCx, &jsel, *item, mIndexMap) ||
!JS_DefineElement(mCx, eventsArray, currentPos++, jsel,
JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
}
return NS_OK;
}
nsresult UntrustedModulesDataSerializer::AddSingleData(
const UntrustedModulesData& aData) {
// Serialize each entry in the modules hashtable out to the "modules" array
// and store the indices in |mIndexMap|
for (const auto& entry : aData.mModules) {
if (!mIndexMap.WithEntryHandle(entry.GetKey(), [&](auto&& addPtr) {
if (!addPtr) {
addPtr.Insert(mCurModulesArrayIdx);
JS::Rooted<JS::Value> jsModule(mCx);
if (!SerializeModule(mCx, &jsModule, entry.GetData(), mFlags) ||
!JS_DefineElement(mCx, mModulesArray, mCurModulesArrayIdx,
jsModule, JSPROP_ENUMERATE)) {
return false;
}
++mCurModulesArrayIdx;
}
return true;
})) {
return NS_ERROR_FAILURE;
}
}
if (mCurModulesArrayIdx >= mMaxModulesArrayLen) {
return NS_ERROR_CANNOT_CONVERT_DATA;
}
nsAutoCString strPid;
strPid.Append(GetProcessTypeString(aData.mProcessType));
strPid.AppendLiteral(".0x");
strPid.AppendInt(static_cast<uint32_t>(aData.mPid), 16);
if (mFlags & nsITelemetry::EXCLUDE_STACKINFO_FROM_LOADEVENTS) {
JS::Rooted<JS::Value> perProcVal(mCx);
if (JS_GetProperty(mCx, mPerProcObjContainer, strPid.get(), &perProcVal) &&
perProcVal.isObject()) {
// If a corresponding per-proc object already exists in the dictionary,
// and we skip to serialize CombinedStacks, we can add loading events
// into the JS object directly.
JS::Rooted<JSObject*> perProcObj(mCx, &perProcVal.toObject());
return AddLoadEvents(aData.mEvents, &perProcObj);
}
}
JS::Rooted<JSObject*> perProcObj(mCx, JS_NewPlainObject(mCx));
if (!perProcObj) {
return NS_ERROR_FAILURE;
}
nsresult rv = GetPerProcObject(aData, &perProcObj);
if (NS_FAILED(rv)) {
return rv;
}
JS::Rooted<JS::Value> jsPerProcObjValue(mCx);
jsPerProcObjValue.setObject(*perProcObj);
if (!JS_DefineProperty(mCx, mPerProcObjContainer, strPid.get(),
jsPerProcObjValue, JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
return NS_OK;
}
UntrustedModulesDataSerializer::UntrustedModulesDataSerializer(
JSContext* aCx, uint32_t aMaxModulesArrayLen, uint32_t aFlags)
: mCtorResult(NS_ERROR_FAILURE),
mCx(aCx),
mMainObj(mCx, JS_NewPlainObject(mCx)),
mModulesArray(mCx, JS::NewArrayObject(mCx, 0)),
mBlockedModulesArray(mCx, JS::NewArrayObject(mCx, 0)),
mPerProcObjContainer(mCx, JS_NewPlainObject(mCx)),
mMaxModulesArrayLen(aMaxModulesArrayLen),
mCurModulesArrayIdx(0),
mCurBlockedModulesArrayIdx(0),
mFlags(aFlags) {
if (!mMainObj || !mModulesArray || !mBlockedModulesArray ||
!mPerProcObjContainer) {
return;
}
JS::Rooted<JS::Value> jsVersion(mCx);
jsVersion.setNumber(kThirdPartyModulesPingVersion);
if (!JS_DefineProperty(mCx, mMainObj, "structVersion", jsVersion,
JSPROP_ENUMERATE)) {
return;
}
JS::Rooted<JS::Value> jsModulesArrayValue(mCx);
jsModulesArrayValue.setObject(*mModulesArray);
if (!JS_DefineProperty(mCx, mMainObj, "modules", jsModulesArrayValue,
JSPROP_ENUMERATE)) {
return;
}
JS::Rooted<JS::Value> jsBlockedModulesArrayValue(mCx);
jsBlockedModulesArrayValue.setObject(*mBlockedModulesArray);
if (!JS_DefineProperty(mCx, mMainObj, "blockedModules",
jsBlockedModulesArrayValue, JSPROP_ENUMERATE)) {
return;
}
JS::Rooted<JS::Value> jsPerProcObjContainerValue(mCx);
jsPerProcObjContainerValue.setObject(*mPerProcObjContainer);
if (!JS_DefineProperty(mCx, mMainObj, "processes", jsPerProcObjContainerValue,
JSPROP_ENUMERATE)) {
return;
}
mCtorResult = NS_OK;
}
UntrustedModulesDataSerializer::operator bool() const {
return NS_SUCCEEDED(mCtorResult);
}
void UntrustedModulesDataSerializer::GetObject(
JS::MutableHandle<JS::Value> aRet) {
aRet.setObject(*mMainObj);
}
nsresult UntrustedModulesDataSerializer::Add(
const UntrustedModulesBackupData& aData) {
if (NS_FAILED(mCtorResult)) {
return mCtorResult;
}
for (const RefPtr<UntrustedModulesDataContainer>& container :
aData.Values()) {
if (!container) {
continue;
}
nsresult rv = AddSingleData(container->mData);
if (NS_FAILED(rv)) {
return rv;
}
}
return NS_OK;
}
nsresult UntrustedModulesDataSerializer::AddBlockedModules(
const nsTArray<nsDependentSubstring>& blockedModules) {
if (NS_FAILED(mCtorResult)) {
return mCtorResult;
}
if (blockedModules.Length() >= mMaxModulesArrayLen) {
return NS_ERROR_CANNOT_CONVERT_DATA;
}
for (const auto& blockedModule : blockedModules) {
JS::Rooted<JS::Value> jsBlockedModule(mCx);
jsBlockedModule.setString(Common::ToJSString(mCx, blockedModule));
if (!JS_DefineElement(mCx, mBlockedModulesArray, mCurBlockedModulesArrayIdx,
jsBlockedModule, JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
++mCurBlockedModulesArrayIdx;
}
return NS_OK;
}
} // namespace Telemetry
} // namespace mozilla