Files
tubestation/toolkit/components/telemetry/other/UntrustedModulesDataSerializer.cpp
Noemi Erli 559ebf9905 Backed out 8 changesets (bug 1634785) for causing bustages in EHABIStackWalk.cpp CLOSED TREE
Backed out changeset 3ea11e90d26d (bug 1634785)
Backed out changeset cb492d775d37 (bug 1634785)
Backed out changeset 1c43270bdcaf (bug 1634785)
Backed out changeset 42d0181c117a (bug 1634785)
Backed out changeset 32b940c88cca (bug 1634785)
Backed out changeset f9b6ae065ffc (bug 1634785)
Backed out changeset 2abc88b08f69 (bug 1634785)
Backed out changeset bf73d2f240f1 (bug 1634785)
2024-09-17 02:48:59 +03:00

607 lines
19 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 "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 "shared-libraries.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) {
nsCString breakpadId = info.GetEntry(0).GetBreakpadId();
if (!AddLengthLimitedStringProp(aCx, obj, "debugID",
NS_ConvertASCIItoUTF16(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