/* -*- 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; } // 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 static bool VectorToJSArray(JSContext* cx, JS::MutableHandleObject aRet, const Vector& 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(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& 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(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(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(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& 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(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&& aData, RefPtr&& 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, ipc::ResponseRejectReason, true>; using MultiGetUntrustedModulesPromise = MozPromise, nsresult, true>; class MOZ_HEAP_CLASS MultiGetUntrustedModulesData final { public: MultiGetUntrustedModulesData() : mPromise(new MultiGetUntrustedModulesPromise::Private(__func__)), mNumPending(0) {} NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MultiGetUntrustedModulesData) RefPtr GetUntrustedModuleLoadEvents(); MultiGetUntrustedModulesData(const MultiGetUntrustedModulesData&) = delete; MultiGetUntrustedModulesData(MultiGetUntrustedModulesData&&) = delete; MultiGetUntrustedModulesData& operator=(const MultiGetUntrustedModulesData&) = delete; MultiGetUntrustedModulesData& operator=(MultiGetUntrustedModulesData&&) = delete; private: ~MultiGetUntrustedModulesData() = default; void AddPending(RefPtr&& aNewPending) { MOZ_ASSERT(NS_IsMainThread()); ++mNumPending; RefPtr self(this); aNewPending->Then( GetMainThreadSerialEventTarget(), __func__, [self](Maybe&& aResult) { self->OnCompletion(std::move(aResult)); }, [self](nsresult aReason) { self->OnCompletion(); }); } void AddPending(RefPtr&& aNewPending) { MOZ_ASSERT(NS_IsMainThread()); ++mNumPending; RefPtr self(this); aNewPending->Then( GetMainThreadSerialEventTarget(), __func__, [self](Maybe&& 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&& aResult) { MOZ_ASSERT(NS_IsMainThread()); if (aResult.isSome()) { Unused << mResults.emplaceBack(std::move(aResult.ref())); } OnCompletion(); } private: RefPtr mPromise; Vector mResults; size_t mNumPending; }; RefPtr MultiGetUntrustedModulesData::GetUntrustedModuleLoadEvents() { MOZ_ASSERT(XRE_IsParentProcess() && NS_IsMainThread()); // Parent process RefPtr dllSvc(DllServices::Get()); AddPending(dllSvc->GetUntrustedModulesData()); // Child processes nsTArray 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 promise(dom::Promise::Create(global, result)); if (NS_WARN_IF(result.Failed())) { return result.StealNSResult(); } RefPtr multi( new MultiGetUntrustedModulesData()); multi->GetUntrustedModuleLoadEvents()->Then( GetMainThreadSerialEventTarget(), __func__, [promise](Vector&& 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