/* -*- 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/. */ // Documentation for libpref is in modules/libpref/docs/index.rst. #include #include #include #include "SharedPrefMap.h" #include "base/basictypes.h" #include "MainThreadUtils.h" #include "mozilla/AppShutdown.h" #include "mozilla/ArenaAllocatorExtensions.h" #include "mozilla/ArenaAllocator.h" #include "mozilla/ArrayUtils.h" #include "mozilla/Attributes.h" #include "mozilla/Components.h" #include "mozilla/dom/PContent.h" #include "mozilla/dom/RemoteType.h" #include "mozilla/HashFunctions.h" #include "mozilla/HashTable.h" #include "mozilla/Logging.h" #include "mozilla/Maybe.h" #include "mozilla/MemoryReporting.h" #include "mozilla/Omnijar.h" #include "mozilla/Preferences.h" #include "mozilla/ProfilerLabels.h" #include "mozilla/ProfilerMarkers.h" #include "mozilla/ResultExtensions.h" #include "mozilla/SchedulerGroup.h" #include "mozilla/ScopeExit.h" #include "mozilla/ServoStyleSet.h" #include "mozilla/SpinEventLoopUntil.h" #include "mozilla/StaticMutex.h" #include "mozilla/StaticPrefsAll.h" #include "mozilla/SyncRunnable.h" #include "mozilla/Telemetry.h" #include "mozilla/TelemetryEventEnums.h" #include "mozilla/UniquePtrExtensions.h" #include "mozilla/URLPreloader.h" #include "mozilla/Variant.h" #include "mozilla/Vector.h" #include "nsAppDirectoryServiceDefs.h" #include "nsCategoryManagerUtils.h" #include "nsClassHashtable.h" #include "nsCOMArray.h" #include "nsCOMPtr.h" #include "nsComponentManagerUtils.h" #include "nsContentUtils.h" #include "nsCRT.h" #include "nsTHashMap.h" #include "nsDirectoryServiceDefs.h" #include "nsIConsoleService.h" #include "nsIFile.h" #include "nsIMemoryReporter.h" #include "nsIObserver.h" #include "nsIObserverService.h" #include "nsIOutputStream.h" #include "nsIPrefBranch.h" #include "nsIPrefLocalizedString.h" #include "nsIRelativeFilePref.h" #include "nsISafeOutputStream.h" #include "nsISimpleEnumerator.h" #include "nsIStringBundle.h" #include "nsISupportsImpl.h" #include "nsISupportsPrimitives.h" #include "nsIZipReader.h" #include "nsNetUtil.h" #include "nsPrintfCString.h" #include "nsQuickSort.h" #include "nsReadableUtils.h" #include "nsRefPtrHashtable.h" #include "nsRelativeFilePref.h" #include "nsString.h" #include "nsTArray.h" #include "nsThreadUtils.h" #include "nsUTF8Utils.h" #include "nsWeakReference.h" #include "nsXPCOMCID.h" #include "nsXPCOM.h" #include "nsXULAppAPI.h" #include "nsZipArchive.h" #include "plbase64.h" #include "PLDHashTable.h" #include "plstr.h" #include "prdtoa.h" #include "prlink.h" #include "xpcpublic.h" #include "js/RootingAPI.h" #ifdef MOZ_BACKGROUNDTASKS # include "mozilla/BackgroundTasks.h" #endif #ifdef DEBUG # include #endif #ifdef MOZ_MEMORY # include "mozmemory.h" #endif #ifdef XP_WIN # include "windows.h" #endif #if defined(MOZ_WIDGET_GTK) # include "mozilla/WidgetUtilsGtk.h" #endif // defined(MOZ_WIDGET_GTK) using namespace mozilla; using ipc::FileDescriptor; #ifdef DEBUG # define ENSURE_PARENT_PROCESS(func, pref) \ do { \ if (MOZ_UNLIKELY(!XRE_IsParentProcess())) { \ nsPrintfCString msg( \ "ENSURE_PARENT_PROCESS: called %s on %s in a non-parent process", \ func, pref); \ NS_ERROR(msg.get()); \ return NS_ERROR_NOT_AVAILABLE; \ } \ } while (0) #else // DEBUG # define ENSURE_PARENT_PROCESS(func, pref) \ if (MOZ_UNLIKELY(!XRE_IsParentProcess())) { \ return NS_ERROR_NOT_AVAILABLE; \ } #endif // DEBUG // Forward declarations. namespace mozilla::StaticPrefs { static void InitAll(); static void StartObservingAlwaysPrefs(); static void InitOncePrefs(); static void InitStaticPrefsFromShared(); static void RegisterOncePrefs(SharedPrefMapBuilder& aBuilder); static void ShutdownAlwaysPrefs(); } // namespace mozilla::StaticPrefs //=========================================================================== // Low-level types and operations //=========================================================================== Atomic sPrefTelemetryEventEnabled(false); typedef nsTArray PrefSaveData; // 1 MB should be enough for everyone. static const uint32_t MAX_PREF_LENGTH = 1 * 1024 * 1024; // Actually, 4kb should be enough for everyone. static const uint32_t MAX_ADVISABLE_PREF_LENGTH = 4 * 1024; // This is used for pref names and string pref values. We encode the string // length, then a '/', then the string chars. This encoding means there are no // special chars that are forbidden or require escaping. static void SerializeAndAppendString(const nsCString& aChars, nsCString& aStr) { aStr.AppendInt(uint64_t(aChars.Length())); aStr.Append('/'); aStr.Append(aChars); } static char* DeserializeString(char* aChars, nsCString& aStr) { char* p = aChars; uint32_t length = strtol(p, &p, 10); MOZ_ASSERT(p[0] == '/'); p++; // move past the '/' aStr.Assign(p, length); p += length; // move past the string itself return p; } // Keep this in sync with PrefValue in parser/src/lib.rs. union PrefValue { // PrefValues within Pref objects own their chars. PrefValues passed around // as arguments don't own their chars. const char* mStringVal; int32_t mIntVal; bool mBoolVal; PrefValue() = default; explicit PrefValue(bool aVal) : mBoolVal(aVal) {} explicit PrefValue(int32_t aVal) : mIntVal(aVal) {} explicit PrefValue(const char* aVal) : mStringVal(aVal) {} bool Equals(PrefType aType, PrefValue aValue) { switch (aType) { case PrefType::String: { if (mStringVal && aValue.mStringVal) { return strcmp(mStringVal, aValue.mStringVal) == 0; } if (!mStringVal && !aValue.mStringVal) { return true; } return false; } case PrefType::Int: return mIntVal == aValue.mIntVal; case PrefType::Bool: return mBoolVal == aValue.mBoolVal; default: MOZ_CRASH("Unhandled enum value"); } } template T Get() const; void Init(PrefType aNewType, PrefValue aNewValue) { if (aNewType == PrefType::String) { MOZ_ASSERT(aNewValue.mStringVal); aNewValue.mStringVal = moz_xstrdup(aNewValue.mStringVal); } *this = aNewValue; } void Clear(PrefType aType) { if (aType == PrefType::String) { free(const_cast(mStringVal)); } // Zero the entire value (regardless of type) via mStringVal. mStringVal = nullptr; } void Replace(bool aHasValue, PrefType aOldType, PrefType aNewType, PrefValue aNewValue) { if (aHasValue) { Clear(aOldType); } Init(aNewType, aNewValue); } void ToDomPrefValue(PrefType aType, dom::PrefValue* aDomValue) { switch (aType) { case PrefType::String: *aDomValue = nsDependentCString(mStringVal); return; case PrefType::Int: *aDomValue = mIntVal; return; case PrefType::Bool: *aDomValue = mBoolVal; return; default: MOZ_CRASH(); } } PrefType FromDomPrefValue(const dom::PrefValue& aDomValue) { switch (aDomValue.type()) { case dom::PrefValue::TnsCString: mStringVal = aDomValue.get_nsCString().get(); return PrefType::String; case dom::PrefValue::Tint32_t: mIntVal = aDomValue.get_int32_t(); return PrefType::Int; case dom::PrefValue::Tbool: mBoolVal = aDomValue.get_bool(); return PrefType::Bool; default: MOZ_CRASH(); } } void SerializeAndAppend(PrefType aType, nsCString& aStr) { switch (aType) { case PrefType::Bool: aStr.Append(mBoolVal ? 'T' : 'F'); break; case PrefType::Int: aStr.AppendInt(mIntVal); break; case PrefType::String: { SerializeAndAppendString(nsDependentCString(mStringVal), aStr); break; } case PrefType::None: default: MOZ_CRASH(); } } static char* Deserialize(PrefType aType, char* aStr, Maybe* aDomValue) { char* p = aStr; switch (aType) { case PrefType::Bool: if (*p == 'T') { *aDomValue = Some(true); } else if (*p == 'F') { *aDomValue = Some(false); } else { *aDomValue = Some(false); NS_ERROR("bad bool pref value"); } p++; return p; case PrefType::Int: { *aDomValue = Some(int32_t(strtol(p, &p, 10))); return p; } case PrefType::String: { nsCString str; p = DeserializeString(p, str); *aDomValue = Some(str); return p; } default: MOZ_CRASH(); } } }; template <> bool PrefValue::Get() const { return mBoolVal; } template <> int32_t PrefValue::Get() const { return mIntVal; } template <> nsDependentCString PrefValue::Get() const { return nsDependentCString(mStringVal); } #ifdef DEBUG const char* PrefTypeToString(PrefType aType) { switch (aType) { case PrefType::None: return "none"; case PrefType::String: return "string"; case PrefType::Int: return "int"; case PrefType::Bool: return "bool"; default: MOZ_CRASH("Unhandled enum value"); } } #endif // Assign to aResult a quoted, escaped copy of aOriginal. static void StrEscape(const char* aOriginal, nsCString& aResult) { if (aOriginal == nullptr) { aResult.AssignLiteral("\"\""); return; } // JavaScript does not allow quotes, slashes, or line terminators inside // strings so we must escape them. ECMAScript defines four line terminators, // but we're only worrying about \r and \n here. We currently feed our pref // script to the JS interpreter as Latin-1 so we won't encounter \u2028 // (line separator) or \u2029 (paragraph separator). // // WARNING: There are hints that we may be moving to storing prefs as utf8. // If we ever feed them to the JS compiler as UTF8 then we'll have to worry // about the multibyte sequences that would be interpreted as \u2028 and // \u2029. const char* p; aResult.Assign('"'); // Paranoid worst case all slashes will free quickly. for (p = aOriginal; *p; ++p) { switch (*p) { case '\n': aResult.AppendLiteral("\\n"); break; case '\r': aResult.AppendLiteral("\\r"); break; case '\\': aResult.AppendLiteral("\\\\"); break; case '\"': aResult.AppendLiteral("\\\""); break; default: aResult.Append(*p); break; } } aResult.Append('"'); } // Mimic the behaviour of nsTStringRepr::ToFloat before bug 840706 to preserve // error case handling for parsing pref strings. Many callers do not check error // codes, so the returned values may be used even if an error is set. // // This method should never return NaN, but may return +-inf if the provided // number is too large to fit in a float. static float ParsePrefFloat(const nsCString& aString, nsresult* aError) { if (aString.IsEmpty()) { *aError = NS_ERROR_ILLEGAL_VALUE; return 0.f; } // PR_strtod does a locale-independent conversion. char* stopped = nullptr; float result = PR_strtod(aString.get(), &stopped); // Defensively avoid potential breakage caused by returning NaN into // unsuspecting code. AFAIK this should never happen as PR_strtod cannot // return NaN as currently configured. if (mozilla::IsNaN(result)) { MOZ_ASSERT_UNREACHABLE("PR_strtod shouldn't return NaN"); *aError = NS_ERROR_ILLEGAL_VALUE; return 0.f; } *aError = (stopped == aString.EndReading()) ? NS_OK : NS_ERROR_ILLEGAL_VALUE; return result; } namespace mozilla { struct PrefsSizes { PrefsSizes() : mHashTable(0), mPrefValues(0), mStringValues(0), mRootBranches(0), mPrefNameArena(0), mCallbacksObjects(0), mCallbacksDomains(0), mMisc(0) {} size_t mHashTable; size_t mPrefValues; size_t mStringValues; size_t mRootBranches; size_t mPrefNameArena; size_t mCallbacksObjects; size_t mCallbacksDomains; size_t mMisc; }; } // namespace mozilla static StaticRefPtr gSharedMap; // Arena for Pref names. // Never access sPrefNameArena directly, always use PrefNameArena() // because it must only be accessed on the Main Thread typedef ArenaAllocator<4096, 1> NameArena; static NameArena* sPrefNameArena; static inline NameArena& PrefNameArena() { MOZ_ASSERT(NS_IsMainThread()); if (!sPrefNameArena) { sPrefNameArena = new NameArena(); } return *sPrefNameArena; } class PrefWrapper; // Three forward declarations for immediately below class Pref; static bool IsPreferenceSanitized(const Pref* const aPref); static bool ShouldSanitizePreference(const Pref* const aPref); // Note that this never changes in the parent process, and is only read in // content processes. static bool gContentProcessPrefsAreInited = false; class Pref { public: explicit Pref(const nsACString& aName) : mName(ArenaStrdup(aName, PrefNameArena()), aName.Length()), mType(static_cast(PrefType::None)), mIsSticky(false), mIsLocked(false), mIsSanitized(false), mHasDefaultValue(false), mHasUserValue(false), mIsSkippedByIteration(false), mDefaultValue(), mUserValue() {} ~Pref() { // There's no need to free mName because it's allocated in memory owned by // sPrefNameArena. mDefaultValue.Clear(Type()); mUserValue.Clear(Type()); } const char* Name() const { return mName.get(); } const nsDependentCString& NameString() const { return mName; } // Types. PrefType Type() const { return static_cast(mType); } void SetType(PrefType aType) { mType = static_cast(aType); } bool IsType(PrefType aType) const { return Type() == aType; } bool IsTypeNone() const { return IsType(PrefType::None); } bool IsTypeString() const { return IsType(PrefType::String); } bool IsTypeInt() const { return IsType(PrefType::Int); } bool IsTypeBool() const { return IsType(PrefType::Bool); } // Other properties. bool IsLocked() const { return mIsLocked; } void SetIsLocked(bool aValue) { mIsLocked = aValue; } bool IsSkippedByIteration() const { return mIsSkippedByIteration; } void SetIsSkippedByIteration(bool aValue) { mIsSkippedByIteration = aValue; } bool IsSticky() const { return mIsSticky; } bool IsSanitized() const { return mIsSanitized; } bool HasDefaultValue() const { return mHasDefaultValue; } bool HasUserValue() const { return mHasUserValue; } template void AddToMap(SharedPrefMapBuilder& aMap) { // Sanitized preferences should never be added to the shared pref map MOZ_ASSERT(!ShouldSanitizePreference(this)); aMap.Add(NameString(), {HasDefaultValue(), HasUserValue(), IsSticky(), IsLocked(), /* isSanitized */ false, IsSkippedByIteration()}, HasDefaultValue() ? mDefaultValue.Get() : T(), HasUserValue() ? mUserValue.Get() : T()); } void AddToMap(SharedPrefMapBuilder& aMap) { if (IsTypeBool()) { AddToMap(aMap); } else if (IsTypeInt()) { AddToMap(aMap); } else if (IsTypeString()) { AddToMap(aMap); } else { MOZ_ASSERT_UNREACHABLE("Unexpected preference type"); } } // Other operations. #define CHECK_SANITIZATION() \ if (IsPreferenceSanitized(this)) { \ if (!sPrefTelemetryEventEnabled.exchange(true)) { \ sPrefTelemetryEventEnabled = true; \ Telemetry::SetEventRecordingEnabled("security"_ns, true); \ } \ Telemetry::RecordEvent( \ Telemetry::EventID::Security_Prefusage_Contentprocess, \ mozilla::Some(Name()), mozilla::Nothing()); \ if (sCrashOnBlocklistedPref) { \ MOZ_CRASH_UNSAFE_PRINTF( \ "Should not access the preference '%s' in the Content Processes", \ Name()); \ } \ } bool GetBoolValue(PrefValueKind aKind = PrefValueKind::User) const { MOZ_ASSERT(IsTypeBool()); MOZ_ASSERT(aKind == PrefValueKind::Default ? HasDefaultValue() : HasUserValue()); CHECK_SANITIZATION(); return aKind == PrefValueKind::Default ? mDefaultValue.mBoolVal : mUserValue.mBoolVal; } int32_t GetIntValue(PrefValueKind aKind = PrefValueKind::User) const { MOZ_ASSERT(IsTypeInt()); MOZ_ASSERT(aKind == PrefValueKind::Default ? HasDefaultValue() : HasUserValue()); CHECK_SANITIZATION(); return aKind == PrefValueKind::Default ? mDefaultValue.mIntVal : mUserValue.mIntVal; } const char* GetBareStringValue( PrefValueKind aKind = PrefValueKind::User) const { MOZ_ASSERT(IsTypeString()); MOZ_ASSERT(aKind == PrefValueKind::Default ? HasDefaultValue() : HasUserValue()); CHECK_SANITIZATION(); return aKind == PrefValueKind::Default ? mDefaultValue.mStringVal : mUserValue.mStringVal; } #undef CHECK_SANITIZATION nsDependentCString GetStringValue( PrefValueKind aKind = PrefValueKind::User) const { return nsDependentCString(GetBareStringValue(aKind)); } void ToDomPref(dom::Pref* aDomPref, bool aIsDestinationWebContentProcess) { MOZ_ASSERT(XRE_IsParentProcess()); aDomPref->name() = mName; aDomPref->isLocked() = mIsLocked; aDomPref->isSanitized() = aIsDestinationWebContentProcess && ShouldSanitizePreference(this); if (mHasDefaultValue) { aDomPref->defaultValue() = Some(dom::PrefValue()); mDefaultValue.ToDomPrefValue(Type(), &aDomPref->defaultValue().ref()); } else { aDomPref->defaultValue() = Nothing(); } if (mHasUserValue && !(aDomPref->isSanitized() && sOmitBlocklistedPrefValues)) { aDomPref->userValue() = Some(dom::PrefValue()); mUserValue.ToDomPrefValue(Type(), &aDomPref->userValue().ref()); } else { aDomPref->userValue() = Nothing(); } MOZ_ASSERT(aDomPref->defaultValue().isNothing() || aDomPref->userValue().isNothing() || (mIsSanitized && sOmitBlocklistedPrefValues) || (aDomPref->defaultValue().ref().type() == aDomPref->userValue().ref().type())); } void FromDomPref(const dom::Pref& aDomPref, bool* aValueChanged) { MOZ_ASSERT(!XRE_IsParentProcess()); MOZ_ASSERT(mName == aDomPref.name()); mIsLocked = aDomPref.isLocked(); mIsSanitized = aDomPref.isSanitized(); const Maybe& defaultValue = aDomPref.defaultValue(); bool defaultValueChanged = false; if (defaultValue.isSome()) { PrefValue value; PrefType type = value.FromDomPrefValue(defaultValue.ref()); if (!ValueMatches(PrefValueKind::Default, type, value)) { // Type() is PrefType::None if it's a newly added pref. This is ok. mDefaultValue.Replace(mHasDefaultValue, Type(), type, value); SetType(type); mHasDefaultValue = true; defaultValueChanged = true; } } // Note: we never clear a default value. const Maybe& userValue = aDomPref.userValue(); bool userValueChanged = false; if (userValue.isSome()) { PrefValue value; PrefType type = value.FromDomPrefValue(userValue.ref()); if (!ValueMatches(PrefValueKind::User, type, value)) { // Type() is PrefType::None if it's a newly added pref. This is ok. mUserValue.Replace(mHasUserValue, Type(), type, value); SetType(type); mHasUserValue = true; userValueChanged = true; } } else if (mHasUserValue) { ClearUserValue(); userValueChanged = true; } if (userValueChanged || (defaultValueChanged && !mHasUserValue)) { *aValueChanged = true; } } void FromWrapper(PrefWrapper& aWrapper); bool HasAdvisablySizedValues() { MOZ_ASSERT(XRE_IsParentProcess()); if (!IsTypeString()) { return true; } if (mHasDefaultValue && strlen(mDefaultValue.mStringVal) > MAX_ADVISABLE_PREF_LENGTH) { return false; } if (mHasUserValue && strlen(mUserValue.mStringVal) > MAX_ADVISABLE_PREF_LENGTH) { return false; } return true; } private: bool ValueMatches(PrefValueKind aKind, PrefType aType, PrefValue aValue) { return IsType(aType) && (aKind == PrefValueKind::Default ? mHasDefaultValue && mDefaultValue.Equals(aType, aValue) : mHasUserValue && mUserValue.Equals(aType, aValue)); } public: void ClearUserValue() { mUserValue.Clear(Type()); mHasUserValue = false; } nsresult SetDefaultValue(PrefType aType, PrefValue aValue, bool aIsSticky, bool aIsLocked, bool* aValueChanged) { // Types must always match when setting the default value. if (!IsType(aType)) { return NS_ERROR_UNEXPECTED; } // Should we set the default value? Only if the pref is not locked, and // doing so would change the default value. if (!IsLocked()) { if (aIsLocked) { SetIsLocked(true); } if (!ValueMatches(PrefValueKind::Default, aType, aValue)) { mDefaultValue.Replace(mHasDefaultValue, Type(), aType, aValue); mHasDefaultValue = true; if (aIsSticky) { mIsSticky = true; } if (!mHasUserValue) { *aValueChanged = true; } // What if we change the default to be the same as the user value? // Should we clear the user value? Currently we don't. } } return NS_OK; } nsresult SetUserValue(PrefType aType, PrefValue aValue, bool aFromInit, bool* aValueChanged) { // If we have a default value, types must match when setting the user // value. if (mHasDefaultValue && !IsType(aType)) { return NS_ERROR_UNEXPECTED; } // Should we clear the user value, if present? Only if the new user value // matches the default value, and the pref isn't sticky, and we aren't // force-setting it during initialization. if (ValueMatches(PrefValueKind::Default, aType, aValue) && !mIsSticky && !aFromInit) { if (mHasUserValue) { ClearUserValue(); if (!IsLocked()) { *aValueChanged = true; } } // Otherwise, should we set the user value? Only if doing so would // change the user value. } else if (!ValueMatches(PrefValueKind::User, aType, aValue)) { mUserValue.Replace(mHasUserValue, Type(), aType, aValue); SetType(aType); // needed because we may have changed the type mHasUserValue = true; if (!IsLocked()) { *aValueChanged = true; } } return NS_OK; } // Prefs are serialized in a manner that mirrors dom::Pref. The two should be // kept in sync. E.g. if something is added to one it should also be added to // the other. (It would be nice to be able to use the code generated from // IPDL for serializing dom::Pref here instead of writing by hand this // serialization/deserialization. Unfortunately, that generated code is // difficult to use directly, outside of the IPDL IPC code.) // // The grammar for the serialized prefs has the following form. // // = ':' ':' ? ':' // ? '\n' // = 'B' | 'I' | 'S' // = 'L' | '-' // = 'S' | '-' // = // = | | // = 'T' | 'F' // = an integer literal accepted by strtol() // = '/' // = any char sequence of length dictated by the preceding // . // // No whitespace is tolerated between tokens. must match the types of // the values. // // The serialization is text-based, rather than binary, for the following // reasons. // // - The size difference wouldn't be much different between text-based and // binary. Most of the space is for strings (pref names and string pref // values), which would be the same in both styles. And other differences // would be minimal, e.g. small integers are shorter in text but long // integers are longer in text. // // - Likewise, speed differences should be negligible. // // - It's much easier to debug a text-based serialization. E.g. you can // print it and inspect it easily in a debugger. // // Examples of unlocked boolean prefs: // - "B--:8/my.bool1:F:T\n" // - "B--:8/my.bool2:F:\n" // - "B--:8/my.bool3::T\n" // // Examples of sanitized, unlocked boolean prefs: // - "B-S:8/my.bool1:F:T\n" // - "B-S:8/my.bool2:F:\n" // - "B-S:8/my.bool3::T\n" // // Examples of locked integer prefs: // - "IL-:7/my.int1:0:1\n" // - "IL-:7/my.int2:123:\n" // - "IL-:7/my.int3::-99\n" // // Examples of unlocked string prefs: // - "S--:10/my.string1:3/abc:4/wxyz\n" // - "S--:10/my.string2:5/1.234:\n" // - "S--:10/my.string3::7/string!\n" void SerializeAndAppend(nsCString& aStr, bool aSanitizeUserValue) { switch (Type()) { case PrefType::Bool: aStr.Append('B'); break; case PrefType::Int: aStr.Append('I'); break; case PrefType::String: { aStr.Append('S'); break; } case PrefType::None: default: MOZ_CRASH(); } aStr.Append(mIsLocked ? 'L' : '-'); aStr.Append(aSanitizeUserValue ? 'S' : '-'); aStr.Append(':'); SerializeAndAppendString(mName, aStr); aStr.Append(':'); if (mHasDefaultValue) { mDefaultValue.SerializeAndAppend(Type(), aStr); } aStr.Append(':'); if (mHasUserValue && !(aSanitizeUserValue && sOmitBlocklistedPrefValues)) { mUserValue.SerializeAndAppend(Type(), aStr); } aStr.Append('\n'); } static char* Deserialize(char* aStr, dom::Pref* aDomPref) { char* p = aStr; // The type. PrefType type; if (*p == 'B') { type = PrefType::Bool; } else if (*p == 'I') { type = PrefType::Int; } else if (*p == 'S') { type = PrefType::String; } else { NS_ERROR("bad pref type"); type = PrefType::None; } p++; // move past the type char // Locked? bool isLocked; if (*p == 'L') { isLocked = true; } else if (*p == '-') { isLocked = false; } else { NS_ERROR("bad pref locked status"); isLocked = false; } p++; // move past the isLocked char // Sanitize? bool isSanitized; if (*p == 'S') { isSanitized = true; } else if (*p == '-') { isSanitized = false; } else { NS_ERROR("bad pref sanitized status"); isSanitized = false; } p++; // move past the isSanitized char MOZ_ASSERT(*p == ':'); p++; // move past the ':' // The pref name. nsCString name; p = DeserializeString(p, name); MOZ_ASSERT(*p == ':'); p++; // move past the ':' preceding the default value Maybe maybeDefaultValue; if (*p != ':') { dom::PrefValue defaultValue; p = PrefValue::Deserialize(type, p, &maybeDefaultValue); } MOZ_ASSERT(*p == ':'); p++; // move past the ':' between the default and user values Maybe maybeUserValue; if (*p != '\n') { dom::PrefValue userValue; p = PrefValue::Deserialize(type, p, &maybeUserValue); } MOZ_ASSERT(*p == '\n'); p++; // move past the '\n' following the user value *aDomPref = dom::Pref(name, isLocked, isSanitized, maybeDefaultValue, maybeUserValue); return p; } void AddSizeOfIncludingThis(MallocSizeOf aMallocSizeOf, PrefsSizes& aSizes) { // Note: mName is allocated in sPrefNameArena, measured elsewhere. aSizes.mPrefValues += aMallocSizeOf(this); if (IsTypeString()) { if (mHasDefaultValue) { aSizes.mStringValues += aMallocSizeOf(mDefaultValue.mStringVal); } if (mHasUserValue) { aSizes.mStringValues += aMallocSizeOf(mUserValue.mStringVal); } } } void RelocateName(NameArena* aArena) { mName.Rebind(ArenaStrdup(mName.get(), *aArena), mName.Length()); } private: nsDependentCString mName; // allocated in sPrefNameArena uint32_t mType : 2; uint32_t mIsSticky : 1; uint32_t mIsLocked : 1; uint32_t mIsSanitized : 1; uint32_t mHasDefaultValue : 1; uint32_t mHasUserValue : 1; uint32_t mIsSkippedByIteration : 1; PrefValue mDefaultValue; PrefValue mUserValue; }; struct PrefHasher { using Key = UniquePtr; using Lookup = const char*; static HashNumber hash(const Lookup aLookup) { return HashString(aLookup); } static bool match(const Key& aKey, const Lookup aLookup) { if (!aLookup || !aKey->Name()) { return false; } return strcmp(aLookup, aKey->Name()) == 0; } }; using PrefWrapperBase = Variant; class MOZ_STACK_CLASS PrefWrapper : public PrefWrapperBase { using SharedPref = const SharedPrefMap::Pref; public: MOZ_IMPLICIT PrefWrapper(Pref* aPref) : PrefWrapperBase(AsVariant(aPref)) {} MOZ_IMPLICIT PrefWrapper(const SharedPrefMap::Pref& aPref) : PrefWrapperBase(AsVariant(aPref)) {} // Types. bool IsType(PrefType aType) const { return Type() == aType; } bool IsTypeNone() const { return IsType(PrefType::None); } bool IsTypeString() const { return IsType(PrefType::String); } bool IsTypeInt() const { return IsType(PrefType::Int); } bool IsTypeBool() const { return IsType(PrefType::Bool); } #define FORWARD(retType, method) \ retType method() const { \ struct Matcher { \ retType operator()(const Pref* aPref) { return aPref->method(); } \ retType operator()(SharedPref& aPref) { return aPref.method(); } \ }; \ return match(Matcher()); \ } FORWARD(bool, IsLocked) FORWARD(bool, IsSanitized) FORWARD(bool, IsSticky) FORWARD(bool, HasDefaultValue) FORWARD(bool, HasUserValue) FORWARD(const char*, Name) FORWARD(nsCString, NameString) FORWARD(PrefType, Type) #undef FORWARD #define FORWARD(retType, method) \ retType method(PrefValueKind aKind = PrefValueKind::User) const { \ struct Matcher { \ PrefValueKind mKind; \ \ retType operator()(const Pref* aPref) { return aPref->method(mKind); } \ retType operator()(SharedPref& aPref) { return aPref.method(mKind); } \ }; \ return match(Matcher{aKind}); \ } FORWARD(bool, GetBoolValue) FORWARD(int32_t, GetIntValue) FORWARD(nsCString, GetStringValue) FORWARD(const char*, GetBareStringValue) #undef FORWARD PrefValue GetValue(PrefValueKind aKind = PrefValueKind::User) const { switch (Type()) { case PrefType::Bool: return PrefValue{GetBoolValue(aKind)}; case PrefType::Int: return PrefValue{GetIntValue(aKind)}; case PrefType::String: return PrefValue{GetBareStringValue(aKind)}; case PrefType::None: // This check will be performed in the above functions; but for NoneType // we need to do it explicitly, then fall-through. if (IsPreferenceSanitized(Name())) { if (!sPrefTelemetryEventEnabled.exchange(true)) { sPrefTelemetryEventEnabled = true; Telemetry::SetEventRecordingEnabled("security"_ns, true); } Telemetry::RecordEvent( Telemetry::EventID::Security_Prefusage_Contentprocess, mozilla::Some(Name()), mozilla::Nothing()); if (sCrashOnBlocklistedPref) { MOZ_CRASH_UNSAFE_PRINTF( "Should not access the preference '%s' in the Content " "Processes", Name()); } } [[fallthrough]]; default: MOZ_ASSERT_UNREACHABLE("Unexpected pref type"); return PrefValue{}; } } Result WantValueKind(PrefType aType, PrefValueKind aKind) const { // WantValueKind may short-circuit GetValue functions and cause them to // return early, before this check occurs in GetFooValue() if (this->is() && IsPreferenceSanitized(this->as())) { if (!sPrefTelemetryEventEnabled.exchange(true)) { sPrefTelemetryEventEnabled = true; Telemetry::SetEventRecordingEnabled("security"_ns, true); } Telemetry::RecordEvent( Telemetry::EventID::Security_Prefusage_Contentprocess, mozilla::Some(Name()), mozilla::Nothing()); if (sCrashOnBlocklistedPref) { MOZ_CRASH_UNSAFE_PRINTF( "Should not access the preference '%s' in the Content Processes", Name()); } } else if (!this->is()) { // While we could use Name() above, and avoid the Variant checks, it // would less efficient than needed and we can instead do a debug-only // assert here to limit the inefficientcy MOZ_ASSERT(!IsPreferenceSanitized(Name()), "We should never have a sanitized SharedPrefMap::Pref."); } if (Type() != aType) { return Err(NS_ERROR_UNEXPECTED); } if (aKind == PrefValueKind::Default || IsLocked() || !HasUserValue()) { if (!HasDefaultValue()) { return Err(NS_ERROR_UNEXPECTED); } return PrefValueKind::Default; } return PrefValueKind::User; } nsresult GetValue(PrefValueKind aKind, bool* aResult) const { PrefValueKind kind; MOZ_TRY_VAR(kind, WantValueKind(PrefType::Bool, aKind)); *aResult = GetBoolValue(kind); return NS_OK; } nsresult GetValue(PrefValueKind aKind, int32_t* aResult) const { PrefValueKind kind; MOZ_TRY_VAR(kind, WantValueKind(PrefType::Int, aKind)); *aResult = GetIntValue(kind); return NS_OK; } nsresult GetValue(PrefValueKind aKind, uint32_t* aResult) const { return GetValue(aKind, reinterpret_cast(aResult)); } nsresult GetValue(PrefValueKind aKind, float* aResult) const { nsAutoCString result; nsresult rv = GetValue(aKind, result); if (NS_SUCCEEDED(rv)) { // ParsePrefFloat() does a locale-independent conversion. // FIXME: Other `GetValue` overloads don't clobber `aResult` on error. *aResult = ParsePrefFloat(result, &rv); } return rv; } nsresult GetValue(PrefValueKind aKind, nsACString& aResult) const { PrefValueKind kind; MOZ_TRY_VAR(kind, WantValueKind(PrefType::String, aKind)); aResult = GetStringValue(kind); return NS_OK; } nsresult GetValue(PrefValueKind aKind, nsACString* aResult) const { return GetValue(aKind, *aResult); } // Returns false if this pref doesn't have a user value worth saving. bool UserValueToStringForSaving(nsCString& aStr) { // Should we save the user value, if present? Only if it does not match the // default value, or it is sticky. if (HasUserValue() && (!ValueMatches(PrefValueKind::Default, Type(), GetValue()) || IsSticky())) { if (IsTypeString()) { StrEscape(GetStringValue().get(), aStr); } else if (IsTypeInt()) { aStr.AppendInt(GetIntValue()); } else if (IsTypeBool()) { aStr = GetBoolValue() ? "true" : "false"; } return true; } // Do not save default prefs that haven't changed. return false; } bool Matches(PrefType aType, PrefValueKind aKind, PrefValue& aValue, bool aIsSticky, bool aIsLocked) const { return (ValueMatches(aKind, aType, aValue) && aIsSticky == IsSticky() && aIsLocked == IsLocked()); } bool ValueMatches(PrefValueKind aKind, PrefType aType, const PrefValue& aValue) const { if (!IsType(aType)) { return false; } if (!(aKind == PrefValueKind::Default ? HasDefaultValue() : HasUserValue())) { return false; } switch (aType) { case PrefType::Bool: return GetBoolValue(aKind) == aValue.mBoolVal; case PrefType::Int: return GetIntValue(aKind) == aValue.mIntVal; case PrefType::String: return strcmp(GetBareStringValue(aKind), aValue.mStringVal) == 0; default: MOZ_ASSERT_UNREACHABLE("Unexpected preference type"); return false; } } }; void Pref::FromWrapper(PrefWrapper& aWrapper) { MOZ_ASSERT(aWrapper.is()); auto pref = aWrapper.as(); MOZ_ASSERT(IsTypeNone()); MOZ_ASSERT(mName == pref.NameString()); mType = uint32_t(pref.Type()); mIsLocked = pref.IsLocked(); mIsSanitized = pref.IsSanitized(); mIsSticky = pref.IsSticky(); mHasDefaultValue = pref.HasDefaultValue(); mHasUserValue = pref.HasUserValue(); if (mHasDefaultValue) { mDefaultValue.Init(Type(), aWrapper.GetValue(PrefValueKind::Default)); } if (mHasUserValue) { mUserValue.Init(Type(), aWrapper.GetValue(PrefValueKind::User)); } } class CallbackNode { public: CallbackNode(const nsACString& aDomain, PrefChangedFunc aFunc, void* aData, Preferences::MatchKind aMatchKind) : mDomain(AsVariant(nsCString(aDomain))), mFunc(aFunc), mData(aData), mNextAndMatchKind(aMatchKind) {} CallbackNode(const char** aDomains, PrefChangedFunc aFunc, void* aData, Preferences::MatchKind aMatchKind) : mDomain(AsVariant(aDomains)), mFunc(aFunc), mData(aData), mNextAndMatchKind(aMatchKind) {} // mDomain is a UniquePtr<>, so any uses of Domain() should only be temporary // borrows. const Variant& Domain() const { return mDomain; } PrefChangedFunc Func() const { return mFunc; } void ClearFunc() { mFunc = nullptr; } void* Data() const { return mData; } Preferences::MatchKind MatchKind() const { return static_cast(mNextAndMatchKind & kMatchKindMask); } bool DomainIs(const nsACString& aDomain) const { return mDomain.is() && mDomain.as() == aDomain; } bool DomainIs(const char** aPrefs) const { return mDomain == AsVariant(aPrefs); } bool Matches(const nsACString& aPrefName) const { auto match = [&](const nsACString& aStr) { return MatchKind() == Preferences::ExactMatch ? aPrefName == aStr : StringBeginsWith(aPrefName, aStr); }; if (mDomain.is()) { return match(mDomain.as()); } for (const char** ptr = mDomain.as(); *ptr; ptr++) { if (match(nsDependentCString(*ptr))) { return true; } } return false; } CallbackNode* Next() const { return reinterpret_cast(mNextAndMatchKind & kNextMask); } void SetNext(CallbackNode* aNext) { uintptr_t matchKind = mNextAndMatchKind & kMatchKindMask; mNextAndMatchKind = reinterpret_cast(aNext); MOZ_ASSERT((mNextAndMatchKind & kMatchKindMask) == 0); mNextAndMatchKind |= matchKind; } void AddSizeOfIncludingThis(MallocSizeOf aMallocSizeOf, PrefsSizes& aSizes) { aSizes.mCallbacksObjects += aMallocSizeOf(this); if (mDomain.is()) { aSizes.mCallbacksDomains += mDomain.as().SizeOfExcludingThisIfUnshared(aMallocSizeOf); } } private: static const uintptr_t kMatchKindMask = uintptr_t(0x1); static const uintptr_t kNextMask = ~kMatchKindMask; Variant mDomain; // If someone attempts to remove the node from the callback list while // NotifyCallbacks() is running, |func| is set to nullptr. Such nodes will // be removed at the end of NotifyCallbacks(). PrefChangedFunc mFunc; void* mData; // Conceptually this is two fields: // - CallbackNode* mNext; // - Preferences::MatchKind mMatchKind; // They are combined into a tagged pointer to save memory. uintptr_t mNextAndMatchKind; }; using PrefsHashTable = HashSet, PrefHasher>; // The main prefs hash table. Inside a function so we can assert it's only // accessed on the main thread. (That assertion can be avoided but only do so // with great care!) static inline PrefsHashTable*& HashTable(bool aOffMainThread = false) { MOZ_ASSERT(NS_IsMainThread() || ServoStyleSet::IsInServoTraversal()); static PrefsHashTable* sHashTable = nullptr; return sHashTable; } #ifdef DEBUG // This defines the type used to store our `once` mirrors checker. We can't use // HashMap for now due to alignment restrictions when dealing with // std::function (see bug 1557617). typedef std::function AntiFootgunCallback; struct CompareStr { bool operator()(char const* a, char const* b) const { return std::strcmp(a, b) < 0; } }; typedef std::map AntiFootgunMap; static AntiFootgunMap* gOnceStaticPrefsAntiFootgun; #endif // The callback list contains all the priority callbacks followed by the // non-priority callbacks. gLastPriorityNode records where the first part ends. static CallbackNode* gFirstCallback = nullptr; static CallbackNode* gLastPriorityNode = nullptr; #ifdef DEBUG # define ACCESS_COUNTS #endif #ifdef ACCESS_COUNTS using AccessCountsHashTable = nsTHashMap; static AccessCountsHashTable* gAccessCounts = nullptr; static void AddAccessCount(const nsACString& aPrefName) { // FIXME: Servo reads preferences from background threads in unsafe ways (bug // 1474789), and triggers assertions here if we try to add usage count entries // from background threads. if (NS_IsMainThread()) { JS::AutoSuppressGCAnalysis nogc; // Hash functions will not GC. uint32_t& count = gAccessCounts->LookupOrInsert(aPrefName); count++; } } static void AddAccessCount(const char* aPrefName) { AddAccessCount(nsDependentCString(aPrefName)); } #else static void MOZ_MAYBE_UNUSED AddAccessCount(const nsACString& aPrefName) {} static void AddAccessCount(const char* aPrefName) {} #endif // These are only used during the call to NotifyCallbacks(). static bool gCallbacksInProgress = false; static bool gShouldCleanupDeadNodes = false; class PrefsHashIter { using Iterator = decltype(HashTable()->modIter()); using ElemType = Pref*; Iterator mIter; public: explicit PrefsHashIter(PrefsHashTable* aTable) : mIter(aTable->modIter()) {} class Elem { friend class PrefsHashIter; PrefsHashIter& mParent; bool mDone; Elem(PrefsHashIter& aIter, bool aDone) : mParent(aIter), mDone(aDone) {} Iterator& Iter() { return mParent.mIter; } public: Elem& operator*() { return *this; } ElemType get() { if (mDone) { return nullptr; } return Iter().get().get(); } ElemType get() const { return const_cast(this)->get(); } ElemType operator->() { return get(); } ElemType operator->() const { return get(); } operator ElemType() { return get(); } void Remove() { Iter().remove(); } Elem& operator++() { MOZ_ASSERT(!mDone); Iter().next(); mDone = Iter().done(); return *this; } bool operator!=(Elem& other) { return mDone != other.mDone || this->get() != other.get(); } }; Elem begin() { return Elem(*this, mIter.done()); } Elem end() { return Elem(*this, true); } }; class PrefsIter { using Iterator = decltype(HashTable()->iter()); using ElemType = PrefWrapper; using HashElem = PrefsHashIter::Elem; using SharedElem = SharedPrefMap::Pref; using ElemTypeVariant = Variant; SharedPrefMap* mSharedMap; PrefsHashTable* mHashTable; PrefsHashIter mIter; ElemTypeVariant mPos; ElemTypeVariant mEnd; Maybe mEntry; public: PrefsIter(PrefsHashTable* aHashTable, SharedPrefMap* aSharedMap) : mSharedMap(aSharedMap), mHashTable(aHashTable), mIter(aHashTable), mPos(AsVariant(mIter.begin())), mEnd(AsVariant(mIter.end())) { if (Done()) { NextIterator(); } } private: #define MATCH(type, ...) \ do { \ struct Matcher { \ PrefsIter& mIter; \ type operator()(HashElem& pos) { \ HashElem& end MOZ_MAYBE_UNUSED = mIter.mEnd.as(); \ __VA_ARGS__; \ } \ type operator()(SharedElem& pos) { \ SharedElem& end MOZ_MAYBE_UNUSED = mIter.mEnd.as(); \ __VA_ARGS__; \ } \ }; \ return mPos.match(Matcher{*this}); \ } while (0); bool Done() { MATCH(bool, return pos == end); } PrefWrapper MakeEntry() { MATCH(PrefWrapper, return PrefWrapper(pos)); } void NextEntry() { mEntry.reset(); MATCH(void, ++pos); } #undef MATCH bool Next() { NextEntry(); return !Done() || NextIterator(); } bool NextIterator() { if (mPos.is() && mSharedMap) { mPos = AsVariant(mSharedMap->begin()); mEnd = AsVariant(mSharedMap->end()); return !Done(); } return false; } bool IteratingBase() { return mPos.is(); } PrefWrapper& Entry() { MOZ_ASSERT(!Done()); if (!mEntry.isSome()) { mEntry.emplace(MakeEntry()); } return mEntry.ref(); } public: class Elem { friend class PrefsIter; PrefsIter& mParent; bool mDone; Elem(PrefsIter& aIter, bool aDone) : mParent(aIter), mDone(aDone) { SkipDuplicates(); } void Next() { mDone = !mParent.Next(); } void SkipDuplicates() { while (!mDone && (mParent.IteratingBase() ? mParent.mHashTable->has(ref().Name()) : ref().IsTypeNone())) { Next(); } } public: Elem& operator*() { return *this; } ElemType& ref() { return mParent.Entry(); } const ElemType& ref() const { return const_cast(this)->ref(); } ElemType* operator->() { return &ref(); } const ElemType* operator->() const { return &ref(); } operator ElemType() { return ref(); } Elem& operator++() { MOZ_ASSERT(!mDone); Next(); SkipDuplicates(); return *this; } bool operator!=(Elem& other) { if (mDone != other.mDone) { return true; } if (mDone) { return false; } return &this->ref() != &other.ref(); } }; Elem begin() { return {*this, Done()}; } Elem end() { return {*this, true}; } }; static Pref* pref_HashTableLookup(const char* aPrefName); static void NotifyCallbacks(const nsCString& aPrefName, const PrefWrapper* aPref = nullptr); static void NotifyCallbacks(const nsCString& aPrefName, const PrefWrapper& aPref) { NotifyCallbacks(aPrefName, &aPref); } // The approximate number of preferences in the dynamic hashtable for the parent // and content processes, respectively. These numbers are used to determine the // initial size of the dynamic preference hashtables, and should be chosen to // avoid rehashing during normal usage. The actual number of preferences will, // or course, change over time, but these numbers only need to be within a // binary order of magnitude of the actual values to remain effective. // // The number for the parent process should reflect the total number of // preferences in the database, since the parent process needs to initially // build a dynamic hashtable of the entire preference database. The number for // the child process should reflect the number of preferences which are likely // to change after the startup of the first content process, since content // processes only store changed preferences on top of a snapshot of the database // created at startup. // // Note: The capacity of a hashtable doubles when its length reaches an exact // power of two. A table with an initial length of 64 is twice as large as one // with an initial length of 63. This is important in content processes, where // lookup speed is less critical and we pay the price of the additional overhead // for each content process. So the initial content length should generally be // *under* the next power-of-two larger than its expected length. constexpr size_t kHashTableInitialLengthParent = 3000; constexpr size_t kHashTableInitialLengthContent = 64; static PrefSaveData pref_savePrefs() { MOZ_ASSERT(NS_IsMainThread()); PrefSaveData savedPrefs(HashTable()->count()); for (auto& pref : PrefsIter(HashTable(), gSharedMap)) { nsAutoCString prefValueStr; if (!pref->UserValueToStringForSaving(prefValueStr)) { continue; } nsAutoCString prefNameStr; StrEscape(pref->Name(), prefNameStr); nsPrintfCString str("user_pref(%s, %s);", prefNameStr.get(), prefValueStr.get()); savedPrefs.AppendElement(str); } return savedPrefs; } static Pref* pref_HashTableLookup(const char* aPrefName) { MOZ_ASSERT(NS_IsMainThread() || ServoStyleSet::IsInServoTraversal()); MOZ_ASSERT_IF(!XRE_IsParentProcess(), gContentProcessPrefsAreInited); // We use readonlyThreadsafeLookup() because we often have concurrent lookups // from multiple Stylo threads. This is safe because those threads cannot // modify sHashTable, and the main thread is blocked while Stylo threads are // doing these lookups. auto p = HashTable()->readonlyThreadsafeLookup(aPrefName); return p ? p->get() : nullptr; } // While notifying preference callbacks, this holds the wrapper for the // preference being notified, in order to optimize lookups. // // Note: Callbacks and lookups only happen on the main thread, so this is safe // to use without locking. static const PrefWrapper* gCallbackPref; Maybe pref_SharedLookup(const char* aPrefName) { MOZ_DIAGNOSTIC_ASSERT(gSharedMap, "gSharedMap must be initialized"); if (Maybe pref = gSharedMap->Get(aPrefName)) { return Some(*pref); } return Nothing(); } Maybe pref_Lookup(const char* aPrefName, bool aIncludeTypeNone = false) { MOZ_ASSERT(NS_IsMainThread() || ServoStyleSet::IsInServoTraversal()); AddAccessCount(aPrefName); if (gCallbackPref && strcmp(aPrefName, gCallbackPref->Name()) == 0) { return Some(*gCallbackPref); } if (Pref* pref = pref_HashTableLookup(aPrefName)) { if (aIncludeTypeNone || !pref->IsTypeNone() || pref->IsSanitized()) { return Some(pref); } } else if (gSharedMap) { return pref_SharedLookup(aPrefName); } return Nothing(); } static Result pref_LookupForModify( const nsCString& aPrefName, const std::function& aCheckFn) { Maybe wrapper = pref_Lookup(aPrefName.get(), /* includeTypeNone */ true); if (wrapper.isNothing()) { return Err(NS_ERROR_INVALID_ARG); } if (!aCheckFn(*wrapper)) { return nullptr; } if (wrapper->is()) { return wrapper->as(); } Pref* pref = new Pref(aPrefName); if (!HashTable()->putNew(aPrefName.get(), pref)) { delete pref; return Err(NS_ERROR_OUT_OF_MEMORY); } pref->FromWrapper(*wrapper); return pref; } static nsresult pref_SetPref(const nsCString& aPrefName, PrefType aType, PrefValueKind aKind, PrefValue aValue, bool aIsSticky, bool aIsLocked, bool aFromInit) { MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(NS_IsMainThread()); if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) { printf( "pref_SetPref: Attempt to write pref %s after XPCOMShutdownThreads " "started.\n", aPrefName.get()); if (nsContentUtils::IsInitialized()) { xpc_DumpJSStack(true, true, false); } MOZ_ASSERT(false, "Late preference writes should be avoided."); return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; } if (!HashTable()) { return NS_ERROR_OUT_OF_MEMORY; } Pref* pref = nullptr; if (gSharedMap) { auto result = pref_LookupForModify(aPrefName, [&](const PrefWrapper& aWrapper) { return !aWrapper.Matches(aType, aKind, aValue, aIsSticky, aIsLocked); }); if (result.isOk() && !(pref = result.unwrap())) { // No changes required. return NS_OK; } } if (!pref) { auto p = HashTable()->lookupForAdd(aPrefName.get()); if (!p) { pref = new Pref(aPrefName); pref->SetType(aType); if (!HashTable()->add(p, pref)) { delete pref; return NS_ERROR_OUT_OF_MEMORY; } } else { pref = p->get(); } } bool valueChanged = false; nsresult rv; if (aKind == PrefValueKind::Default) { rv = pref->SetDefaultValue(aType, aValue, aIsSticky, aIsLocked, &valueChanged); } else { MOZ_ASSERT(!aIsLocked); // `locked` is disallowed in user pref files rv = pref->SetUserValue(aType, aValue, aFromInit, &valueChanged); } if (NS_FAILED(rv)) { NS_WARNING( nsPrintfCString("Rejected attempt to change type of pref %s's %s value " "from %s to %s", aPrefName.get(), (aKind == PrefValueKind::Default) ? "default" : "user", PrefTypeToString(pref->Type()), PrefTypeToString(aType)) .get()); return rv; } if (valueChanged) { if (aKind == PrefValueKind::User) { Preferences::HandleDirty(); } NotifyCallbacks(aPrefName, PrefWrapper(pref)); } return NS_OK; } // Removes |node| from callback list. Returns the node after the deleted one. static CallbackNode* pref_RemoveCallbackNode(CallbackNode* aNode, CallbackNode* aPrevNode) { MOZ_ASSERT(!aPrevNode || aPrevNode->Next() == aNode); MOZ_ASSERT(aPrevNode || gFirstCallback == aNode); MOZ_ASSERT(!gCallbacksInProgress); CallbackNode* next_node = aNode->Next(); if (aPrevNode) { aPrevNode->SetNext(next_node); } else { gFirstCallback = next_node; } if (gLastPriorityNode == aNode) { gLastPriorityNode = aPrevNode; } delete aNode; return next_node; } static void NotifyCallbacks(const nsCString& aPrefName, const PrefWrapper* aPref) { bool reentered = gCallbacksInProgress; gCallbackPref = aPref; auto cleanup = MakeScopeExit([]() { gCallbackPref = nullptr; }); // Nodes must not be deleted while gCallbacksInProgress is true. // Nodes that need to be deleted are marked for deletion by nulling // out the |func| pointer. We release them at the end of this function // if we haven't reentered. gCallbacksInProgress = true; for (CallbackNode* node = gFirstCallback; node; node = node->Next()) { if (node->Func()) { if (node->Matches(aPrefName)) { (node->Func())(aPrefName.get(), node->Data()); } } } gCallbacksInProgress = reentered; if (gShouldCleanupDeadNodes && !gCallbacksInProgress) { CallbackNode* prev_node = nullptr; CallbackNode* node = gFirstCallback; while (node) { if (!node->Func()) { node = pref_RemoveCallbackNode(node, prev_node); } else { prev_node = node; node = node->Next(); } } gShouldCleanupDeadNodes = false; } #ifdef DEBUG if (XRE_IsParentProcess() && !StaticPrefs::preferences_force_disable_check_once_policy() && (StaticPrefs::preferences_check_once_policy() || xpc::IsInAutomation())) { // Check that we aren't modifying a `once`-mirrored pref using that pref // name. We have about 100 `once`-mirrored prefs. std::map performs a // search in O(log n), so this is fast enough. MOZ_ASSERT(gOnceStaticPrefsAntiFootgun); auto search = gOnceStaticPrefsAntiFootgun->find(aPrefName.get()); if (search != gOnceStaticPrefsAntiFootgun->end()) { // Run the callback. (search->second)(); } } #endif } //=========================================================================== // Prefs parsing //=========================================================================== extern "C" { // Keep this in sync with PrefFn in parser/src/lib.rs. typedef void (*PrefsParserPrefFn)(const char* aPrefName, PrefType aType, PrefValueKind aKind, PrefValue aValue, bool aIsSticky, bool aIsLocked); // Keep this in sync with ErrorFn in parser/src/lib.rs. // // `aMsg` is just a borrow of the string, and must be copied if it is used // outside the lifetime of the prefs_parser_parse() call. typedef void (*PrefsParserErrorFn)(const char* aMsg); // Keep this in sync with prefs_parser_parse() in parser/src/lib.rs. bool prefs_parser_parse(const char* aPath, PrefValueKind aKind, const char* aBuf, size_t aLen, PrefsParserPrefFn aPrefFn, PrefsParserErrorFn aErrorFn); } class Parser { public: Parser() = default; ~Parser() = default; bool Parse(PrefValueKind aKind, const char* aPath, const nsCString& aBuf) { MOZ_ASSERT(XRE_IsParentProcess()); return prefs_parser_parse(aPath, aKind, aBuf.get(), aBuf.Length(), HandlePref, HandleError); } private: static void HandlePref(const char* aPrefName, PrefType aType, PrefValueKind aKind, PrefValue aValue, bool aIsSticky, bool aIsLocked) { MOZ_ASSERT(XRE_IsParentProcess()); pref_SetPref(nsDependentCString(aPrefName), aType, aKind, aValue, aIsSticky, aIsLocked, /* fromInit */ true); } static void HandleError(const char* aMsg) { nsresult rv; nsCOMPtr console = do_GetService("@mozilla.org/consoleservice;1", &rv); if (NS_SUCCEEDED(rv)) { console->LogStringMessage(NS_ConvertUTF8toUTF16(aMsg).get()); } #ifdef DEBUG NS_ERROR(aMsg); #else printf_stderr("%s\n", aMsg); #endif } }; // The following code is test code for the gtest. static void TestParseErrorHandlePref(const char* aPrefName, PrefType aType, PrefValueKind aKind, PrefValue aValue, bool aIsSticky, bool aIsLocked) {} static nsCString gTestParseErrorMsgs; static void TestParseErrorHandleError(const char* aMsg) { gTestParseErrorMsgs.Append(aMsg); gTestParseErrorMsgs.Append('\n'); } // Keep this in sync with the declaration in test/gtest/Parser.cpp. void TestParseError(PrefValueKind aKind, const char* aText, nsCString& aErrorMsg) { prefs_parser_parse("test", aKind, aText, strlen(aText), TestParseErrorHandlePref, TestParseErrorHandleError); // Copy the error messages into the outparam, then clear them from // gTestParseErrorMsgs. aErrorMsg.Assign(gTestParseErrorMsgs); gTestParseErrorMsgs.Truncate(); } //=========================================================================== // nsPrefBranch et al. //=========================================================================== namespace mozilla { class PreferenceServiceReporter; } // namespace mozilla class PrefCallback : public PLDHashEntryHdr { friend class mozilla::PreferenceServiceReporter; public: typedef PrefCallback* KeyType; typedef const PrefCallback* KeyTypePointer; static const PrefCallback* KeyToPointer(PrefCallback* aKey) { return aKey; } static PLDHashNumber HashKey(const PrefCallback* aKey) { uint32_t hash = HashString(aKey->mDomain); return AddToHash(hash, aKey->mCanonical); } public: // Create a PrefCallback with a strong reference to its observer. PrefCallback(const nsACString& aDomain, nsIObserver* aObserver, nsPrefBranch* aBranch) : mDomain(aDomain), mBranch(aBranch), mWeakRef(nullptr), mStrongRef(aObserver) { MOZ_COUNT_CTOR(PrefCallback); nsCOMPtr canonical = do_QueryInterface(aObserver); mCanonical = canonical; } // Create a PrefCallback with a weak reference to its observer. PrefCallback(const nsACString& aDomain, nsISupportsWeakReference* aObserver, nsPrefBranch* aBranch) : mDomain(aDomain), mBranch(aBranch), mWeakRef(do_GetWeakReference(aObserver)), mStrongRef(nullptr) { MOZ_COUNT_CTOR(PrefCallback); nsCOMPtr canonical = do_QueryInterface(aObserver); mCanonical = canonical; } // This is explicitly not a copy constructor. explicit PrefCallback(const PrefCallback*& aCopy) : mDomain(aCopy->mDomain), mBranch(aCopy->mBranch), mWeakRef(aCopy->mWeakRef), mStrongRef(aCopy->mStrongRef), mCanonical(aCopy->mCanonical) { MOZ_COUNT_CTOR(PrefCallback); } PrefCallback(const PrefCallback&) = delete; PrefCallback(PrefCallback&&) = default; MOZ_COUNTED_DTOR(PrefCallback) bool KeyEquals(const PrefCallback* aKey) const { // We want to be able to look up a weakly-referencing PrefCallback after // its observer has died so we can remove it from the table. Once the // callback's observer dies, its canonical pointer is stale -- in // particular, we may have allocated a new observer in the same spot in // memory! So we can't just compare canonical pointers to determine whether // aKey refers to the same observer as this. // // Our workaround is based on the way we use this hashtable: When we ask // the hashtable to remove a PrefCallback whose weak reference has expired, // we use as the key for removal the same object as was inserted into the // hashtable. Thus we can say that if one of the keys' weak references has // expired, the two keys are equal iff they're the same object. if (IsExpired() || aKey->IsExpired()) { return this == aKey; } if (mCanonical != aKey->mCanonical) { return false; } return mDomain.Equals(aKey->mDomain); } PrefCallback* GetKey() const { return const_cast(this); } // Get a reference to the callback's observer, or null if the observer was // weakly referenced and has been destroyed. already_AddRefed GetObserver() const { if (!IsWeak()) { nsCOMPtr copy = mStrongRef; return copy.forget(); } nsCOMPtr observer = do_QueryReferent(mWeakRef); return observer.forget(); } const nsCString& GetDomain() const { return mDomain; } nsPrefBranch* GetPrefBranch() const { return mBranch; } // Has this callback's weak reference died? bool IsExpired() const { if (!IsWeak()) return false; nsCOMPtr observer(do_QueryReferent(mWeakRef)); return !observer; } size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { size_t n = aMallocSizeOf(this); n += mDomain.SizeOfExcludingThisIfUnshared(aMallocSizeOf); // All the other fields are non-owning pointers, so we don't measure them. return n; } enum { ALLOW_MEMMOVE = true }; private: nsCString mDomain; nsPrefBranch* mBranch; // Exactly one of mWeakRef and mStrongRef should be non-null. nsWeakPtr mWeakRef; nsCOMPtr mStrongRef; // We need a canonical nsISupports pointer, per bug 578392. nsISupports* mCanonical; bool IsWeak() const { return !!mWeakRef; } }; class nsPrefBranch final : public nsIPrefBranch, public nsIObserver, public nsSupportsWeakReference { friend class mozilla::PreferenceServiceReporter; public: NS_DECL_ISUPPORTS NS_DECL_NSIPREFBRANCH NS_DECL_NSIOBSERVER nsPrefBranch(const char* aPrefRoot, PrefValueKind aKind); nsPrefBranch() = delete; static void NotifyObserver(const char* aNewpref, void* aData); size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const; private: using PrefName = nsCString; virtual ~nsPrefBranch(); int32_t GetRootLength() const { return mPrefRoot.Length(); } nsresult GetDefaultFromPropertiesFile(const char* aPrefName, nsAString& aReturn); // As SetCharPref, but without any check on the length of |aValue|. nsresult SetCharPrefNoLengthCheck(const char* aPrefName, const nsACString& aValue); // Reject strings that are more than 1Mb, warn if strings are more than 16kb. nsresult CheckSanityOfStringLength(const char* aPrefName, const nsAString& aValue); nsresult CheckSanityOfStringLength(const char* aPrefName, const nsACString& aValue); nsresult CheckSanityOfStringLength(const char* aPrefName, const uint32_t aLength); void RemoveExpiredCallback(PrefCallback* aCallback); PrefName GetPrefName(const char* aPrefName) const { return GetPrefName(nsDependentCString(aPrefName)); } PrefName GetPrefName(const nsACString& aPrefName) const; void FreeObserverList(void); const nsCString mPrefRoot; PrefValueKind mKind; bool mFreeingObserverList; nsClassHashtable mObservers; }; class nsPrefLocalizedString final : public nsIPrefLocalizedString { public: nsPrefLocalizedString(); NS_DECL_ISUPPORTS NS_FORWARD_NSISUPPORTSPRIMITIVE(mUnicodeString->) NS_FORWARD_NSISUPPORTSSTRING(mUnicodeString->) nsresult Init(); private: virtual ~nsPrefLocalizedString(); nsCOMPtr mUnicodeString; }; //---------------------------------------------------------------------------- // nsPrefBranch //---------------------------------------------------------------------------- nsPrefBranch::nsPrefBranch(const char* aPrefRoot, PrefValueKind aKind) : mPrefRoot(aPrefRoot), mKind(aKind), mFreeingObserverList(false), mObservers() { nsCOMPtr observerService = services::GetObserverService(); if (observerService) { ++mRefCnt; // must be > 0 when we call this, or we'll get deleted! // Add weakly so we don't have to clean up at shutdown. observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true); --mRefCnt; } } nsPrefBranch::~nsPrefBranch() { FreeObserverList(); } NS_IMPL_ISUPPORTS(nsPrefBranch, nsIPrefBranch, nsIObserver, nsISupportsWeakReference) NS_IMETHODIMP nsPrefBranch::GetRoot(nsACString& aRoot) { aRoot = mPrefRoot; return NS_OK; } NS_IMETHODIMP nsPrefBranch::GetPrefType(const char* aPrefName, int32_t* aRetVal) { NS_ENSURE_ARG(aPrefName); const PrefName& prefName = GetPrefName(aPrefName); *aRetVal = Preferences::GetType(prefName.get()); return NS_OK; } NS_IMETHODIMP nsPrefBranch::GetBoolPrefWithDefault(const char* aPrefName, bool aDefaultValue, uint8_t aArgc, bool* aRetVal) { nsresult rv = GetBoolPref(aPrefName, aRetVal); if (NS_FAILED(rv) && aArgc == 1) { *aRetVal = aDefaultValue; return NS_OK; } return rv; } NS_IMETHODIMP nsPrefBranch::GetBoolPref(const char* aPrefName, bool* aRetVal) { NS_ENSURE_ARG(aPrefName); const PrefName& pref = GetPrefName(aPrefName); return Preferences::GetBool(pref.get(), aRetVal, mKind); } NS_IMETHODIMP nsPrefBranch::SetBoolPref(const char* aPrefName, bool aValue) { NS_ENSURE_ARG(aPrefName); const PrefName& pref = GetPrefName(aPrefName); return Preferences::SetBool(pref.get(), aValue, mKind); } NS_IMETHODIMP nsPrefBranch::GetFloatPrefWithDefault(const char* aPrefName, float aDefaultValue, uint8_t aArgc, float* aRetVal) { nsresult rv = GetFloatPref(aPrefName, aRetVal); if (NS_FAILED(rv) && aArgc == 1) { *aRetVal = aDefaultValue; return NS_OK; } return rv; } NS_IMETHODIMP nsPrefBranch::GetFloatPref(const char* aPrefName, float* aRetVal) { NS_ENSURE_ARG(aPrefName); nsAutoCString stringVal; nsresult rv = GetCharPref(aPrefName, stringVal); if (NS_SUCCEEDED(rv)) { // ParsePrefFloat() does a locale-independent conversion. *aRetVal = ParsePrefFloat(stringVal, &rv); } return rv; } NS_IMETHODIMP nsPrefBranch::GetCharPrefWithDefault(const char* aPrefName, const nsACString& aDefaultValue, uint8_t aArgc, nsACString& aRetVal) { nsresult rv = GetCharPref(aPrefName, aRetVal); if (NS_FAILED(rv) && aArgc == 1) { aRetVal = aDefaultValue; return NS_OK; } return rv; } NS_IMETHODIMP nsPrefBranch::GetCharPref(const char* aPrefName, nsACString& aRetVal) { NS_ENSURE_ARG(aPrefName); const PrefName& pref = GetPrefName(aPrefName); return Preferences::GetCString(pref.get(), aRetVal, mKind); } NS_IMETHODIMP nsPrefBranch::SetCharPref(const char* aPrefName, const nsACString& aValue) { nsresult rv = CheckSanityOfStringLength(aPrefName, aValue); if (NS_FAILED(rv)) { return rv; } return SetCharPrefNoLengthCheck(aPrefName, aValue); } nsresult nsPrefBranch::SetCharPrefNoLengthCheck(const char* aPrefName, const nsACString& aValue) { NS_ENSURE_ARG(aPrefName); const PrefName& pref = GetPrefName(aPrefName); return Preferences::SetCString(pref.get(), aValue, mKind); } NS_IMETHODIMP nsPrefBranch::GetStringPref(const char* aPrefName, const nsACString& aDefaultValue, uint8_t aArgc, nsACString& aRetVal) { nsCString utf8String; nsresult rv = GetCharPref(aPrefName, utf8String); if (NS_SUCCEEDED(rv)) { aRetVal = utf8String; return rv; } if (aArgc == 1) { aRetVal = aDefaultValue; return NS_OK; } return rv; } NS_IMETHODIMP nsPrefBranch::SetStringPref(const char* aPrefName, const nsACString& aValue) { nsresult rv = CheckSanityOfStringLength(aPrefName, aValue); if (NS_FAILED(rv)) { return rv; } return SetCharPrefNoLengthCheck(aPrefName, aValue); } NS_IMETHODIMP nsPrefBranch::GetIntPrefWithDefault(const char* aPrefName, int32_t aDefaultValue, uint8_t aArgc, int32_t* aRetVal) { nsresult rv = GetIntPref(aPrefName, aRetVal); if (NS_FAILED(rv) && aArgc == 1) { *aRetVal = aDefaultValue; return NS_OK; } return rv; } NS_IMETHODIMP nsPrefBranch::GetIntPref(const char* aPrefName, int32_t* aRetVal) { NS_ENSURE_ARG(aPrefName); const PrefName& pref = GetPrefName(aPrefName); return Preferences::GetInt(pref.get(), aRetVal, mKind); } NS_IMETHODIMP nsPrefBranch::SetIntPref(const char* aPrefName, int32_t aValue) { NS_ENSURE_ARG(aPrefName); const PrefName& pref = GetPrefName(aPrefName); return Preferences::SetInt(pref.get(), aValue, mKind); } NS_IMETHODIMP nsPrefBranch::GetComplexValue(const char* aPrefName, const nsIID& aType, void** aRetVal) { NS_ENSURE_ARG(aPrefName); nsresult rv; nsAutoCString utf8String; // We have to do this one first because it's different to all the rest. if (aType.Equals(NS_GET_IID(nsIPrefLocalizedString))) { nsCOMPtr theString( do_CreateInstance(NS_PREFLOCALIZEDSTRING_CONTRACTID, &rv)); if (NS_FAILED(rv)) { return rv; } const PrefName& pref = GetPrefName(aPrefName); bool bNeedDefault = false; if (mKind == PrefValueKind::Default) { bNeedDefault = true; } else { // if there is no user (or locked) value if (!Preferences::HasUserValue(pref.get()) && !Preferences::IsLocked(pref.get())) { bNeedDefault = true; } } // if we need to fetch the default value, do that instead, otherwise use the // value we pulled in at the top of this function if (bNeedDefault) { nsAutoString utf16String; rv = GetDefaultFromPropertiesFile(pref.get(), utf16String); if (NS_SUCCEEDED(rv)) { theString->SetData(utf16String); } } else { rv = GetCharPref(aPrefName, utf8String); if (NS_SUCCEEDED(rv)) { theString->SetData(NS_ConvertUTF8toUTF16(utf8String)); } } if (NS_SUCCEEDED(rv)) { theString.forget(reinterpret_cast(aRetVal)); } return rv; } // if we can't get the pref, there's no point in being here rv = GetCharPref(aPrefName, utf8String); if (NS_FAILED(rv)) { return rv; } if (aType.Equals(NS_GET_IID(nsIFile))) { ENSURE_PARENT_PROCESS("GetComplexValue(nsIFile)", aPrefName); nsCOMPtr file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); if (NS_SUCCEEDED(rv)) { rv = file->SetPersistentDescriptor(utf8String); if (NS_SUCCEEDED(rv)) { file.forget(reinterpret_cast(aRetVal)); return NS_OK; } } return rv; } if (aType.Equals(NS_GET_IID(nsIRelativeFilePref))) { ENSURE_PARENT_PROCESS("GetComplexValue(nsIRelativeFilePref)", aPrefName); nsACString::const_iterator keyBegin, strEnd; utf8String.BeginReading(keyBegin); utf8String.EndReading(strEnd); // The pref has the format: [fromKey]a/b/c if (*keyBegin++ != '[') { return NS_ERROR_FAILURE; } nsACString::const_iterator keyEnd(keyBegin); if (!FindCharInReadable(']', keyEnd, strEnd)) { return NS_ERROR_FAILURE; } nsAutoCString key(Substring(keyBegin, keyEnd)); nsCOMPtr fromFile; nsCOMPtr directoryService( do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv)); if (NS_FAILED(rv)) { return rv; } rv = directoryService->Get(key.get(), NS_GET_IID(nsIFile), getter_AddRefs(fromFile)); if (NS_FAILED(rv)) { return rv; } nsCOMPtr theFile; rv = NS_NewNativeLocalFile(""_ns, true, getter_AddRefs(theFile)); if (NS_FAILED(rv)) { return rv; } rv = theFile->SetRelativeDescriptor(fromFile, Substring(++keyEnd, strEnd)); if (NS_FAILED(rv)) { return rv; } nsCOMPtr relativePref = new nsRelativeFilePref(); Unused << relativePref->SetFile(theFile); Unused << relativePref->SetRelativeToKey(key); relativePref.forget(reinterpret_cast(aRetVal)); return NS_OK; } NS_WARNING("nsPrefBranch::GetComplexValue - Unsupported interface type"); return NS_NOINTERFACE; } nsresult nsPrefBranch::CheckSanityOfStringLength(const char* aPrefName, const nsAString& aValue) { return CheckSanityOfStringLength(aPrefName, aValue.Length()); } nsresult nsPrefBranch::CheckSanityOfStringLength(const char* aPrefName, const nsACString& aValue) { return CheckSanityOfStringLength(aPrefName, aValue.Length()); } nsresult nsPrefBranch::CheckSanityOfStringLength(const char* aPrefName, const uint32_t aLength) { if (aLength > MAX_PREF_LENGTH) { return NS_ERROR_ILLEGAL_VALUE; } if (aLength <= MAX_ADVISABLE_PREF_LENGTH) { return NS_OK; } nsresult rv; nsCOMPtr console = do_GetService("@mozilla.org/consoleservice;1", &rv); if (NS_FAILED(rv)) { return rv; } nsAutoCString message(nsPrintfCString( "Warning: attempting to write %d bytes to preference %s. This is bad " "for general performance and memory usage. Such an amount of data " "should rather be written to an external file.", aLength, GetPrefName(aPrefName).get())); rv = console->LogStringMessage(NS_ConvertUTF8toUTF16(message).get()); if (NS_FAILED(rv)) { return rv; } return NS_OK; } NS_IMETHODIMP nsPrefBranch::SetComplexValue(const char* aPrefName, const nsIID& aType, nsISupports* aValue) { ENSURE_PARENT_PROCESS("SetComplexValue", aPrefName); NS_ENSURE_ARG(aPrefName); nsresult rv = NS_NOINTERFACE; if (aType.Equals(NS_GET_IID(nsIFile))) { nsCOMPtr file = do_QueryInterface(aValue); if (!file) { return NS_NOINTERFACE; } nsAutoCString descriptorString; rv = file->GetPersistentDescriptor(descriptorString); if (NS_SUCCEEDED(rv)) { rv = SetCharPrefNoLengthCheck(aPrefName, descriptorString); } return rv; } if (aType.Equals(NS_GET_IID(nsIRelativeFilePref))) { nsCOMPtr relFilePref = do_QueryInterface(aValue); if (!relFilePref) { return NS_NOINTERFACE; } nsCOMPtr file; relFilePref->GetFile(getter_AddRefs(file)); if (!file) { return NS_NOINTERFACE; } nsAutoCString relativeToKey; (void)relFilePref->GetRelativeToKey(relativeToKey); nsCOMPtr relativeToFile; nsCOMPtr directoryService( do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv)); if (NS_FAILED(rv)) { return rv; } rv = directoryService->Get(relativeToKey.get(), NS_GET_IID(nsIFile), getter_AddRefs(relativeToFile)); if (NS_FAILED(rv)) { return rv; } nsAutoCString relDescriptor; rv = file->GetRelativeDescriptor(relativeToFile, relDescriptor); if (NS_FAILED(rv)) { return rv; } nsAutoCString descriptorString; descriptorString.Append('['); descriptorString.Append(relativeToKey); descriptorString.Append(']'); descriptorString.Append(relDescriptor); return SetCharPrefNoLengthCheck(aPrefName, descriptorString); } if (aType.Equals(NS_GET_IID(nsIPrefLocalizedString))) { nsCOMPtr theString = do_QueryInterface(aValue); if (theString) { nsString wideString; rv = theString->GetData(wideString); if (NS_SUCCEEDED(rv)) { // Check sanity of string length before any lengthy conversion rv = CheckSanityOfStringLength(aPrefName, wideString); if (NS_FAILED(rv)) { return rv; } rv = SetCharPrefNoLengthCheck(aPrefName, NS_ConvertUTF16toUTF8(wideString)); } } return rv; } NS_WARNING("nsPrefBranch::SetComplexValue - Unsupported interface type"); return NS_NOINTERFACE; } NS_IMETHODIMP nsPrefBranch::ClearUserPref(const char* aPrefName) { NS_ENSURE_ARG(aPrefName); const PrefName& pref = GetPrefName(aPrefName); return Preferences::ClearUser(pref.get()); } NS_IMETHODIMP nsPrefBranch::PrefHasUserValue(const char* aPrefName, bool* aRetVal) { NS_ENSURE_ARG_POINTER(aRetVal); NS_ENSURE_ARG(aPrefName); const PrefName& pref = GetPrefName(aPrefName); *aRetVal = Preferences::HasUserValue(pref.get()); return NS_OK; } NS_IMETHODIMP nsPrefBranch::PrefHasDefaultValue(const char* aPrefName, bool* aRetVal) { NS_ENSURE_ARG_POINTER(aRetVal); NS_ENSURE_ARG(aPrefName); const PrefName& pref = GetPrefName(aPrefName); *aRetVal = Preferences::HasDefaultValue(pref.get()); return NS_OK; } NS_IMETHODIMP nsPrefBranch::LockPref(const char* aPrefName) { NS_ENSURE_ARG(aPrefName); const PrefName& pref = GetPrefName(aPrefName); return Preferences::Lock(pref.get()); } NS_IMETHODIMP nsPrefBranch::PrefIsLocked(const char* aPrefName, bool* aRetVal) { NS_ENSURE_ARG_POINTER(aRetVal); NS_ENSURE_ARG(aPrefName); const PrefName& pref = GetPrefName(aPrefName); *aRetVal = Preferences::IsLocked(pref.get()); return NS_OK; } NS_IMETHODIMP nsPrefBranch::PrefIsSanitized(const char* aPrefName, bool* aRetVal) { NS_ENSURE_ARG_POINTER(aRetVal); NS_ENSURE_ARG(aPrefName); const PrefName& pref = GetPrefName(aPrefName); *aRetVal = Preferences::IsSanitized(pref.get()); return NS_OK; } NS_IMETHODIMP nsPrefBranch::UnlockPref(const char* aPrefName) { NS_ENSURE_ARG(aPrefName); const PrefName& pref = GetPrefName(aPrefName); return Preferences::Unlock(pref.get()); } NS_IMETHODIMP nsPrefBranch::ResetBranch(const char* aStartingAt) { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP nsPrefBranch::DeleteBranch(const char* aStartingAt) { ENSURE_PARENT_PROCESS("DeleteBranch", aStartingAt); NS_ENSURE_ARG(aStartingAt); MOZ_ASSERT(NS_IsMainThread()); if (!HashTable()) { return NS_ERROR_NOT_INITIALIZED; } const PrefName& pref = GetPrefName(aStartingAt); nsAutoCString branchName(pref.get()); // Add a trailing '.' if it doesn't already have one. if (branchName.Length() > 1 && !StringEndsWith(branchName, "."_ns)) { branchName += '.'; } const nsACString& branchNameNoDot = Substring(branchName, 0, branchName.Length() - 1); for (auto iter = HashTable()->modIter(); !iter.done(); iter.next()) { // The first disjunct matches branches: e.g. a branch name "foo.bar." // matches a name "foo.bar.baz" (but it won't match "foo.barrel.baz"). // The second disjunct matches leaf nodes: e.g. a branch name "foo.bar." // matches a name "foo.bar" (by ignoring the trailing '.'). nsDependentCString name(iter.get()->Name()); if (StringBeginsWith(name, branchName) || name.Equals(branchNameNoDot)) { iter.remove(); // The saved callback pref may be invalid now. gCallbackPref = nullptr; } } Preferences::HandleDirty(); return NS_OK; } NS_IMETHODIMP nsPrefBranch::GetChildList(const char* aStartingAt, nsTArray& aChildArray) { NS_ENSURE_ARG(aStartingAt); MOZ_ASSERT(NS_IsMainThread()); // This will contain a list of all the pref name strings. Allocated on the // stack for speed. AutoTArray prefArray; const PrefName& parent = GetPrefName(aStartingAt); size_t parentLen = parent.Length(); for (auto& pref : PrefsIter(HashTable(), gSharedMap)) { if (strncmp(pref->Name(), parent.get(), parentLen) == 0) { prefArray.AppendElement(pref->NameString()); } } // Now that we've built up the list, run the callback on all the matching // elements. aChildArray.SetCapacity(prefArray.Length()); for (auto& element : prefArray) { // we need to lop off mPrefRoot in case the user is planning to pass this // back to us because if they do we are going to add mPrefRoot again. aChildArray.AppendElement(Substring(element, mPrefRoot.Length())); } return NS_OK; } NS_IMETHODIMP nsPrefBranch::AddObserverImpl(const nsACString& aDomain, nsIObserver* aObserver, bool aHoldWeak) { UniquePtr pCallback; NS_ENSURE_ARG(aObserver); const nsCString& prefName = GetPrefName(aDomain); // Hold a weak reference to the observer if so requested. if (aHoldWeak) { nsCOMPtr weakRefFactory = do_QueryInterface(aObserver); if (!weakRefFactory) { // The caller didn't give us a object that supports weak reference... // tell them. return NS_ERROR_INVALID_ARG; } // Construct a PrefCallback with a weak reference to the observer. pCallback = MakeUnique(prefName, weakRefFactory, this); } else { // Construct a PrefCallback with a strong reference to the observer. pCallback = MakeUnique(prefName, aObserver, this); } mObservers.WithEntryHandle(pCallback.get(), [&](auto&& p) { if (p) { NS_WARNING("Ignoring duplicate observer."); } else { // We must pass a fully qualified preference name to the callback // aDomain == nullptr is the only possible failure, and we trapped it with // NS_ENSURE_ARG above. Preferences::RegisterCallback(NotifyObserver, prefName, pCallback.get(), Preferences::PrefixMatch, /* isPriority */ false); p.Insert(std::move(pCallback)); } }); return NS_OK; } NS_IMETHODIMP nsPrefBranch::RemoveObserverImpl(const nsACString& aDomain, nsIObserver* aObserver) { NS_ENSURE_ARG(aObserver); nsresult rv = NS_OK; // If we're in the middle of a call to FreeObserverList, don't process this // RemoveObserver call -- the observer in question will be removed soon, if // it hasn't been already. // // It's important that we don't touch mObservers in any way -- even a Get() // which returns null might cause the hashtable to resize itself, which will // break the iteration in FreeObserverList. if (mFreeingObserverList) { return NS_OK; } // Remove the relevant PrefCallback from mObservers and get an owning pointer // to it. Unregister the callback first, and then let the owning pointer go // out of scope and destroy the callback. const nsCString& prefName = GetPrefName(aDomain); PrefCallback key(prefName, aObserver, this); mozilla::UniquePtr pCallback; mObservers.Remove(&key, &pCallback); if (pCallback) { rv = Preferences::UnregisterCallback( NotifyObserver, prefName, pCallback.get(), Preferences::PrefixMatch); } return rv; } NS_IMETHODIMP nsPrefBranch::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { // Watch for xpcom shutdown and free our observers to eliminate any cyclic // references. if (!nsCRT::strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { FreeObserverList(); } return NS_OK; } /* static */ void nsPrefBranch::NotifyObserver(const char* aNewPref, void* aData) { PrefCallback* pCallback = (PrefCallback*)aData; nsCOMPtr observer = pCallback->GetObserver(); if (!observer) { // The observer has expired. Let's remove this callback. pCallback->GetPrefBranch()->RemoveExpiredCallback(pCallback); return; } // Remove any root this string may contain so as to not confuse the observer // by passing them something other than what they passed us as a topic. uint32_t len = pCallback->GetPrefBranch()->GetRootLength(); nsDependentCString suffix(aNewPref + len); observer->Observe(static_cast(pCallback->GetPrefBranch()), NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, NS_ConvertASCIItoUTF16(suffix).get()); } size_t nsPrefBranch::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { size_t n = aMallocSizeOf(this); n += mPrefRoot.SizeOfExcludingThisIfUnshared(aMallocSizeOf); n += mObservers.ShallowSizeOfExcludingThis(aMallocSizeOf); for (const auto& entry : mObservers) { const PrefCallback* data = entry.GetWeak(); n += data->SizeOfIncludingThis(aMallocSizeOf); } return n; } void nsPrefBranch::FreeObserverList() { // We need to prevent anyone from modifying mObservers while we're iterating // over it. In particular, some clients will call RemoveObserver() when // they're removed and destructed via the iterator; we set // mFreeingObserverList to keep those calls from touching mObservers. mFreeingObserverList = true; for (auto iter = mObservers.Iter(); !iter.Done(); iter.Next()) { auto callback = iter.UserData(); Preferences::UnregisterCallback(nsPrefBranch::NotifyObserver, callback->GetDomain(), callback, Preferences::PrefixMatch); iter.Remove(); } nsCOMPtr observerService = services::GetObserverService(); if (observerService) { observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); } mFreeingObserverList = false; } void nsPrefBranch::RemoveExpiredCallback(PrefCallback* aCallback) { MOZ_ASSERT(aCallback->IsExpired()); mObservers.Remove(aCallback); } nsresult nsPrefBranch::GetDefaultFromPropertiesFile(const char* aPrefName, nsAString& aReturn) { // The default value contains a URL to a .properties file. nsAutoCString propertyFileURL; nsresult rv = Preferences::GetCString(aPrefName, propertyFileURL, PrefValueKind::Default); if (NS_FAILED(rv)) { return rv; } nsCOMPtr bundleService = components::StringBundle::Service(); if (!bundleService) { return NS_ERROR_FAILURE; } nsCOMPtr bundle; rv = bundleService->CreateBundle(propertyFileURL.get(), getter_AddRefs(bundle)); if (NS_FAILED(rv)) { return rv; } return bundle->GetStringFromName(aPrefName, aReturn); } nsPrefBranch::PrefName nsPrefBranch::GetPrefName( const nsACString& aPrefName) const { if (mPrefRoot.IsEmpty()) { return PrefName(PromiseFlatCString(aPrefName)); } return PrefName(mPrefRoot + aPrefName); } //---------------------------------------------------------------------------- // nsPrefLocalizedString //---------------------------------------------------------------------------- nsPrefLocalizedString::nsPrefLocalizedString() = default; nsPrefLocalizedString::~nsPrefLocalizedString() = default; NS_IMPL_ISUPPORTS(nsPrefLocalizedString, nsIPrefLocalizedString, nsISupportsString) nsresult nsPrefLocalizedString::Init() { nsresult rv; mUnicodeString = do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv); return rv; } //---------------------------------------------------------------------------- // nsRelativeFilePref //---------------------------------------------------------------------------- NS_IMPL_ISUPPORTS(nsRelativeFilePref, nsIRelativeFilePref) nsRelativeFilePref::nsRelativeFilePref() = default; nsRelativeFilePref::~nsRelativeFilePref() = default; NS_IMETHODIMP nsRelativeFilePref::GetFile(nsIFile** aFile) { NS_ENSURE_ARG_POINTER(aFile); *aFile = mFile; NS_IF_ADDREF(*aFile); return NS_OK; } NS_IMETHODIMP nsRelativeFilePref::SetFile(nsIFile* aFile) { mFile = aFile; return NS_OK; } NS_IMETHODIMP nsRelativeFilePref::GetRelativeToKey(nsACString& aRelativeToKey) { aRelativeToKey.Assign(mRelativeToKey); return NS_OK; } NS_IMETHODIMP nsRelativeFilePref::SetRelativeToKey(const nsACString& aRelativeToKey) { mRelativeToKey.Assign(aRelativeToKey); return NS_OK; } //=========================================================================== // class Preferences and related things //=========================================================================== namespace mozilla { #define INITIAL_PREF_FILES 10 static NS_DEFINE_CID(kZipReaderCID, NS_ZIPREADER_CID); void Preferences::HandleDirty() { MOZ_ASSERT(XRE_IsParentProcess()); if (!HashTable() || !sPreferences) { return; } if (sPreferences->mProfileShutdown) { NS_WARNING("Setting user pref after profile shutdown."); return; } if (!sPreferences->mDirty) { sPreferences->mDirty = true; if (sPreferences->mCurrentFile && sPreferences->AllowOffMainThreadSave() && !sPreferences->mSavePending) { sPreferences->mSavePending = true; static const int PREF_DELAY_MS = 500; NS_DelayedDispatchToCurrentThread( NewRunnableMethod("Preferences::SavePrefFileAsynchronous", sPreferences.get(), &Preferences::SavePrefFileAsynchronous), PREF_DELAY_MS); } } } static nsresult openPrefFile(nsIFile* aFile, PrefValueKind aKind); static nsresult parsePrefData(const nsCString& aData, PrefValueKind aKind); // clang-format off static const char kPrefFileHeader[] = "// Mozilla User Preferences" NS_LINEBREAK NS_LINEBREAK "// DO NOT EDIT THIS FILE." NS_LINEBREAK "//" NS_LINEBREAK "// If you make changes to this file while the application is running," NS_LINEBREAK "// the changes will be overwritten when the application exits." NS_LINEBREAK "//" NS_LINEBREAK "// To change a preference value, you can either:" NS_LINEBREAK "// - modify it via the UI (e.g. via about:config in the browser); or" NS_LINEBREAK "// - set it within a user.js file in your profile." NS_LINEBREAK NS_LINEBREAK; // clang-format on // Note: if sShutdown is true, sPreferences will be nullptr. StaticRefPtr Preferences::sPreferences; bool Preferences::sShutdown = false; // This globally enables or disables OMT pref writing, both sync and async. static int32_t sAllowOMTPrefWrite = -1; // Write the preference data to a file. class PreferencesWriter final { public: PreferencesWriter() = default; static nsresult Write(nsIFile* aFile, PrefSaveData& aPrefs) { nsCOMPtr outStreamSink; nsCOMPtr outStream; uint32_t writeAmount; nsresult rv; // Execute a "safe" save by saving through a tempfile. rv = NS_NewSafeLocalFileOutputStream(getter_AddRefs(outStreamSink), aFile, -1, 0600); if (NS_FAILED(rv)) { return rv; } rv = NS_NewBufferedOutputStream(getter_AddRefs(outStream), outStreamSink.forget(), 4096); if (NS_FAILED(rv)) { return rv; } struct CharComparator { bool LessThan(const nsCString& aA, const nsCString& aB) const { return aA < aB; } bool Equals(const nsCString& aA, const nsCString& aB) const { return aA == aB; } }; // Sort the preferences to make a readable file on disk. aPrefs.Sort(CharComparator()); // Write out the file header. outStream->Write(kPrefFileHeader, sizeof(kPrefFileHeader) - 1, &writeAmount); for (nsCString& pref : aPrefs) { outStream->Write(pref.get(), pref.Length(), &writeAmount); outStream->Write(NS_LINEBREAK, NS_LINEBREAK_LEN, &writeAmount); } // Tell the safe output stream to overwrite the real prefs file. // (It'll abort if there were any errors during writing.) nsCOMPtr safeStream = do_QueryInterface(outStream); MOZ_ASSERT(safeStream, "expected a safe output stream!"); if (safeStream) { rv = safeStream->Finish(); } #ifdef DEBUG if (NS_FAILED(rv)) { NS_WARNING("failed to save prefs file! possible data loss"); } #endif return rv; } static void Flush() { MOZ_DIAGNOSTIC_ASSERT(sPendingWriteCount >= 0); // SpinEventLoopUntil is unfortunate, but ultimately it's the best thing // we can do here given the constraint that we need to ensure that // the preferences on disk match what we have in memory. We could // easily perform the write here ourselves by doing exactly what // happens in PWRunnable::Run. This would be the right thing to do // if we're stuck here because other unrelated runnables are taking // a long time, and the wrong thing to do if PreferencesWriter::Write // is what takes a long time, as we would be trading a SpinEventLoopUntil // for a synchronous disk write, wherein we could not even spin the // event loop. Given that PWRunnable generally runs on a thread pool, // if we're stuck here, it's likely because of PreferencesWriter::Write // and not some other runnable. Thus, spin away. mozilla::SpinEventLoopUntil("PreferencesWriter::Flush"_ns, []() { return sPendingWriteCount <= 0; }); } // This is the data that all of the runnables (see below) will attempt // to write. It will always have the most up to date version, or be // null, if the up to date information has already been written out. static Atomic sPendingWriteData; // This is the number of writes via PWRunnables which have been dispatched // but not yet completed. This is intended to be used by Flush to ensure // that there are no outstanding writes left incomplete, and thus our prefs // on disk are in sync with what we have in memory. static Atomic sPendingWriteCount; // See PWRunnable::Run for details on why we need this lock. static StaticMutex sWritingToFile MOZ_UNANNOTATED; }; Atomic PreferencesWriter::sPendingWriteData(nullptr); Atomic PreferencesWriter::sPendingWriteCount(0); StaticMutex PreferencesWriter::sWritingToFile; class PWRunnable : public Runnable { public: explicit PWRunnable(nsIFile* aFile) : Runnable("PWRunnable"), mFile(aFile) {} NS_IMETHOD Run() override { // Preference writes are handled a bit strangely, in that a "newer" // write is generally regarded as always better. For this reason, // sPendingWriteData can be overwritten multiple times before anyone // gets around to actually using it, minimizing writes. However, // once we've acquired sPendingWriteData we've reached a // "point of no return" and have to complete the write. // // Unfortunately, this design allows the following behaviour: // // 1. write1 is queued up // 2. thread1 acquires write1 // 3. write2 is queued up // 4. thread2 acquires write2 // 5. thread1 and thread2 concurrently clobber each other // // To avoid this, we use this lock to ensure that only one thread // at a time is trying to acquire the write, and when it does, // all other threads are prevented from acquiring writes until it // completes the write. New writes are still allowed to be queued // up in this time. // // Although it's atomic, the acquire needs to be guarded by the mutex // to avoid reordering of writes -- we don't want an older write to // run after a newer one. To avoid this causing too much waiting, we check // if sPendingWriteData is already null before acquiring the mutex. If it // is, then there's definitely no work to be done (or someone is in the // middle of doing it for us). // // Note that every time a new write is queued up, a new write task is // is also queued up, so there will always be a task that can see the newest // write. // // Ideally this lock wouldn't be necessary, and the PreferencesWriter // would be used more carefully, but it's hard to untangle all that. nsresult rv = NS_OK; if (PreferencesWriter::sPendingWriteData) { StaticMutexAutoLock lock(PreferencesWriter::sWritingToFile); // If we get a nullptr on the exchange, it means that somebody // else has already processed the request, and we can just return. UniquePtr prefs( PreferencesWriter::sPendingWriteData.exchange(nullptr)); if (prefs) { rv = PreferencesWriter::Write(mFile, *prefs); // Make a copy of these so we can have them in runnable lambda. // nsIFile is only there so that we would never release the // ref counted pointer off main thread. nsresult rvCopy = rv; nsCOMPtr fileCopy(mFile); SchedulerGroup::Dispatch( TaskCategory::Other, NS_NewRunnableFunction("Preferences::WriterRunnable", [fileCopy, rvCopy] { MOZ_RELEASE_ASSERT(NS_IsMainThread()); if (NS_FAILED(rvCopy)) { Preferences::HandleDirty(); } })); } } // We've completed the write to the best of our abilities, whether // we had prefs to write or another runnable got to them first. If // PreferencesWriter::Write failed, this is still correct as the // write is no longer outstanding, and the above HandleDirty call // will just start the cycle again. PreferencesWriter::sPendingWriteCount--; return rv; } protected: nsCOMPtr mFile; }; // Although this is a member of Preferences, it measures sPreferences and // several other global structures. /* static */ void Preferences::AddSizeOfIncludingThis(MallocSizeOf aMallocSizeOf, PrefsSizes& aSizes) { if (!sPreferences) { return; } aSizes.mMisc += aMallocSizeOf(sPreferences.get()); aSizes.mRootBranches += static_cast(sPreferences->mRootBranch.get()) ->SizeOfIncludingThis(aMallocSizeOf) + static_cast(sPreferences->mDefaultRootBranch.get()) ->SizeOfIncludingThis(aMallocSizeOf); } class PreferenceServiceReporter final : public nsIMemoryReporter { ~PreferenceServiceReporter() {} public: NS_DECL_ISUPPORTS NS_DECL_NSIMEMORYREPORTER protected: static const uint32_t kSuspectReferentCount = 1000; }; NS_IMPL_ISUPPORTS(PreferenceServiceReporter, nsIMemoryReporter) MOZ_DEFINE_MALLOC_SIZE_OF(PreferenceServiceMallocSizeOf) NS_IMETHODIMP PreferenceServiceReporter::CollectReports( nsIHandleReportCallback* aHandleReport, nsISupports* aData, bool aAnonymize) { MOZ_ASSERT(NS_IsMainThread()); MallocSizeOf mallocSizeOf = PreferenceServiceMallocSizeOf; PrefsSizes sizes; Preferences::AddSizeOfIncludingThis(mallocSizeOf, sizes); if (HashTable()) { sizes.mHashTable += HashTable()->shallowSizeOfIncludingThis(mallocSizeOf); for (auto iter = HashTable()->iter(); !iter.done(); iter.next()) { iter.get()->AddSizeOfIncludingThis(mallocSizeOf, sizes); } } sizes.mPrefNameArena += PrefNameArena().SizeOfExcludingThis(mallocSizeOf); for (CallbackNode* node = gFirstCallback; node; node = node->Next()) { node->AddSizeOfIncludingThis(mallocSizeOf, sizes); } if (gSharedMap) { sizes.mMisc += mallocSizeOf(gSharedMap); } #ifdef ACCESS_COUNTS if (gAccessCounts) { sizes.mMisc += gAccessCounts->ShallowSizeOfIncludingThis(mallocSizeOf); } #endif MOZ_COLLECT_REPORT("explicit/preferences/hash-table", KIND_HEAP, UNITS_BYTES, sizes.mHashTable, "Memory used by libpref's hash table."); MOZ_COLLECT_REPORT("explicit/preferences/pref-values", KIND_HEAP, UNITS_BYTES, sizes.mPrefValues, "Memory used by PrefValues hanging off the hash table."); MOZ_COLLECT_REPORT("explicit/preferences/string-values", KIND_HEAP, UNITS_BYTES, sizes.mStringValues, "Memory used by libpref's string pref values."); MOZ_COLLECT_REPORT("explicit/preferences/root-branches", KIND_HEAP, UNITS_BYTES, sizes.mRootBranches, "Memory used by libpref's root branches."); MOZ_COLLECT_REPORT("explicit/preferences/pref-name-arena", KIND_HEAP, UNITS_BYTES, sizes.mPrefNameArena, "Memory used by libpref's arena for pref names."); MOZ_COLLECT_REPORT("explicit/preferences/callbacks/objects", KIND_HEAP, UNITS_BYTES, sizes.mCallbacksObjects, "Memory used by pref callback objects."); MOZ_COLLECT_REPORT("explicit/preferences/callbacks/domains", KIND_HEAP, UNITS_BYTES, sizes.mCallbacksDomains, "Memory used by pref callback domains (pref names and " "prefixes)."); MOZ_COLLECT_REPORT("explicit/preferences/misc", KIND_HEAP, UNITS_BYTES, sizes.mMisc, "Miscellaneous memory used by libpref."); if (gSharedMap) { if (XRE_IsParentProcess()) { MOZ_COLLECT_REPORT("explicit/preferences/shared-memory-map", KIND_NONHEAP, UNITS_BYTES, gSharedMap->MapSize(), "The shared memory mapping used to share a " "snapshot of preference values across processes."); } } nsPrefBranch* rootBranch = static_cast(Preferences::GetRootBranch()); if (!rootBranch) { return NS_OK; } size_t numStrong = 0; size_t numWeakAlive = 0; size_t numWeakDead = 0; nsTArray suspectPreferences; // Count of the number of referents for each preference. nsTHashMap prefCounter; for (const auto& entry : rootBranch->mObservers) { auto* callback = entry.GetWeak(); if (callback->IsWeak()) { nsCOMPtr callbackRef = do_QueryReferent(callback->mWeakRef); if (callbackRef) { numWeakAlive++; } else { numWeakDead++; } } else { numStrong++; } const uint32_t currentCount = prefCounter.Get(callback->GetDomain()) + 1; prefCounter.InsertOrUpdate(callback->GetDomain(), currentCount); // Keep track of preferences that have a suspiciously large number of // referents (a symptom of a leak). if (currentCount == kSuspectReferentCount) { suspectPreferences.AppendElement(callback->GetDomain()); } } for (uint32_t i = 0; i < suspectPreferences.Length(); i++) { nsCString& suspect = suspectPreferences[i]; const uint32_t totalReferentCount = prefCounter.Get(suspect); nsPrintfCString suspectPath( "preference-service-suspect/" "referent(pref=%s)", suspect.get()); aHandleReport->Callback( /* process = */ ""_ns, suspectPath, KIND_OTHER, UNITS_COUNT, totalReferentCount, "A preference with a suspiciously large number " "referents (symptom of a leak)."_ns, aData); } MOZ_COLLECT_REPORT( "preference-service/referent/strong", KIND_OTHER, UNITS_COUNT, numStrong, "The number of strong referents held by the preference service."); MOZ_COLLECT_REPORT( "preference-service/referent/weak/alive", KIND_OTHER, UNITS_COUNT, numWeakAlive, "The number of weak referents held by the preference service that are " "still alive."); MOZ_COLLECT_REPORT( "preference-service/referent/weak/dead", KIND_OTHER, UNITS_COUNT, numWeakDead, "The number of weak referents held by the preference service that are " "dead."); return NS_OK; } namespace { class AddPreferencesMemoryReporterRunnable : public Runnable { public: AddPreferencesMemoryReporterRunnable() : Runnable("AddPreferencesMemoryReporterRunnable") {} NS_IMETHOD Run() override { return RegisterStrongMemoryReporter(new PreferenceServiceReporter()); } }; } // namespace // A list of changed prefs sent from the parent via shared memory. static nsTArray* gChangedDomPrefs; static const char kTelemetryPref[] = "toolkit.telemetry.enabled"; static const char kChannelPref[] = "app.update.channel"; #ifdef MOZ_WIDGET_ANDROID static Maybe TelemetryPrefValue() { // Leave it unchanged if it's already set. // XXX: how could it already be set? if (Preferences::GetType(kTelemetryPref) != nsIPrefBranch::PREF_INVALID) { return Nothing(); } // Determine the correct default for toolkit.telemetry.enabled. If this // build has MOZ_TELEMETRY_ON_BY_DEFAULT *or* we're on the beta channel, // telemetry is on by default, otherwise not. This is necessary so that // beta users who are testing final release builds don't flipflop defaults. # ifdef MOZ_TELEMETRY_ON_BY_DEFAULT return Some(true); # else nsAutoCString channelPrefValue; Unused << Preferences::GetCString(kChannelPref, channelPrefValue, PrefValueKind::Default); return Some(channelPrefValue.EqualsLiteral("beta")); # endif } /* static */ void Preferences::SetupTelemetryPref() { MOZ_ASSERT(XRE_IsParentProcess()); Maybe telemetryPrefValue = TelemetryPrefValue(); if (telemetryPrefValue.isSome()) { Preferences::SetBool(kTelemetryPref, *telemetryPrefValue, PrefValueKind::Default); } } #else // !MOZ_WIDGET_ANDROID static bool TelemetryPrefValue() { // For platforms with Unified Telemetry (here meaning not-Android), // toolkit.telemetry.enabled determines whether we send "extended" data. // We only want extended data from pre-release channels due to size. constexpr auto channel = MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL) ""_ns; // Easy cases: Nightly, Aurora, Beta. if (channel.EqualsLiteral("nightly") || channel.EqualsLiteral("aurora") || channel.EqualsLiteral("beta")) { return true; } # ifndef MOZILLA_OFFICIAL // Local developer builds: non-official builds on the "default" channel. if (channel.EqualsLiteral("default")) { return true; } # endif // Release Candidate builds: builds that think they are release builds, but // are shipped to beta users. if (channel.EqualsLiteral("release")) { nsAutoCString channelPrefValue; Unused << Preferences::GetCString(kChannelPref, channelPrefValue, PrefValueKind::Default); if (channelPrefValue.EqualsLiteral("beta")) { return true; } } return false; } /* static */ void Preferences::SetupTelemetryPref() { MOZ_ASSERT(XRE_IsParentProcess()); Preferences::SetBool(kTelemetryPref, TelemetryPrefValue(), PrefValueKind::Default); Preferences::Lock(kTelemetryPref); } static void CheckTelemetryPref() { MOZ_ASSERT(!XRE_IsParentProcess()); // Make sure the children got passed the right telemetry pref details. DebugOnly value; MOZ_ASSERT(NS_SUCCEEDED(Preferences::GetBool(kTelemetryPref, &value)) && value == TelemetryPrefValue()); MOZ_ASSERT(Preferences::IsLocked(kTelemetryPref)); } #endif // MOZ_WIDGET_ANDROID /* static */ already_AddRefed Preferences::GetInstanceForService() { if (sPreferences) { return do_AddRef(sPreferences); } if (sShutdown) { return nullptr; } sPreferences = new Preferences(); MOZ_ASSERT(!HashTable()); HashTable() = new PrefsHashTable(XRE_IsParentProcess() ? kHashTableInitialLengthParent : kHashTableInitialLengthContent); #ifdef DEBUG gOnceStaticPrefsAntiFootgun = new AntiFootgunMap(); #endif #ifdef ACCESS_COUNTS MOZ_ASSERT(!gAccessCounts); gAccessCounts = new AccessCountsHashTable(); #endif nsresult rv = InitInitialObjects(/* isStartup */ true); if (NS_FAILED(rv)) { sPreferences = nullptr; return nullptr; } if (!XRE_IsParentProcess()) { MOZ_ASSERT(gChangedDomPrefs); for (unsigned int i = 0; i < gChangedDomPrefs->Length(); i++) { Preferences::SetPreference(gChangedDomPrefs->ElementAt(i)); } delete gChangedDomPrefs; gChangedDomPrefs = nullptr; #ifndef MOZ_WIDGET_ANDROID CheckTelemetryPref(); #endif } else { // Check if there is a deployment configuration file. If so, set up the // pref config machinery, which will actually read the file. nsAutoCString lockFileName; nsresult rv = Preferences::GetCString("general.config.filename", lockFileName, PrefValueKind::User); if (NS_SUCCEEDED(rv)) { NS_CreateServicesFromCategory( "pref-config-startup", static_cast(static_cast(sPreferences)), "pref-config-startup"); } nsCOMPtr observerService = services::GetObserverService(); if (!observerService) { sPreferences = nullptr; return nullptr; } observerService->AddObserver(sPreferences, "profile-before-change-telemetry", true); rv = observerService->AddObserver(sPreferences, "profile-before-change", true); observerService->AddObserver(sPreferences, "suspend_process_notification", true); if (NS_FAILED(rv)) { sPreferences = nullptr; return nullptr; } } const char* defaultPrefs = getenv("MOZ_DEFAULT_PREFS"); if (defaultPrefs) { parsePrefData(nsCString(defaultPrefs), PrefValueKind::Default); } // Preferences::GetInstanceForService() can be called from GetService(), and // RegisterStrongMemoryReporter calls GetService(nsIMemoryReporter). To // avoid a potential recursive GetService() call, we can't register the // memory reporter here; instead, do it off a runnable. RefPtr runnable = new AddPreferencesMemoryReporterRunnable(); NS_DispatchToMainThread(runnable); return do_AddRef(sPreferences); } /* static */ bool Preferences::IsServiceAvailable() { return !!sPreferences; } /* static */ bool Preferences::InitStaticMembers() { MOZ_ASSERT(NS_IsMainThread() || ServoStyleSet::IsInServoTraversal()); if (MOZ_LIKELY(sPreferences)) { return true; } if (!sShutdown) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr prefService = do_GetService(NS_PREFSERVICE_CONTRACTID); } return sPreferences != nullptr; } /* static */ void Preferences::Shutdown() { if (!sShutdown) { sShutdown = true; // Don't create the singleton instance after here. sPreferences = nullptr; StaticPrefs::ShutdownAlwaysPrefs(); } } Preferences::Preferences() : mRootBranch(new nsPrefBranch("", PrefValueKind::User)), mDefaultRootBranch(new nsPrefBranch("", PrefValueKind::Default)) {} Preferences::~Preferences() { MOZ_ASSERT(!sPreferences); MOZ_ASSERT(!gCallbacksInProgress); CallbackNode* node = gFirstCallback; while (node) { CallbackNode* next_node = node->Next(); delete node; node = next_node; } gLastPriorityNode = gFirstCallback = nullptr; delete HashTable(); HashTable() = nullptr; #ifdef DEBUG delete gOnceStaticPrefsAntiFootgun; gOnceStaticPrefsAntiFootgun = nullptr; #endif #ifdef ACCESS_COUNTS delete gAccessCounts; #endif gSharedMap = nullptr; PrefNameArena().Clear(); } NS_IMPL_ISUPPORTS(Preferences, nsIPrefService, nsIObserver, nsIPrefBranch, nsISupportsWeakReference) /* static */ void Preferences::SerializePreferences(nsCString& aStr, bool aIsDestinationWebContentProcess) { MOZ_RELEASE_ASSERT(InitStaticMembers()); aStr.Truncate(); for (auto iter = HashTable()->iter(); !iter.done(); iter.next()) { Pref* pref = iter.get().get(); if (!pref->IsTypeNone() && pref->HasAdvisablySizedValues()) { pref->SerializeAndAppend(aStr, aIsDestinationWebContentProcess && ShouldSanitizePreference(pref)); } } aStr.Append('\0'); } /* static */ void Preferences::DeserializePreferences(char* aStr, size_t aPrefsLen) { MOZ_ASSERT(!XRE_IsParentProcess()); MOZ_ASSERT(!gChangedDomPrefs); gChangedDomPrefs = new nsTArray(); char* p = aStr; while (*p != '\0') { dom::Pref pref; p = Pref::Deserialize(p, &pref); gChangedDomPrefs->AppendElement(pref); } // We finished parsing on a '\0'. That should be the last char in the shared // memory. (aPrefsLen includes the '\0'.) MOZ_ASSERT(p == aStr + aPrefsLen - 1); MOZ_ASSERT(!gContentProcessPrefsAreInited); gContentProcessPrefsAreInited = true; } /* static */ FileDescriptor Preferences::EnsureSnapshot(size_t* aSize) { MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(NS_IsMainThread()); if (!gSharedMap) { SharedPrefMapBuilder builder; nsTArray toRepopulate; NameArena* newPrefNameArena = new NameArena(); for (auto iter = HashTable()->modIter(); !iter.done(); iter.next()) { if (!ShouldSanitizePreference(iter.get().get())) { iter.get()->AddToMap(builder); } else { Pref* pref = iter.getMutable().release(); pref->RelocateName(newPrefNameArena); toRepopulate.AppendElement(pref); } } // Store the current value of `once`-mirrored prefs. After this point they // will be immutable. StaticPrefs::RegisterOncePrefs(builder); gSharedMap = new SharedPrefMap(std::move(builder)); // Once we've built a snapshot of the database, there's no need to continue // storing dynamic copies of the preferences it contains. Once we reset the // hashtable, preference lookups will fall back to the snapshot for any // preferences not in the dynamic hashtable. // // And since the majority of the database is now contained in the snapshot, // we can initialize the hashtable with the expected number of per-session // changed preferences, rather than the expected total number of // preferences. HashTable()->clearAndCompact(); Unused << HashTable()->reserve(kHashTableInitialLengthContent); delete sPrefNameArena; sPrefNameArena = newPrefNameArena; gCallbackPref = nullptr; for (uint32_t i = 0; i < toRepopulate.Length(); i++) { auto pref = toRepopulate[i]; auto p = HashTable()->lookupForAdd(pref->Name()); MOZ_ASSERT(!p.found()); Unused << HashTable()->add(p, pref); } } *aSize = gSharedMap->MapSize(); return gSharedMap->CloneFileDescriptor(); } /* static */ void Preferences::InitSnapshot(const FileDescriptor& aHandle, size_t aSize) { MOZ_ASSERT(!XRE_IsParentProcess()); MOZ_ASSERT(!gSharedMap); gSharedMap = new SharedPrefMap(aHandle, aSize); StaticPrefs::InitStaticPrefsFromShared(); } /* static */ void Preferences::InitializeUserPrefs() { MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(!sPreferences->mCurrentFile, "Should only initialize prefs once"); // Prefs which are set before we initialize the profile are silently // discarded. This is stupid, but there are various tests which depend on // this behavior. sPreferences->ResetUserPrefs(); nsCOMPtr prefsFile = sPreferences->ReadSavedPrefs(); sPreferences->ReadUserOverridePrefs(); sPreferences->mDirty = false; // Don't set mCurrentFile until we're done so that dirty flags work properly. sPreferences->mCurrentFile = std::move(prefsFile); } /* static */ void Preferences::FinishInitializingUserPrefs() { sPreferences->NotifyServiceObservers(NS_PREFSERVICE_READ_TOPIC_ID); } NS_IMETHODIMP Preferences::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* someData) { if (MOZ_UNLIKELY(!XRE_IsParentProcess())) { return NS_ERROR_NOT_AVAILABLE; } nsresult rv = NS_OK; if (!nsCRT::strcmp(aTopic, "profile-before-change")) { // Normally prefs aren't written after this point, and so we kick off // an asynchronous pref save so that I/O can be done in parallel with // other shutdown. if (AllowOffMainThreadSave()) { SavePrefFile(nullptr); } } else if (!nsCRT::strcmp(aTopic, "profile-before-change-telemetry")) { // It's possible that a profile-before-change observer after ours // set a pref. A blocking save here re-saves if necessary and also waits // for any pending saves to complete. SavePrefFileBlocking(); MOZ_ASSERT(!mDirty, "Preferences should not be dirty"); mProfileShutdown = true; } else if (!nsCRT::strcmp(aTopic, "reload-default-prefs")) { // Reload the default prefs from file. Unused << InitInitialObjects(/* isStartup */ false); } else if (!nsCRT::strcmp(aTopic, "suspend_process_notification")) { // Our process is being suspended. The OS may wake our process later, // or it may kill the process. In case our process is going to be killed // from the suspended state, we save preferences before suspending. rv = SavePrefFileBlocking(); } return rv; } NS_IMETHODIMP Preferences::ReadDefaultPrefsFromFile(nsIFile* aFile) { ENSURE_PARENT_PROCESS("Preferences::ReadDefaultPrefsFromFile", "all prefs"); if (!aFile) { NS_ERROR("ReadDefaultPrefsFromFile requires a parameter"); return NS_ERROR_INVALID_ARG; } return openPrefFile(aFile, PrefValueKind::Default); } NS_IMETHODIMP Preferences::ReadUserPrefsFromFile(nsIFile* aFile) { ENSURE_PARENT_PROCESS("Preferences::ReadUserPrefsFromFile", "all prefs"); if (!aFile) { NS_ERROR("ReadUserPrefsFromFile requires a parameter"); return NS_ERROR_INVALID_ARG; } return openPrefFile(aFile, PrefValueKind::User); } NS_IMETHODIMP Preferences::ResetPrefs() { ENSURE_PARENT_PROCESS("Preferences::ResetPrefs", "all prefs"); if (gSharedMap) { return NS_ERROR_NOT_AVAILABLE; } HashTable()->clearAndCompact(); Unused << HashTable()->reserve(kHashTableInitialLengthParent); PrefNameArena().Clear(); return InitInitialObjects(/* isStartup */ false); } nsresult Preferences::ResetUserPrefs() { ENSURE_PARENT_PROCESS("Preferences::ResetUserPrefs", "all prefs"); NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); MOZ_ASSERT(NS_IsMainThread()); Vector prefNames; for (auto iter = HashTable()->modIter(); !iter.done(); iter.next()) { Pref* pref = iter.get().get(); if (pref->HasUserValue()) { if (!prefNames.append(pref->Name())) { return NS_ERROR_OUT_OF_MEMORY; } pref->ClearUserValue(); if (!pref->HasDefaultValue()) { iter.remove(); } } } for (const char* prefName : prefNames) { NotifyCallbacks(nsDependentCString(prefName)); } Preferences::HandleDirty(); return NS_OK; } bool Preferences::AllowOffMainThreadSave() { // Put in a preference that allows us to disable off main thread preference // file save. if (sAllowOMTPrefWrite < 0) { bool value = false; Preferences::GetBool("preferences.allow.omt-write", &value); sAllowOMTPrefWrite = value ? 1 : 0; } return !!sAllowOMTPrefWrite; } nsresult Preferences::SavePrefFileBlocking() { if (mDirty) { return SavePrefFileInternal(nullptr, SaveMethod::Blocking); } // If we weren't dirty to start, SavePrefFileInternal will early exit so // there is no guarantee that we don't have oustanding async saves in the // pipe. Since the contract of SavePrefFileOnMainThread is that the file on // disk matches the preferences, we have to make sure those requests are // completed. if (AllowOffMainThreadSave()) { PreferencesWriter::Flush(); } return NS_OK; } nsresult Preferences::SavePrefFileAsynchronous() { return SavePrefFileInternal(nullptr, SaveMethod::Asynchronous); } NS_IMETHODIMP Preferences::SavePrefFile(nsIFile* aFile) { // This is the method accessible from service API. Make it off main thread. return SavePrefFileInternal(aFile, SaveMethod::Asynchronous); } /* static */ void Preferences::SetPreference(const dom::Pref& aDomPref) { MOZ_ASSERT(!XRE_IsParentProcess()); NS_ENSURE_TRUE(InitStaticMembers(), (void)0); const nsCString& prefName = aDomPref.name(); Pref* pref; auto p = HashTable()->lookupForAdd(prefName.get()); if (!p) { pref = new Pref(prefName); if (!HashTable()->add(p, pref)) { delete pref; return; } } else { pref = p->get(); } bool valueChanged = false; pref->FromDomPref(aDomPref, &valueChanged); // When the parent process clears a pref's user value we get a DomPref here // with no default value and no user value. There are two possibilities. // // - There was an existing pref with only a user value. FromDomPref() will // have just cleared that user value, so the pref can be removed. // // - There was no existing pref. FromDomPref() will have done nothing, and // `pref` will be valueless. We will end up adding and removing the value // needlessly, but that's ok because this case is rare. // if (!pref->HasDefaultValue() && !pref->HasUserValue() && !pref->IsSanitized()) { // If the preference exists in the shared map, we need to keep the dynamic // entry around to mask it. if (gSharedMap->Has(pref->Name())) { pref->SetType(PrefType::None); } else { HashTable()->remove(prefName.get()); } pref = nullptr; } // Note: we don't have to worry about HandleDirty() because we are setting // prefs in the content process that have come from the parent process. if (valueChanged) { if (pref) { NotifyCallbacks(prefName, PrefWrapper(pref)); } else { NotifyCallbacks(prefName); } } } /* static */ void Preferences::GetPreference(dom::Pref* aDomPref, const GeckoProcessType aDestinationProcessType, const nsACString& aDestinationRemoteType) { MOZ_ASSERT(XRE_IsParentProcess()); bool destIsWebContent = aDestinationProcessType == GeckoProcessType_Content && (StringBeginsWith(aDestinationRemoteType, WEB_REMOTE_TYPE) || StringBeginsWith(aDestinationRemoteType, PREALLOC_REMOTE_TYPE) || StringBeginsWith(aDestinationRemoteType, PRIVILEGEDMOZILLA_REMOTE_TYPE)); Pref* pref = pref_HashTableLookup(aDomPref->name().get()); if (pref && pref->HasAdvisablySizedValues()) { pref->ToDomPref(aDomPref, destIsWebContent); } } #ifdef DEBUG bool Preferences::ArePrefsInitedInContentProcess() { MOZ_ASSERT(!XRE_IsParentProcess()); return gContentProcessPrefsAreInited; } #endif NS_IMETHODIMP Preferences::GetBranch(const char* aPrefRoot, nsIPrefBranch** aRetVal) { if ((nullptr != aPrefRoot) && (*aPrefRoot != '\0')) { // TODO: Cache this stuff and allow consumers to share branches (hold weak // references, I think). RefPtr prefBranch = new nsPrefBranch(aPrefRoot, PrefValueKind::User); prefBranch.forget(aRetVal); } else { // Special case: caching the default root. nsCOMPtr root(sPreferences->mRootBranch); root.forget(aRetVal); } return NS_OK; } NS_IMETHODIMP Preferences::GetDefaultBranch(const char* aPrefRoot, nsIPrefBranch** aRetVal) { if (!aPrefRoot || !aPrefRoot[0]) { nsCOMPtr root(sPreferences->mDefaultRootBranch); root.forget(aRetVal); return NS_OK; } // TODO: Cache this stuff and allow consumers to share branches (hold weak // references, I think). RefPtr prefBranch = new nsPrefBranch(aPrefRoot, PrefValueKind::Default); if (!prefBranch) { return NS_ERROR_OUT_OF_MEMORY; } prefBranch.forget(aRetVal); return NS_OK; } NS_IMETHODIMP Preferences::ReadStats(nsIPrefStatsCallback* aCallback) { #ifdef ACCESS_COUNTS for (const auto& entry : *gAccessCounts) { aCallback->Visit(entry.GetKey(), entry.GetData()); } return NS_OK; #else return NS_ERROR_NOT_IMPLEMENTED; #endif } NS_IMETHODIMP Preferences::ResetStats() { #ifdef ACCESS_COUNTS gAccessCounts->Clear(); return NS_OK; #else return NS_ERROR_NOT_IMPLEMENTED; #endif } // We would much prefer to use C++ lambdas, but we cannot convert // lambdas that capture (here, the underlying observer) to C pointer // to functions. So, here we are, with icky C callbacks. Be aware // that nothing is thread-safe here because there's a single global // `nsIPrefObserver` instance. Use this from the main thread only. nsIPrefObserver* PrefObserver = nullptr; void HandlePref(const char* aPrefName, PrefType aType, PrefValueKind aKind, PrefValue aValue, bool aIsSticky, bool aIsLocked) { MOZ_ASSERT(NS_IsMainThread()); if (!PrefObserver) { return; } const char* kind = aKind == PrefValueKind::Default ? "Default" : "User"; switch (aType) { case PrefType::String: PrefObserver->OnStringPref(kind, aPrefName, aValue.mStringVal, aIsSticky, aIsLocked); break; case PrefType::Int: PrefObserver->OnIntPref(kind, aPrefName, aValue.mIntVal, aIsSticky, aIsLocked); break; case PrefType::Bool: PrefObserver->OnBoolPref(kind, aPrefName, aValue.mBoolVal, aIsSticky, aIsLocked); break; default: PrefObserver->OnError("Unexpected pref type."); } } void HandleError(const char* aMsg) { MOZ_ASSERT(NS_IsMainThread()); if (!PrefObserver) { return; } PrefObserver->OnError(aMsg); } NS_IMETHODIMP Preferences::ParsePrefsFromBuffer(const nsTArray& aBytes, nsIPrefObserver* aObserver, const char* aPathLabel) { MOZ_ASSERT(NS_IsMainThread()); // We need a null-terminated buffer. nsTArray data = aBytes.Clone(); data.AppendElement(0); // Parsing as default handles both `pref` and `user_pref`. PrefObserver = aObserver; prefs_parser_parse(aPathLabel ? aPathLabel : "", PrefValueKind::Default, (const char*)data.Elements(), data.Length() - 1, HandlePref, HandleError); PrefObserver = nullptr; return NS_OK; } NS_IMETHODIMP Preferences::GetDirty(bool* aRetVal) { *aRetVal = mDirty; return NS_OK; } nsresult Preferences::NotifyServiceObservers(const char* aTopic) { nsCOMPtr observerService = services::GetObserverService(); if (!observerService) { return NS_ERROR_FAILURE; } auto subject = static_cast(this); observerService->NotifyObservers(subject, aTopic, nullptr); return NS_OK; } already_AddRefed Preferences::ReadSavedPrefs() { nsCOMPtr file; nsresult rv = NS_GetSpecialDirectory(NS_APP_PREFS_50_FILE, getter_AddRefs(file)); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } rv = openPrefFile(file, PrefValueKind::User); if (rv == NS_ERROR_FILE_NOT_FOUND) { // This is a normal case for new users. rv = NS_OK; } else if (NS_FAILED(rv)) { // Save a backup copy of the current (invalid) prefs file, since all prefs // from the error line to the end of the file will be lost (bug 361102). // TODO we should notify the user about it (bug 523725). Telemetry::ScalarSet( Telemetry::ScalarID::PREFERENCES_PREFS_FILE_WAS_INVALID, true); MakeBackupPrefFile(file); } return file.forget(); } void Preferences::ReadUserOverridePrefs() { nsCOMPtr aFile; nsresult rv = NS_GetSpecialDirectory(NS_APP_PREFS_50_DIR, getter_AddRefs(aFile)); if (NS_WARN_IF(NS_FAILED(rv))) { return; } aFile->AppendNative("user.js"_ns); rv = openPrefFile(aFile, PrefValueKind::User); } nsresult Preferences::MakeBackupPrefFile(nsIFile* aFile) { // Example: this copies "prefs.js" to "Invalidprefs.js" in the same directory. // "Invalidprefs.js" is removed if it exists, prior to making the copy. nsAutoString newFilename; nsresult rv = aFile->GetLeafName(newFilename); NS_ENSURE_SUCCESS(rv, rv); newFilename.InsertLiteral(u"Invalid", 0); nsCOMPtr newFile; rv = aFile->GetParent(getter_AddRefs(newFile)); NS_ENSURE_SUCCESS(rv, rv); rv = newFile->Append(newFilename); NS_ENSURE_SUCCESS(rv, rv); bool exists = false; newFile->Exists(&exists); if (exists) { rv = newFile->Remove(false); NS_ENSURE_SUCCESS(rv, rv); } rv = aFile->CopyTo(nullptr, newFilename); NS_ENSURE_SUCCESS(rv, rv); return rv; } nsresult Preferences::SavePrefFileInternal(nsIFile* aFile, SaveMethod aSaveMethod) { ENSURE_PARENT_PROCESS("Preferences::SavePrefFileInternal", "all prefs"); // We allow different behavior here when aFile argument is not null, but it // happens to be the same as the current file. It is not clear that we // should, but it does give us a "force" save on the unmodified pref file // (see the original bug 160377 when we added this.) if (nullptr == aFile) { mSavePending = false; // Off main thread writing only if allowed. if (!AllowOffMainThreadSave()) { aSaveMethod = SaveMethod::Blocking; } // The mDirty flag tells us if we should write to mCurrentFile. We only // check this flag when the caller wants to write to the default. if (!mDirty) { return NS_OK; } // Check for profile shutdown after mDirty because the runnables from // HandleDirty() can still be pending. if (mProfileShutdown) { NS_WARNING("Cannot save pref file after profile shutdown."); return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; } // It's possible that we never got a prefs file. nsresult rv = NS_OK; if (mCurrentFile) { rv = WritePrefFile(mCurrentFile, aSaveMethod); } // If we succeeded writing to mCurrentFile, reset the dirty flag. if (NS_SUCCEEDED(rv)) { mDirty = false; } return rv; } else { // We only allow off main thread writes on mCurrentFile. return WritePrefFile(aFile, SaveMethod::Blocking); } } nsresult Preferences::WritePrefFile(nsIFile* aFile, SaveMethod aSaveMethod) { MOZ_ASSERT(XRE_IsParentProcess()); if (!HashTable()) { return NS_ERROR_NOT_INITIALIZED; } AUTO_PROFILER_LABEL("Preferences::WritePrefFile", OTHER); if (AllowOffMainThreadSave()) { nsresult rv = NS_OK; UniquePtr prefs = MakeUnique(pref_savePrefs()); // Put the newly constructed preference data into sPendingWriteData // for the next request to pick up prefs.reset(PreferencesWriter::sPendingWriteData.exchange(prefs.release())); if (prefs) { // There was a previous request that hasn't been processed, // and this is the data it had. return rv; } // There were no previous requests. Dispatch one since sPendingWriteData has // the up to date information. nsCOMPtr target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv); if (NS_SUCCEEDED(rv)) { bool async = aSaveMethod == SaveMethod::Asynchronous; // Increment sPendingWriteCount, even though it's redundant to track this // in the case of a sync runnable; it just makes it easier to simply // decrement this inside PWRunnable. We cannot use the constructor / // destructor for increment/decrement, as on dispatch failure we might // leak the runnable in order to not destroy it on the wrong thread, which // would make us get stuck in an infinite SpinEventLoopUntil inside // PreferencesWriter::Flush. Better that in future code we miss an // increment of sPendingWriteCount and cause a simple crash due to it // ending up negative. PreferencesWriter::sPendingWriteCount++; if (async) { rv = target->Dispatch(new PWRunnable(aFile), nsIEventTarget::DISPATCH_NORMAL); } else { rv = SyncRunnable::DispatchToThread(target, new PWRunnable(aFile), true); } if (NS_FAILED(rv)) { // If our dispatch failed, we should correct our bookkeeping to // avoid shutdown hangs. PreferencesWriter::sPendingWriteCount--; } return rv; } // If we can't get the thread for writing, for whatever reason, do the main // thread write after making some noise. MOZ_ASSERT(false, "failed to get the target thread for OMT pref write"); } // This will do a main thread write. It is safe to do it this way because // AllowOffMainThreadSave() returns a consistent value for the lifetime of // the parent process. PrefSaveData prefsData = pref_savePrefs(); return PreferencesWriter::Write(aFile, prefsData); } static nsresult openPrefFile(nsIFile* aFile, PrefValueKind aKind) { MOZ_ASSERT(XRE_IsParentProcess()); nsCString data; MOZ_TRY_VAR(data, URLPreloader::ReadFile(aFile)); nsAutoString filenameUtf16; aFile->GetLeafName(filenameUtf16); NS_ConvertUTF16toUTF8 filename(filenameUtf16); nsAutoString path; aFile->GetPath(path); Parser parser; if (!parser.Parse(aKind, NS_ConvertUTF16toUTF8(path).get(), data)) { return NS_ERROR_FILE_CORRUPTED; } return NS_OK; } static nsresult parsePrefData(const nsCString& aData, PrefValueKind aKind) { const nsCString path = "$MOZ_DEFAULT_PREFS"_ns; Parser parser; if (!parser.Parse(aKind, path.get(), aData)) { return NS_ERROR_FILE_CORRUPTED; } return NS_OK; } static int pref_CompareFileNames(nsIFile* aFile1, nsIFile* aFile2, void* /* unused */) { nsAutoCString filename1, filename2; aFile1->GetNativeLeafName(filename1); aFile2->GetNativeLeafName(filename2); return Compare(filename2, filename1); } // Load default pref files from a directory. The files in the directory are // sorted reverse-alphabetically; a set of "special file names" may be // specified which are loaded after all the others. static nsresult pref_LoadPrefsInDir(nsIFile* aDir, char const* const* aSpecialFiles, uint32_t aSpecialFilesCount) { MOZ_ASSERT(XRE_IsParentProcess()); nsresult rv, rv2; nsCOMPtr dirIterator; // This may fail in some normal cases, such as embedders who do not use a // GRE. rv = aDir->GetDirectoryEntries(getter_AddRefs(dirIterator)); if (NS_FAILED(rv)) { // If the directory doesn't exist, then we have no reason to complain. We // loaded everything (and nothing) successfully. if (rv == NS_ERROR_FILE_NOT_FOUND) { rv = NS_OK; } return rv; } nsCOMArray prefFiles(INITIAL_PREF_FILES); nsCOMArray specialFiles(aSpecialFilesCount); nsCOMPtr prefFile; while (NS_SUCCEEDED(dirIterator->GetNextFile(getter_AddRefs(prefFile))) && prefFile) { nsAutoCString leafName; prefFile->GetNativeLeafName(leafName); MOZ_ASSERT( !leafName.IsEmpty(), "Failure in default prefs: directory enumerator returned empty file?"); // Skip non-js files. if (StringEndsWith(leafName, ".js"_ns, nsCaseInsensitiveCStringComparator)) { bool shouldParse = true; // Separate out special files. for (uint32_t i = 0; i < aSpecialFilesCount; ++i) { if (leafName.Equals(nsDependentCString(aSpecialFiles[i]))) { shouldParse = false; // Special files should be processed in order. We put them into the // array by index, which can make the array sparse. specialFiles.ReplaceObjectAt(prefFile, i); } } if (shouldParse) { prefFiles.AppendObject(prefFile); } } } if (prefFiles.Count() + specialFiles.Count() == 0) { NS_WARNING("No default pref files found."); if (NS_SUCCEEDED(rv)) { rv = NS_SUCCESS_FILE_DIRECTORY_EMPTY; } return rv; } prefFiles.Sort(pref_CompareFileNames, nullptr); uint32_t arrayCount = prefFiles.Count(); uint32_t i; for (i = 0; i < arrayCount; ++i) { rv2 = openPrefFile(prefFiles[i], PrefValueKind::Default); if (NS_FAILED(rv2)) { NS_ERROR("Default pref file not parsed successfully."); rv = rv2; } } arrayCount = specialFiles.Count(); for (i = 0; i < arrayCount; ++i) { // This may be a sparse array; test before parsing. nsIFile* file = specialFiles[i]; if (file) { rv2 = openPrefFile(file, PrefValueKind::Default); if (NS_FAILED(rv2)) { NS_ERROR("Special default pref file not parsed successfully."); rv = rv2; } } } return rv; } static nsresult pref_ReadPrefFromJar(nsZipArchive* aJarReader, const char* aName) { nsCString manifest; MOZ_TRY_VAR(manifest, URLPreloader::ReadZip(aJarReader, nsDependentCString(aName))); Parser parser; if (!parser.Parse(PrefValueKind::Default, aName, manifest)) { return NS_ERROR_FILE_CORRUPTED; } return NS_OK; } static nsresult pref_ReadDefaultPrefs(const RefPtr jarReader, const char* path) { UniquePtr find; nsTArray prefEntries; const char* entryName; uint16_t entryNameLen; nsresult rv = jarReader->FindInit(path, getter_Transfers(find)); NS_ENSURE_SUCCESS(rv, rv); while (NS_SUCCEEDED(find->FindNext(&entryName, &entryNameLen))) { prefEntries.AppendElement(Substring(entryName, entryNameLen)); } prefEntries.Sort(); for (uint32_t i = prefEntries.Length(); i--;) { rv = pref_ReadPrefFromJar(jarReader, prefEntries[i].get()); if (NS_FAILED(rv)) { NS_WARNING("Error parsing preferences."); } } return NS_OK; } static nsCString PrefValueToString(const bool* b) { return nsCString(*b ? "true" : "false"); } static nsCString PrefValueToString(const int* i) { return nsPrintfCString("%d", *i); } static nsCString PrefValueToString(const uint32_t* u) { return nsPrintfCString("%d", *u); } static nsCString PrefValueToString(const float* f) { return nsPrintfCString("%f", *f); } static nsCString PrefValueToString(const nsACString* s) { return nsCString(*s); } static nsCString PrefValueToString(const nsACString& s) { return nsCString(s); } // These preference getter wrappers allow us to look up the value for static // preferences based on their native types, rather than manually mapping them to // the appropriate Preferences::Get* functions. // We define these methods in a struct which is made friend of Preferences in // order to access private members. struct Internals { struct PreferenceReadMarker { static constexpr Span MarkerTypeName() { return MakeStringSpan("PreferenceRead"); } static void StreamJSONMarkerData( baseprofiler::SpliceableJSONWriter& aWriter, const ProfilerString8View& aPrefName, const Maybe& aPrefKind, PrefType aPrefType, const ProfilerString8View& aPrefValue) { aWriter.StringProperty("prefName", aPrefName); aWriter.StringProperty("prefKind", PrefValueKindToString(aPrefKind)); aWriter.StringProperty("prefType", PrefTypeToString(aPrefType)); aWriter.StringProperty("prefValue", aPrefValue); } static MarkerSchema MarkerTypeDisplay() { using MS = MarkerSchema; MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable}; schema.AddKeyLabelFormatSearchable("prefName", "Name", MS::Format::String, MS::Searchable::Searchable); schema.AddKeyLabelFormat("prefKind", "Kind", MS::Format::String); schema.AddKeyLabelFormat("prefType", "Type", MS::Format::String); schema.AddKeyLabelFormat("prefValue", "Value", MS::Format::String); schema.SetTableLabel( "{marker.data.prefName}: {marker.data.prefValue} " "({marker.data.prefType})"); return schema; } private: static Span PrefValueKindToString( const Maybe& aKind) { if (aKind) { return *aKind == PrefValueKind::Default ? MakeStringSpan("Default") : MakeStringSpan("User"); } return "Shared"; } static Span PrefTypeToString(PrefType type) { switch (type) { case PrefType::None: return "None"; case PrefType::Int: return "Int"; case PrefType::Bool: return "Bool"; case PrefType::String: return "String"; default: MOZ_ASSERT_UNREACHABLE("Unknown preference type."); return "Unknown"; } } }; template static nsresult GetPrefValue(const char* aPrefName, T&& aResult, PrefValueKind aKind) { nsresult rv = NS_ERROR_UNEXPECTED; NS_ENSURE_TRUE(Preferences::InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); if (Maybe pref = pref_Lookup(aPrefName)) { rv = pref->GetValue(aKind, std::forward(aResult)); if (profiler_feature_active(ProfilerFeature::PreferenceReads)) { profiler_add_marker( "PreferenceRead", baseprofiler::category::OTHER_PreferenceRead, {}, PreferenceReadMarker{}, ProfilerString8View::WrapNullTerminatedString(aPrefName), Some(aKind), pref->Type(), PrefValueToString(aResult)); } } return rv; } template static nsresult GetSharedPrefValue(const char* aName, T* aResult) { nsresult rv = NS_ERROR_UNEXPECTED; if (Maybe pref = pref_SharedLookup(aName)) { rv = pref->GetValue(PrefValueKind::User, aResult); if (profiler_feature_active(ProfilerFeature::PreferenceReads)) { profiler_add_marker( "PreferenceRead", baseprofiler::category::OTHER_PreferenceRead, {}, PreferenceReadMarker{}, ProfilerString8View::WrapNullTerminatedString(aName), Nothing() /* indicates Shared */, pref->Type(), PrefValueToString(aResult)); } } return rv; } template static T GetPref(const char* aPrefName, T aFallback, PrefValueKind aKind = PrefValueKind::User) { T result = aFallback; GetPrefValue(aPrefName, &result, aKind); return result; } template static void MOZ_NEVER_INLINE AssignMirror(T& aMirror, V aValue) { aMirror = aValue; } static void MOZ_NEVER_INLINE AssignMirror(DataMutexString& aMirror, nsCString&& aValue) { auto lock = aMirror.Lock(); lock->Assign(std::move(aValue)); } static void MOZ_NEVER_INLINE AssignMirror(DataMutexString& aMirror, const nsLiteralCString& aValue) { auto lock = aMirror.Lock(); lock->Assign(aValue); } static void ClearMirror(DataMutexString& aMirror) { auto lock = aMirror.Lock(); lock->Assign(nsCString()); } template static void UpdateMirror(const char* aPref, void* aMirror) { StripAtomic value; nsresult rv = GetPrefValue(aPref, &value, PrefValueKind::User); if (NS_SUCCEEDED(rv)) { AssignMirror(*static_cast(aMirror), std::forward>(value)); } else { // GetPrefValue() can fail if the update is caused by the pref being // deleted or if it fails to make a cast. This assertion is the only place // where we safeguard these. In this case the mirror variable will be // untouched, thus keeping the value it had prior to the change. // (Note that this case won't happen for a deletion via DeleteBranch() // unless bug 343600 is fixed, but it will happen for a deletion via // ClearUserPref().) NS_WARNING(nsPrintfCString("Pref changed failure: %s\n", aPref).get()); MOZ_ASSERT(false); } } template static nsresult RegisterCallback(void* aMirror, const nsACString& aPref) { return Preferences::RegisterCallback(UpdateMirror, aPref, aMirror, Preferences::ExactMatch, /* isPriority */ true); } }; // Initialize default preference JavaScript buffers from appropriate TEXT // resources. /* static */ nsresult Preferences::InitInitialObjects(bool aIsStartup) { MOZ_ASSERT(NS_IsMainThread()); if (!XRE_IsParentProcess()) { MOZ_DIAGNOSTIC_ASSERT(gSharedMap); if (aIsStartup) { StaticPrefs::StartObservingAlwaysPrefs(); } return NS_OK; } // Initialize static prefs before prefs from data files so that the latter // will override the former. StaticPrefs::InitAll(); // In the omni.jar case, we load the following prefs: // - jar:$gre/omni.jar!/greprefs.js // - jar:$gre/omni.jar!/defaults/pref/*.js // // In the non-omni.jar case, we load: // - $gre/greprefs.js // // In both cases, we also load: // - $gre/defaults/pref/*.js // // This is kept for bug 591866 (channel-prefs.js should not be in omni.jar) // in the `$app == $gre` case; we load all files instead of channel-prefs.js // only to have the same behaviour as `$app != $gre`, where this is required // as a supported location for GRE preferences. // // When `$app != $gre`, we additionally load, in the omni.jar case: // - jar:$app/omni.jar!/defaults/preferences/*.js // - $app/defaults/preferences/*.js // // and in the non-omni.jar case: // - $app/defaults/preferences/*.js // // When `$app == $gre`, we additionally load, in the omni.jar case: // - jar:$gre/omni.jar!/defaults/preferences/*.js // // Thus, in the omni.jar case, we always load app-specific default // preferences from omni.jar, whether or not `$app == $gre`. nsresult rv = NS_ERROR_FAILURE; UniquePtr find; nsTArray prefEntries; const char* entryName; uint16_t entryNameLen; RefPtr jarReader = Omnijar::GetReader(Omnijar::GRE); if (jarReader) { #ifdef MOZ_WIDGET_ANDROID // Try to load an architecture-specific greprefs.js first. This will be // present in FAT AAR builds of GeckoView on Android. const char* abi = getenv("MOZ_ANDROID_CPU_ABI"); if (abi) { nsAutoCString path; path.AppendPrintf("%s/greprefs.js", abi); rv = pref_ReadPrefFromJar(jarReader, path.get()); } if (NS_FAILED(rv)) { // Fallback to toplevel greprefs.js if arch-specific load fails. rv = pref_ReadPrefFromJar(jarReader, "greprefs.js"); } #else // Load jar:$gre/omni.jar!/greprefs.js. rv = pref_ReadPrefFromJar(jarReader, "greprefs.js"); #endif NS_ENSURE_SUCCESS(rv, rv); // Load jar:$gre/omni.jar!/defaults/pref/*.js. rv = pref_ReadDefaultPrefs(jarReader, "defaults/pref/*.js$"); NS_ENSURE_SUCCESS(rv, rv); #ifdef MOZ_BACKGROUNDTASKS if (BackgroundTasks::IsBackgroundTaskMode()) { rv = pref_ReadDefaultPrefs(jarReader, "defaults/backgroundtasks/*.js$"); NS_ENSURE_SUCCESS(rv, rv); } #endif #ifdef MOZ_WIDGET_ANDROID // Load jar:$gre/omni.jar!/defaults/pref/$MOZ_ANDROID_CPU_ABI/*.js. nsAutoCString path; path.AppendPrintf("jar:$gre/omni.jar!/defaults/pref/%s/*.js$", abi); pref_ReadDefaultPrefs(jarReader, path.get()); NS_ENSURE_SUCCESS(rv, rv); #endif } else { // Load $gre/greprefs.js. nsCOMPtr greprefsFile; rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(greprefsFile)); NS_ENSURE_SUCCESS(rv, rv); rv = greprefsFile->AppendNative("greprefs.js"_ns); NS_ENSURE_SUCCESS(rv, rv); rv = openPrefFile(greprefsFile, PrefValueKind::Default); if (NS_FAILED(rv)) { NS_WARNING( "Error parsing GRE default preferences. Is this an old-style " "embedding app?"); } } // Load $gre/defaults/pref/*.js. nsCOMPtr defaultPrefDir; rv = NS_GetSpecialDirectory(NS_APP_PREF_DEFAULTS_50_DIR, getter_AddRefs(defaultPrefDir)); NS_ENSURE_SUCCESS(rv, rv); // These pref file names should not be used: we process them after all other // application pref files for backwards compatibility. static const char* specialFiles[] = { #if defined(XP_MACOSX) "macprefs.js" #elif defined(XP_WIN) "winpref.js" #elif defined(XP_UNIX) "unix.js" # if defined(_AIX) , "aix.js" # endif #endif }; rv = pref_LoadPrefsInDir(defaultPrefDir, specialFiles, ArrayLength(specialFiles)); if (NS_FAILED(rv)) { NS_WARNING("Error parsing application default preferences."); } #if defined(MOZ_WIDGET_GTK) // Under Flatpak/Snap package, load /etc/firefox/defaults/pref/*.js. if (mozilla::widget::IsRunningUnderFlatpakOrSnap()) { nsCOMPtr defaultSnapPrefDir; rv = NS_GetSpecialDirectory(NS_OS_SYSTEM_CONFIG_DIR, getter_AddRefs(defaultSnapPrefDir)); NS_ENSURE_SUCCESS(rv, rv); defaultSnapPrefDir->AppendNative("defaults"_ns); defaultSnapPrefDir->AppendNative("pref"_ns); rv = pref_LoadPrefsInDir(defaultSnapPrefDir, nullptr, 0); if (NS_FAILED(rv)) { NS_WARNING("Error parsing application default preferences under Snap."); } } #endif // Load jar:$app/omni.jar!/defaults/preferences/*.js // or jar:$gre/omni.jar!/defaults/preferences/*.js. RefPtr appJarReader = Omnijar::GetReader(Omnijar::APP); // GetReader(Omnijar::APP) returns null when `$app == $gre`, in // which case we look for app-specific default preferences in $gre. if (!appJarReader) { appJarReader = Omnijar::GetReader(Omnijar::GRE); } if (appJarReader) { rv = appJarReader->FindInit("defaults/preferences/*.js$", getter_Transfers(find)); NS_ENSURE_SUCCESS(rv, rv); prefEntries.Clear(); while (NS_SUCCEEDED(find->FindNext(&entryName, &entryNameLen))) { prefEntries.AppendElement(Substring(entryName, entryNameLen)); } prefEntries.Sort(); for (uint32_t i = prefEntries.Length(); i--;) { rv = pref_ReadPrefFromJar(appJarReader, prefEntries[i].get()); if (NS_FAILED(rv)) { NS_WARNING("Error parsing preferences."); } } #ifdef MOZ_BACKGROUNDTASKS if (BackgroundTasks::IsBackgroundTaskMode()) { rv = appJarReader->FindInit("defaults/backgroundtasks/*.js$", getter_Transfers(find)); NS_ENSURE_SUCCESS(rv, rv); prefEntries.Clear(); while (NS_SUCCEEDED(find->FindNext(&entryName, &entryNameLen))) { prefEntries.AppendElement(Substring(entryName, entryNameLen)); } prefEntries.Sort(); for (uint32_t i = prefEntries.Length(); i--;) { rv = pref_ReadPrefFromJar(appJarReader, prefEntries[i].get()); if (NS_FAILED(rv)) { NS_WARNING("Error parsing preferences."); } } } #endif } nsCOMPtr dirSvc( do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr list; dirSvc->Get(NS_APP_PREFS_DEFAULTS_DIR_LIST, NS_GET_IID(nsISimpleEnumerator), getter_AddRefs(list)); if (list) { bool hasMore; while (NS_SUCCEEDED(list->HasMoreElements(&hasMore)) && hasMore) { nsCOMPtr elem; list->GetNext(getter_AddRefs(elem)); if (!elem) { continue; } nsCOMPtr path = do_QueryInterface(elem); if (!path) { continue; } // Do we care if a file provided by this process fails to load? pref_LoadPrefsInDir(path, nullptr, 0); } } if (XRE_IsParentProcess()) { SetupTelemetryPref(); } if (aIsStartup) { // Now that all prefs have their initial values, install the callbacks for // `always`-mirrored static prefs. We do this now rather than in // StaticPrefs::InitAll() so that the callbacks don't need to be traversed // while we load prefs from data files. StaticPrefs::StartObservingAlwaysPrefs(); } NS_CreateServicesFromCategory(NS_PREFSERVICE_APPDEFAULTS_TOPIC_ID, nullptr, NS_PREFSERVICE_APPDEFAULTS_TOPIC_ID); nsCOMPtr observerService = services::GetObserverService(); NS_ENSURE_SUCCESS(rv, rv); observerService->NotifyObservers(nullptr, NS_PREFSERVICE_APPDEFAULTS_TOPIC_ID, nullptr); return NS_OK; } /* static */ nsresult Preferences::GetBool(const char* aPrefName, bool* aResult, PrefValueKind aKind) { MOZ_ASSERT(aResult); return Internals::GetPrefValue(aPrefName, aResult, aKind); } /* static */ nsresult Preferences::GetInt(const char* aPrefName, int32_t* aResult, PrefValueKind aKind) { MOZ_ASSERT(aResult); return Internals::GetPrefValue(aPrefName, aResult, aKind); } /* static */ nsresult Preferences::GetFloat(const char* aPrefName, float* aResult, PrefValueKind aKind) { MOZ_ASSERT(aResult); return Internals::GetPrefValue(aPrefName, aResult, aKind); } /* static */ nsresult Preferences::GetCString(const char* aPrefName, nsACString& aResult, PrefValueKind aKind) { aResult.SetIsVoid(true); return Internals::GetPrefValue(aPrefName, aResult, aKind); } /* static */ nsresult Preferences::GetString(const char* aPrefName, nsAString& aResult, PrefValueKind aKind) { nsAutoCString result; nsresult rv = Preferences::GetCString(aPrefName, result, aKind); if (NS_SUCCEEDED(rv)) { CopyUTF8toUTF16(result, aResult); } return rv; } /* static */ nsresult Preferences::GetLocalizedCString(const char* aPrefName, nsACString& aResult, PrefValueKind aKind) { nsAutoString result; nsresult rv = GetLocalizedString(aPrefName, result, aKind); if (NS_SUCCEEDED(rv)) { CopyUTF16toUTF8(result, aResult); } return rv; } /* static */ nsresult Preferences::GetLocalizedString(const char* aPrefName, nsAString& aResult, PrefValueKind aKind) { NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); nsCOMPtr prefLocalString; nsresult rv = GetRootBranch(aKind)->GetComplexValue( aPrefName, NS_GET_IID(nsIPrefLocalizedString), getter_AddRefs(prefLocalString)); if (NS_SUCCEEDED(rv)) { MOZ_ASSERT(prefLocalString, "Succeeded but the result is NULL"); prefLocalString->GetData(aResult); } return rv; } /* static */ nsresult Preferences::GetComplex(const char* aPrefName, const nsIID& aType, void** aResult, PrefValueKind aKind) { NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); return GetRootBranch(aKind)->GetComplexValue(aPrefName, aType, aResult); } /* static */ bool Preferences::GetBool(const char* aPrefName, bool aFallback, PrefValueKind aKind) { return Internals::GetPref(aPrefName, aFallback, aKind); } /* static */ int32_t Preferences::GetInt(const char* aPrefName, int32_t aFallback, PrefValueKind aKind) { return Internals::GetPref(aPrefName, aFallback, aKind); } /* static */ uint32_t Preferences::GetUint(const char* aPrefName, uint32_t aFallback, PrefValueKind aKind) { return Internals::GetPref(aPrefName, aFallback, aKind); } /* static */ float Preferences::GetFloat(const char* aPrefName, float aFallback, PrefValueKind aKind) { return Internals::GetPref(aPrefName, aFallback, aKind); } /* static */ nsresult Preferences::SetCString(const char* aPrefName, const nsACString& aValue, PrefValueKind aKind) { ENSURE_PARENT_PROCESS("SetCString", aPrefName); NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); if (aValue.Length() > MAX_PREF_LENGTH) { return NS_ERROR_ILLEGAL_VALUE; } // It's ok to stash a pointer to the temporary PromiseFlatCString's chars in // pref because pref_SetPref() duplicates those chars. PrefValue prefValue; const nsCString& flat = PromiseFlatCString(aValue); prefValue.mStringVal = flat.get(); return pref_SetPref(nsDependentCString(aPrefName), PrefType::String, aKind, prefValue, /* isSticky */ false, /* isLocked */ false, /* fromInit */ false); } /* static */ nsresult Preferences::SetBool(const char* aPrefName, bool aValue, PrefValueKind aKind) { ENSURE_PARENT_PROCESS("SetBool", aPrefName); NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); PrefValue prefValue; prefValue.mBoolVal = aValue; return pref_SetPref(nsDependentCString(aPrefName), PrefType::Bool, aKind, prefValue, /* isSticky */ false, /* isLocked */ false, /* fromInit */ false); } /* static */ nsresult Preferences::SetInt(const char* aPrefName, int32_t aValue, PrefValueKind aKind) { ENSURE_PARENT_PROCESS("SetInt", aPrefName); NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); PrefValue prefValue; prefValue.mIntVal = aValue; return pref_SetPref(nsDependentCString(aPrefName), PrefType::Int, aKind, prefValue, /* isSticky */ false, /* isLocked */ false, /* fromInit */ false); } /* static */ nsresult Preferences::SetComplex(const char* aPrefName, const nsIID& aType, nsISupports* aValue, PrefValueKind aKind) { NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); return GetRootBranch(aKind)->SetComplexValue(aPrefName, aType, aValue); } /* static */ nsresult Preferences::Lock(const char* aPrefName) { ENSURE_PARENT_PROCESS("Lock", aPrefName); NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); const auto& prefName = nsDependentCString(aPrefName); Pref* pref; MOZ_TRY_VAR(pref, pref_LookupForModify(prefName, [](const PrefWrapper& aPref) { return !aPref.IsLocked(); })); if (pref) { pref->SetIsLocked(true); NotifyCallbacks(prefName, PrefWrapper(pref)); } return NS_OK; } /* static */ nsresult Preferences::Unlock(const char* aPrefName) { ENSURE_PARENT_PROCESS("Unlock", aPrefName); NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); const auto& prefName = nsDependentCString(aPrefName); Pref* pref; MOZ_TRY_VAR(pref, pref_LookupForModify(prefName, [](const PrefWrapper& aPref) { return aPref.IsLocked(); })); if (pref) { pref->SetIsLocked(false); NotifyCallbacks(prefName, PrefWrapper(pref)); } return NS_OK; } /* static */ bool Preferences::IsLocked(const char* aPrefName) { NS_ENSURE_TRUE(InitStaticMembers(), false); Maybe pref = pref_Lookup(aPrefName); return pref.isSome() && pref->IsLocked(); } /* static */ bool Preferences::IsSanitized(const char* aPrefName) { NS_ENSURE_TRUE(InitStaticMembers(), false); Maybe pref = pref_Lookup(aPrefName); return pref.isSome() && pref->IsSanitized(); } /* static */ nsresult Preferences::ClearUser(const char* aPrefName) { ENSURE_PARENT_PROCESS("ClearUser", aPrefName); NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); const auto& prefName = nsDependentCString{aPrefName}; auto result = pref_LookupForModify( prefName, [](const PrefWrapper& aPref) { return aPref.HasUserValue(); }); if (result.isErr()) { return NS_OK; } if (Pref* pref = result.unwrap()) { pref->ClearUserValue(); if (!pref->HasDefaultValue()) { MOZ_ASSERT( !gSharedMap || !pref->IsSanitized() || !gSharedMap->Has(pref->Name()), "A sanitized pref should never be in the shared pref map."); if (!pref->IsSanitized() && (!gSharedMap || !gSharedMap->Has(pref->Name()))) { HashTable()->remove(aPrefName); } else { pref->SetType(PrefType::None); } NotifyCallbacks(prefName); } else { NotifyCallbacks(prefName, PrefWrapper(pref)); } Preferences::HandleDirty(); } return NS_OK; } /* static */ bool Preferences::HasUserValue(const char* aPrefName) { NS_ENSURE_TRUE(InitStaticMembers(), false); Maybe pref = pref_Lookup(aPrefName); return pref.isSome() && pref->HasUserValue(); } /* static */ bool Preferences::HasDefaultValue(const char* aPrefName) { NS_ENSURE_TRUE(InitStaticMembers(), false); Maybe pref = pref_Lookup(aPrefName); return pref.isSome() && pref->HasDefaultValue(); } /* static */ int32_t Preferences::GetType(const char* aPrefName) { NS_ENSURE_TRUE(InitStaticMembers(), nsIPrefBranch::PREF_INVALID); if (!HashTable()) { return PREF_INVALID; } Maybe pref = pref_Lookup(aPrefName); if (!pref.isSome()) { return PREF_INVALID; } switch (pref->Type()) { case PrefType::String: return PREF_STRING; case PrefType::Int: return PREF_INT; case PrefType::Bool: return PREF_BOOL; case PrefType::None: if (IsPreferenceSanitized(aPrefName)) { if (!sPrefTelemetryEventEnabled.exchange(true)) { sPrefTelemetryEventEnabled = true; Telemetry::SetEventRecordingEnabled("security"_ns, true); } Telemetry::RecordEvent( Telemetry::EventID::Security_Prefusage_Contentprocess, mozilla::Some(aPrefName), mozilla::Nothing()); if (sCrashOnBlocklistedPref) { MOZ_CRASH_UNSAFE_PRINTF( "Should not access the preference '%s' in the Content Processes", aPrefName); } else { return PREF_INVALID; } } [[fallthrough]]; default: MOZ_CRASH(); } } /* static */ nsresult Preferences::AddStrongObserver(nsIObserver* aObserver, const nsACString& aPref) { MOZ_ASSERT(aObserver); NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); return sPreferences->mRootBranch->AddObserver(aPref, aObserver, false); } /* static */ nsresult Preferences::AddWeakObserver(nsIObserver* aObserver, const nsACString& aPref) { MOZ_ASSERT(aObserver); NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); return sPreferences->mRootBranch->AddObserver(aPref, aObserver, true); } /* static */ nsresult Preferences::RemoveObserver(nsIObserver* aObserver, const nsACString& aPref) { MOZ_ASSERT(aObserver); if (sShutdown) { MOZ_ASSERT(!sPreferences); return NS_OK; // Observers have been released automatically. } NS_ENSURE_TRUE(sPreferences, NS_ERROR_NOT_AVAILABLE); return sPreferences->mRootBranch->RemoveObserver(aPref, aObserver); } template static void AssertNotMallocAllocated(T* aPtr) { #if defined(DEBUG) && defined(MOZ_MEMORY) jemalloc_ptr_info_t info; jemalloc_ptr_info((void*)aPtr, &info); MOZ_ASSERT(info.tag == TagUnknown); #endif } /* static */ nsresult Preferences::AddStrongObservers(nsIObserver* aObserver, const char** aPrefs) { MOZ_ASSERT(aObserver); for (uint32_t i = 0; aPrefs[i]; i++) { AssertNotMallocAllocated(aPrefs[i]); nsCString pref; pref.AssignLiteral(aPrefs[i], strlen(aPrefs[i])); nsresult rv = AddStrongObserver(aObserver, pref); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } /* static */ nsresult Preferences::AddWeakObservers(nsIObserver* aObserver, const char** aPrefs) { MOZ_ASSERT(aObserver); for (uint32_t i = 0; aPrefs[i]; i++) { AssertNotMallocAllocated(aPrefs[i]); nsCString pref; pref.AssignLiteral(aPrefs[i], strlen(aPrefs[i])); nsresult rv = AddWeakObserver(aObserver, pref); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } /* static */ nsresult Preferences::RemoveObservers(nsIObserver* aObserver, const char** aPrefs) { MOZ_ASSERT(aObserver); if (sShutdown) { MOZ_ASSERT(!sPreferences); return NS_OK; // Observers have been released automatically. } NS_ENSURE_TRUE(sPreferences, NS_ERROR_NOT_AVAILABLE); for (uint32_t i = 0; aPrefs[i]; i++) { nsresult rv = RemoveObserver(aObserver, nsDependentCString(aPrefs[i])); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } template /* static */ nsresult Preferences::RegisterCallbackImpl(PrefChangedFunc aCallback, T& aPrefNode, void* aData, MatchKind aMatchKind, bool aIsPriority) { NS_ENSURE_ARG(aCallback); NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); auto node = new CallbackNode(aPrefNode, aCallback, aData, aMatchKind); if (aIsPriority) { // Add to the start of the list. node->SetNext(gFirstCallback); gFirstCallback = node; if (!gLastPriorityNode) { gLastPriorityNode = node; } } else { // Add to the start of the non-priority part of the list. if (gLastPriorityNode) { node->SetNext(gLastPriorityNode->Next()); gLastPriorityNode->SetNext(node); } else { node->SetNext(gFirstCallback); gFirstCallback = node; } } return NS_OK; } /* static */ nsresult Preferences::RegisterCallback(PrefChangedFunc aCallback, const nsACString& aPrefNode, void* aData, MatchKind aMatchKind, bool aIsPriority) { return RegisterCallbackImpl(aCallback, aPrefNode, aData, aMatchKind, aIsPriority); } /* static */ nsresult Preferences::RegisterCallbacks(PrefChangedFunc aCallback, const char** aPrefs, void* aData, MatchKind aMatchKind) { return RegisterCallbackImpl(aCallback, aPrefs, aData, aMatchKind); } /* static */ nsresult Preferences::RegisterCallbackAndCall(PrefChangedFunc aCallback, const nsACString& aPref, void* aClosure, MatchKind aMatchKind) { MOZ_ASSERT(aCallback); nsresult rv = RegisterCallback(aCallback, aPref, aClosure, aMatchKind); if (NS_SUCCEEDED(rv)) { (*aCallback)(PromiseFlatCString(aPref).get(), aClosure); } return rv; } /* static */ nsresult Preferences::RegisterCallbacksAndCall(PrefChangedFunc aCallback, const char** aPrefs, void* aClosure) { MOZ_ASSERT(aCallback); nsresult rv = RegisterCallbacks(aCallback, aPrefs, aClosure, MatchKind::ExactMatch); if (NS_SUCCEEDED(rv)) { for (const char** ptr = aPrefs; *ptr; ptr++) { (*aCallback)(*ptr, aClosure); } } return rv; } template /* static */ nsresult Preferences::UnregisterCallbackImpl(PrefChangedFunc aCallback, T& aPrefNode, void* aData, MatchKind aMatchKind) { MOZ_ASSERT(aCallback); if (sShutdown) { MOZ_ASSERT(!sPreferences); return NS_OK; // Observers have been released automatically. } NS_ENSURE_TRUE(sPreferences, NS_ERROR_NOT_AVAILABLE); nsresult rv = NS_ERROR_FAILURE; CallbackNode* node = gFirstCallback; CallbackNode* prev_node = nullptr; while (node) { if (node->Func() == aCallback && node->Data() == aData && node->MatchKind() == aMatchKind && node->DomainIs(aPrefNode)) { if (gCallbacksInProgress) { // Postpone the node removal until after callbacks enumeration is // finished. node->ClearFunc(); gShouldCleanupDeadNodes = true; prev_node = node; node = node->Next(); } else { node = pref_RemoveCallbackNode(node, prev_node); } rv = NS_OK; } else { prev_node = node; node = node->Next(); } } return rv; } /* static */ nsresult Preferences::UnregisterCallback(PrefChangedFunc aCallback, const nsACString& aPrefNode, void* aData, MatchKind aMatchKind) { return UnregisterCallbackImpl(aCallback, aPrefNode, aData, aMatchKind); } /* static */ nsresult Preferences::UnregisterCallbacks(PrefChangedFunc aCallback, const char** aPrefs, void* aData, MatchKind aMatchKind) { return UnregisterCallbackImpl(aCallback, aPrefs, aData, aMatchKind); } template static void AddMirrorCallback(T* aMirror, const nsACString& aPref) { MOZ_ASSERT(NS_IsMainThread()); Internals::RegisterCallback(aMirror, aPref); } // Don't inline because it explodes compile times. template static MOZ_NEVER_INLINE void AddMirror(T* aMirror, const nsACString& aPref, StripAtomic aDefault) { *aMirror = Internals::GetPref(PromiseFlatCString(aPref).get(), aDefault); AddMirrorCallback(aMirror, aPref); } static MOZ_NEVER_INLINE void AddMirror(DataMutexString& aMirror, const nsACString& aPref) { auto lock = aMirror.Lock(); nsCString result(*lock); Internals::GetPrefValue(PromiseFlatCString(aPref).get(), result, PrefValueKind::User); lock->Assign(std::move(result)); AddMirrorCallback(&aMirror, aPref); } // The InitPref_*() functions below end in a `_` suffix because they are // used by the PREF macro definition in InitAll() below. static void InitPref_bool(const nsCString& aName, bool aDefaultValue) { MOZ_ASSERT(XRE_IsParentProcess()); PrefValue value; value.mBoolVal = aDefaultValue; pref_SetPref(aName, PrefType::Bool, PrefValueKind::Default, value, /* isSticky */ false, /* isLocked */ false, /* fromInit */ true); } static void InitPref_int32_t(const nsCString& aName, int32_t aDefaultValue) { MOZ_ASSERT(XRE_IsParentProcess()); PrefValue value; value.mIntVal = aDefaultValue; pref_SetPref(aName, PrefType::Int, PrefValueKind::Default, value, /* isSticky */ false, /* isLocked */ false, /* fromInit */ true); } static void InitPref_uint32_t(const nsCString& aName, uint32_t aDefaultValue) { InitPref_int32_t(aName, int32_t(aDefaultValue)); } static void InitPref_float(const nsCString& aName, float aDefaultValue) { MOZ_ASSERT(XRE_IsParentProcess()); PrefValue value; // Convert the value in a locale-independent way, including a trailing ".0" // if necessary to distinguish floating-point from integer prefs when viewing // them in about:config. nsAutoCString defaultValue; defaultValue.AppendFloat(aDefaultValue); if (!defaultValue.Contains('.') && !defaultValue.Contains('e')) { defaultValue.AppendLiteral(".0"); } value.mStringVal = defaultValue.get(); pref_SetPref(aName, PrefType::String, PrefValueKind::Default, value, /* isSticky */ false, /* isLocked */ false, /* fromInit */ true); } static void InitPref_String(const nsCString& aName, const char* aDefaultValue) { MOZ_ASSERT(XRE_IsParentProcess()); PrefValue value; value.mStringVal = aDefaultValue; pref_SetPref(aName, PrefType::String, PrefValueKind::Default, value, /* isSticky */ false, /* isLocked */ false, /* fromInit */ true); } static void InitPref(const nsCString& aName, bool aDefaultValue) { InitPref_bool(aName, aDefaultValue); } static void InitPref(const nsCString& aName, int32_t aDefaultValue) { InitPref_int32_t(aName, aDefaultValue); } static void InitPref(const nsCString& aName, uint32_t aDefaultValue) { InitPref_uint32_t(aName, aDefaultValue); } static void InitPref(const nsCString& aName, float aDefaultValue) { InitPref_float(aName, aDefaultValue); } template static void InitAlwaysPref(const nsCString& aName, T* aCache, StripAtomic aDefaultValue) { // Only called in the parent process. Set/reset the pref value and the // `always` mirror to the default value. // `once` mirrors will be initialized lazily in InitOncePrefs(). InitPref(aName, aDefaultValue); *aCache = aDefaultValue; } static void InitAlwaysPref(const nsCString& aName, DataMutexString& aCache, const nsLiteralCString& aDefaultValue) { // Only called in the parent process. Set/reset the pref value and the // `always` mirror to the default value. // `once` mirrors will be initialized lazily in InitOncePrefs(). InitPref_String(aName, aDefaultValue.get()); Internals::AssignMirror(aCache, aDefaultValue); } static Atomic sOncePrefRead(false); static StaticMutex sOncePrefMutex MOZ_UNANNOTATED; namespace StaticPrefs { void MaybeInitOncePrefs() { if (MOZ_LIKELY(sOncePrefRead)) { // `once`-mirrored prefs have already been initialized to their default // value. return; } StaticMutexAutoLock lock(sOncePrefMutex); if (NS_IsMainThread()) { InitOncePrefs(); } else { RefPtr runnable = NS_NewRunnableFunction( "Preferences::MaybeInitOncePrefs", [&]() { InitOncePrefs(); }); // This logic needs to run on the main thread SyncRunnable::DispatchToThread(GetMainThreadSerialEventTarget(), runnable); } sOncePrefRead = true; } // For mirrored prefs we generate a variable definition. #define NEVER_PREF(name, cpp_type, value) #define ALWAYS_PREF(name, base_id, full_id, cpp_type, default_value) \ cpp_type sMirror_##full_id(default_value); #define ALWAYS_DATAMUTEX_PREF(name, base_id, full_id, cpp_type, default_value) \ cpp_type sMirror_##full_id("DataMutexString"); #define ONCE_PREF(name, base_id, full_id, cpp_type, default_value) \ cpp_type sMirror_##full_id(default_value); #include "mozilla/StaticPrefListAll.h" #undef NEVER_PREF #undef ALWAYS_PREF #undef ALWAYS_DATAMUTEX_PREF #undef ONCE_PREF static void InitAll() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(XRE_IsParentProcess()); // For all prefs we generate some initialization code. // // The InitPref_*() functions have a type suffix to avoid ambiguity between // prefs having int32_t and float default values. That suffix is not needed // for the InitAlwaysPref() functions because they take a pointer parameter, // which prevents automatic int-to-float coercion. #define NEVER_PREF(name, cpp_type, value) \ InitPref_##cpp_type(name ""_ns, value); #define ALWAYS_PREF(name, base_id, full_id, cpp_type, value) \ InitAlwaysPref(name ""_ns, &sMirror_##full_id, value); #define ALWAYS_DATAMUTEX_PREF(name, base_id, full_id, cpp_type, value) \ InitAlwaysPref(name ""_ns, sMirror_##full_id, value); #define ONCE_PREF(name, base_id, full_id, cpp_type, value) \ InitPref_##cpp_type(name ""_ns, value); #include "mozilla/StaticPrefListAll.h" #undef NEVER_PREF #undef ALWAYS_PREF #undef ALWAYS_DATAMUTEX_PREF #undef ONCE_PREF } static void StartObservingAlwaysPrefs() { MOZ_ASSERT(NS_IsMainThread()); // Call AddMirror so that our mirrors for `always` prefs will stay updated. // The call to AddMirror re-reads the current pref value into the mirror, so // our mirror will now be up-to-date even if some of the prefs have changed // since the call to InitAll(). #define NEVER_PREF(name, cpp_type, value) #define ALWAYS_PREF(name, base_id, full_id, cpp_type, value) \ AddMirror(&sMirror_##full_id, name ""_ns, sMirror_##full_id); #define ALWAYS_DATAMUTEX_PREF(name, base_id, full_id, cpp_type, value) \ AddMirror(sMirror_##full_id, name ""_ns); #define ONCE_PREF(name, base_id, full_id, cpp_type, value) #include "mozilla/StaticPrefListAll.h" #undef NEVER_PREF #undef ALWAYS_PREF #undef ALWAYS_DATAMUTEX_PREF #undef ONCE_PREF } static void InitOncePrefs() { // For `once`-mirrored prefs we generate some initialization code. This is // done in case the pref value was updated when reading pref data files. It's // necessary because we don't have callbacks registered for `once`-mirrored // prefs. // // In debug builds, we also install a mechanism that can check if the // preference value is modified after `once`-mirrored prefs are initialized. // In tests this would indicate a likely misuse of a `once`-mirrored pref and // suggest that it should instead be `always`-mirrored. #define NEVER_PREF(name, cpp_type, value) #define ALWAYS_PREF(name, base_id, full_id, cpp_type, value) #define ALWAYS_DATAMUTEX_PREF(name, base_id, full_id, cpp_type, value) #ifdef DEBUG # define ONCE_PREF(name, base_id, full_id, cpp_type, value) \ { \ MOZ_ASSERT(gOnceStaticPrefsAntiFootgun); \ sMirror_##full_id = Internals::GetPref(name, cpp_type(value)); \ auto checkPref = [&]() { \ MOZ_ASSERT(sOncePrefRead); \ cpp_type staticPrefValue = full_id(); \ cpp_type preferenceValue = \ Internals::GetPref(GetPrefName_##base_id(), cpp_type(value)); \ MOZ_ASSERT(staticPrefValue == preferenceValue, \ "Preference '" name \ "' got modified since StaticPrefs::" #full_id \ " was initialized. Consider using an `always` mirror kind " \ "instead"); \ }; \ gOnceStaticPrefsAntiFootgun->insert( \ std::pair(GetPrefName_##base_id(), \ std::move(checkPref))); \ } #else # define ONCE_PREF(name, base_id, full_id, cpp_type, value) \ sMirror_##full_id = Internals::GetPref(name, cpp_type(value)); #endif #include "mozilla/StaticPrefListAll.h" #undef NEVER_PREF #undef ALWAYS_PREF #undef ALWAYS_DATAMUTEX_PREF #undef ONCE_PREF } static void ShutdownAlwaysPrefs() { MOZ_ASSERT(NS_IsMainThread()); // We may need to do clean up for leak detection for some StaticPrefs. #define NEVER_PREF(name, cpp_type, value) #define ALWAYS_PREF(name, base_id, full_id, cpp_type, value) #define ALWAYS_DATAMUTEX_PREF(name, base_id, full_id, cpp_type, value) \ Internals::ClearMirror(sMirror_##full_id); #define ONCE_PREF(name, base_id, full_id, cpp_type, value) #include "mozilla/StaticPrefListAll.h" #undef NEVER_PREF #undef ALWAYS_PREF #undef ALWAYS_DATAMUTEX_PREF #undef ONCE_PREF } } // namespace StaticPrefs static MOZ_MAYBE_UNUSED void SaveOncePrefToSharedMap( SharedPrefMapBuilder& aBuilder, const nsACString& aName, bool aValue) { auto oncePref = MakeUnique(aName); oncePref->SetType(PrefType::Bool); oncePref->SetIsSkippedByIteration(true); bool valueChanged = false; MOZ_ALWAYS_SUCCEEDS( oncePref->SetDefaultValue(PrefType::Bool, PrefValue(aValue), /* isSticky */ true, /* isLocked */ true, &valueChanged)); oncePref->AddToMap(aBuilder); } static MOZ_MAYBE_UNUSED void SaveOncePrefToSharedMap( SharedPrefMapBuilder& aBuilder, const nsACString& aName, int32_t aValue) { auto oncePref = MakeUnique(aName); oncePref->SetType(PrefType::Int); oncePref->SetIsSkippedByIteration(true); bool valueChanged = false; MOZ_ALWAYS_SUCCEEDS( oncePref->SetDefaultValue(PrefType::Int, PrefValue(aValue), /* isSticky */ true, /* isLocked */ true, &valueChanged)); oncePref->AddToMap(aBuilder); } static MOZ_MAYBE_UNUSED void SaveOncePrefToSharedMap( SharedPrefMapBuilder& aBuilder, const nsACString& aName, uint32_t aValue) { SaveOncePrefToSharedMap(aBuilder, aName, int32_t(aValue)); } static MOZ_MAYBE_UNUSED void SaveOncePrefToSharedMap( SharedPrefMapBuilder& aBuilder, const nsACString& aName, float aValue) { auto oncePref = MakeUnique(aName); oncePref->SetType(PrefType::String); oncePref->SetIsSkippedByIteration(true); nsAutoCString value; value.AppendFloat(aValue); bool valueChanged = false; // It's ok to stash a pointer to the temporary PromiseFlatCString's chars in // pref because pref_SetPref() duplicates those chars. const nsCString& flat = PromiseFlatCString(value); MOZ_ALWAYS_SUCCEEDS( oncePref->SetDefaultValue(PrefType::String, PrefValue(flat.get()), /* isSticky */ true, /* isLocked */ true, &valueChanged)); oncePref->AddToMap(aBuilder); } #define ONCE_PREF_NAME(name) "$$$" name "$$$" namespace StaticPrefs { static void RegisterOncePrefs(SharedPrefMapBuilder& aBuilder) { MOZ_ASSERT(XRE_IsParentProcess()); MOZ_DIAGNOSTIC_ASSERT(!gSharedMap, "Must be called before gSharedMap has been created"); MaybeInitOncePrefs(); // For `once`-mirrored prefs we generate a save call, which saves the value // as it was at parent startup. It is stored in a special (hidden and locked) // entry in the global SharedPreferenceMap. In order for the entry to be // hidden and not appear in about:config nor ever be stored to disk, we set // its IsSkippedByIteration flag to true. We also distinguish it by adding a // "$$$" prefix and suffix to the preference name. #define NEVER_PREF(name, cpp_type, value) #define ALWAYS_PREF(name, base_id, full_id, cpp_type, value) #define ALWAYS_DATAMUTEX_PREF(name, base_id, full_id, cpp_type, value) #define ONCE_PREF(name, base_id, full_id, cpp_type, value) \ SaveOncePrefToSharedMap(aBuilder, ONCE_PREF_NAME(name) ""_ns, \ cpp_type(sMirror_##full_id)); #include "mozilla/StaticPrefListAll.h" #undef NEVER_PREF #undef ALWAYS_PREF #undef ALWAYS_DATAMUTEX_PREF #undef ONCE_PREF } static void InitStaticPrefsFromShared() { MOZ_ASSERT(!XRE_IsParentProcess()); MOZ_DIAGNOSTIC_ASSERT(gSharedMap, "Must be called once gSharedMap has been created"); // For mirrored static prefs we generate some initialization code. Each // mirror variable is already initialized in the binary with the default // value. If the pref value hasn't changed from the default in the main // process (the common case) then the overwriting here won't change the // mirror variable's value. // // Note that the MOZ_ASSERT calls below can fail in one obscure case: when a // Firefox update occurs and we get a main process from the old binary (with // static prefs {A,B,C,D}) plus a new content process from the new binary // (with static prefs {A,B,C,D,E}). The content process' call to // GetSharedPrefValue() for pref E will fail because the shared pref map was // created by the main process, which doesn't have pref E. // // This silent failure is safe. The mirror variable for pref E is already // initialized to the default value in the content process, and the main // process cannot have changed pref E because it doesn't know about it! // // Nonetheless, it's useful to have the MOZ_ASSERT here for testing of debug // builds, where this scenario involving inconsistent binaries should not // occur. #define NEVER_PREF(name, cpp_type, default_value) #define ALWAYS_PREF(name, base_id, full_id, cpp_type, default_value) \ { \ StripAtomic val; \ if (IsString::value && IsPreferenceSanitized(name)) { \ if (!sPrefTelemetryEventEnabled.exchange(true)) { \ sPrefTelemetryEventEnabled = true; \ Telemetry::SetEventRecordingEnabled("security"_ns, true); \ } \ Telemetry::RecordEvent( \ Telemetry::EventID::Security_Prefusage_Contentprocess, \ mozilla::Some(name##_ns), mozilla::Nothing()); \ MOZ_DIAGNOSTIC_ASSERT(!sCrashOnBlocklistedPref, \ "Should not access the preference '" name \ "' in Content Processes"); \ } \ DebugOnly rv = Internals::GetSharedPrefValue(name, &val); \ MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed accessing " name); \ StaticPrefs::sMirror_##full_id = val; \ } #define ALWAYS_DATAMUTEX_PREF(name, base_id, full_id, cpp_type, default_value) \ { \ StripAtomic val; \ if (IsString::value && IsPreferenceSanitized(name)) { \ if (!sPrefTelemetryEventEnabled.exchange(true)) { \ sPrefTelemetryEventEnabled = true; \ Telemetry::SetEventRecordingEnabled("security"_ns, true); \ } \ Telemetry::RecordEvent( \ Telemetry::EventID::Security_Prefusage_Contentprocess, \ mozilla::Some(name##_ns), mozilla::Nothing()); \ MOZ_DIAGNOSTIC_ASSERT(!sCrashOnBlocklistedPref, \ "Should not access the preference '" name \ "' in Content Processes"); \ } \ DebugOnly rv = Internals::GetSharedPrefValue(name, &val); \ MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed accessing " name); \ Internals::AssignMirror(StaticPrefs::sMirror_##full_id, \ std::forward>(val)); \ } #define ONCE_PREF(name, base_id, full_id, cpp_type, default_value) \ { \ cpp_type val; \ if (IsString::value && IsPreferenceSanitized(name)) { \ if (!sPrefTelemetryEventEnabled.exchange(true)) { \ sPrefTelemetryEventEnabled = true; \ Telemetry::SetEventRecordingEnabled("security"_ns, true); \ } \ Telemetry::RecordEvent( \ Telemetry::EventID::Security_Prefusage_Contentprocess, \ mozilla::Some(name##_ns), mozilla::Nothing()); \ MOZ_DIAGNOSTIC_ASSERT(!sCrashOnBlocklistedPref, \ "Should not access the preference '" name \ "' in Content Processes"); \ } \ DebugOnly rv = \ Internals::GetSharedPrefValue(ONCE_PREF_NAME(name), &val); \ MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed accessing " name); \ StaticPrefs::sMirror_##full_id = val; \ } #include "mozilla/StaticPrefListAll.h" #undef NEVER_PREF #undef ALWAYS_PREF #undef ALWAYS_DATAMUTEX_PREF #undef ONCE_PREF // `once`-mirrored prefs have been set to their value in the step above and // outside the parent process they are immutable. We set sOncePrefRead so // that we can directly skip any lazy initializations. sOncePrefRead = true; } } // namespace StaticPrefs } // namespace mozilla #undef ENSURE_PARENT_PROCESS //=========================================================================== // Module and factory stuff //=========================================================================== NS_IMPL_COMPONENT_FACTORY(nsPrefLocalizedString) { auto str = MakeRefPtr(); if (NS_SUCCEEDED(str->Init())) { return str.forget().downcast(); } return nullptr; } namespace mozilla { void UnloadPrefsModule() { Preferences::Shutdown(); } } // namespace mozilla // Preference Sanitization Related Code --------------------------------------- #define PREF_LIST_ENTRY(s) \ { s, (sizeof(s) / sizeof(char)) - 1 } struct PrefListEntry { const char* mPrefBranch; size_t mLen; }; // These prefs are not useful in child processes. static const PrefListEntry sParentOnlyPrefBranchList[] = { // Remove prefs with user data PREF_LIST_ENTRY("datareporting.policy."), PREF_LIST_ENTRY("browser.download.lastDir"), PREF_LIST_ENTRY("browser.newtabpage.pinned"), PREF_LIST_ENTRY("browser.uiCustomization.state"), PREF_LIST_ENTRY("browser.urlbar"), PREF_LIST_ENTRY("browser.urlbar.resultGroups"), PREF_LIST_ENTRY("devtools.debugger.pending-selected-location"), PREF_LIST_ENTRY("identity.fxaccounts.account.device.name"), PREF_LIST_ENTRY("identity.fxaccounts.account.telemetry.sanitized_uid"), PREF_LIST_ENTRY("identity.fxaccounts.lastSignedInUserHash"), PREF_LIST_ENTRY("print_printer"), PREF_LIST_ENTRY("services."), // Remove UUIDs PREF_LIST_ENTRY("app.normandy.user_id"), PREF_LIST_ENTRY("browser.newtabpage.activity-stream.impressionId"), PREF_LIST_ENTRY("browser.pageActions.persistedActions"), PREF_LIST_ENTRY("browser.startup.lastColdStartupCheck"), PREF_LIST_ENTRY("dom.push.userAgentID"), PREF_LIST_ENTRY("extensions.webextensions.uuids"), PREF_LIST_ENTRY("privacy.userContext.extension"), PREF_LIST_ENTRY("toolkit.telemetry.cachedClientID"), // Remove IDs that could be used to correlate across origins PREF_LIST_ENTRY("app.update.lastUpdateTime."), PREF_LIST_ENTRY( "browser.contentblocking.cfr-milestone.milestone-shown-time"), PREF_LIST_ENTRY("browser.contextual-services.contextId"), PREF_LIST_ENTRY("browser.laterrun.bookkeeping.profileCreationTime"), PREF_LIST_ENTRY("browser.newtabpage.activity-stream.discoverystream."), PREF_LIST_ENTRY("browser.sessionstore.upgradeBackup.latestBuildID"), PREF_LIST_ENTRY("browser.shell.mostRecentDateSetAsDefault"), PREF_LIST_ENTRY("fission.experiment.max-origins.last-"), PREF_LIST_ENTRY("idle.lastDailyNotification"), PREF_LIST_ENTRY("media.gmp-gmpopenh264.lastUpdate"), PREF_LIST_ENTRY("media.gmp-manager.lastCheck"), PREF_LIST_ENTRY("places.database.lastMaintenance"), PREF_LIST_ENTRY("privacy.purge_trackers.last_purge"), PREF_LIST_ENTRY("storage.vacuum.last.places.sqlite"), PREF_LIST_ENTRY("toolkit.startup.last_success"), // Remove fingerprintable things PREF_LIST_ENTRY("browser.startup.homepage_override.buildID"), PREF_LIST_ENTRY("extensions.lastAppBuildId"), PREF_LIST_ENTRY("media.gmp-manager.buildID"), PREF_LIST_ENTRY("toolkit.telemetry.previousBuildID"), }; static const PrefListEntry sDynamicPrefOverrideList[]{ PREF_LIST_ENTRY("apz.subtest"), PREF_LIST_ENTRY("autoadmin.global_config_url"), // Bug 1780575 PREF_LIST_ENTRY("browser.contentblocking.category"), PREF_LIST_ENTRY("browser.search.region"), PREF_LIST_ENTRY( "browser.tabs.remote.testOnly.failPBrowserCreation.browsingContext"), PREF_LIST_ENTRY("browser.translation.bing.authURL"), PREF_LIST_ENTRY("browser.translation.bing.clientIdOverride"), PREF_LIST_ENTRY("browser.translation.bing.translateArrayURL"), PREF_LIST_ENTRY("browser.translation.bing.apiKeyOverride"), PREF_LIST_ENTRY("browser.translation.yandex.apiKeyOverride"), PREF_LIST_ENTRY("browser.translation.yandex.translateURLOverride"), PREF_LIST_ENTRY("browser.uitour.testingOrigins"), PREF_LIST_ENTRY("browser.urlbar.loglevel"), PREF_LIST_ENTRY("browser.urlbar.opencompanionsearch.enabled"), PREF_LIST_ENTRY("capability.policy"), PREF_LIST_ENTRY("dom.securecontext.allowlist"), PREF_LIST_ENTRY("extensions.foobaz"), PREF_LIST_ENTRY( "extensions.formautofill.creditCards.heuristics.testConfidence"), PREF_LIST_ENTRY("general.appname.override"), PREF_LIST_ENTRY("general.appversion.override"), PREF_LIST_ENTRY("general.buildID.override"), PREF_LIST_ENTRY("general.oscpu.override"), PREF_LIST_ENTRY("general.useragent.override"), PREF_LIST_ENTRY("general.platform.override"), PREF_LIST_ENTRY("gfx.blacklist."), PREF_LIST_ENTRY("font.system.whitelist"), PREF_LIST_ENTRY("font.name."), PREF_LIST_ENTRY("logging.config.LOG_FILE"), PREF_LIST_ENTRY("marionette.log.level"), PREF_LIST_ENTRY("media.audio_loopback_dev"), PREF_LIST_ENTRY("media.decoder-doctor."), PREF_LIST_ENTRY("media.cubeb.output_device"), PREF_LIST_ENTRY("media.getusermedia.fake-camera-name"), PREF_LIST_ENTRY("media.hls.server.url"), PREF_LIST_ENTRY("media.peerconnection.nat_simulator.filtering_type"), PREF_LIST_ENTRY("media.peerconnection.nat_simulator.mapping_type"), PREF_LIST_ENTRY("media.peerconnection.nat_simulator.redirect_address"), PREF_LIST_ENTRY("media.peerconnection.nat_simulator.redirect_targets"), PREF_LIST_ENTRY("media.video_loopback_dev"), PREF_LIST_ENTRY("media.webspeech.service.endpoint"), PREF_LIST_ENTRY("network.gio.supported-protocols"), PREF_LIST_ENTRY("network.security.ports.banned"), PREF_LIST_ENTRY("nimbus.syncdatastore."), PREF_LIST_ENTRY("pdfjs."), PREF_LIST_ENTRY("print.printer_"), PREF_LIST_ENTRY("print_printer"), PREF_LIST_ENTRY("places.interactions.customBlocklist"), PREF_LIST_ENTRY( "services.settings.preview_enabled"), // This is really a boolean // dynamic pref, but one Nightly // user has it set as a string... PREF_LIST_ENTRY("spellchecker.dictionary"), PREF_LIST_ENTRY("test.char"), PREF_LIST_ENTRY("Test.IPC."), PREF_LIST_ENTRY("exists.thenDoesNot"), PREF_LIST_ENTRY("type.String."), PREF_LIST_ENTRY("toolkit.mozprotocol.url"), PREF_LIST_ENTRY("toolkit.telemetry.log.level"), PREF_LIST_ENTRY("ui."), }; #undef PREF_LIST_ENTRY static bool ShouldSanitizePreference(const Pref* const aPref) { // In the parent process, we use a heuristic to decide if a pref // value should be sanitized before sending to subprocesses. MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); const char* prefName = aPref->Name(); // If a pref starts with this magic string, it is a Once-Initialized pref // from Static Prefs. It should* not be in the above list and while it looks // like a dnyamically named pref, it is not. // * nothing enforces this if (strncmp(prefName, "$$$", 3) == 0) { return false; } // First check against the denylist. // The services pref is an annoying one - it's much easier to blocklist // the whole branch and then add this one check to let this one annoying // pref through. for (const auto& entry : sParentOnlyPrefBranchList) { if (strncmp(entry.mPrefBranch, prefName, entry.mLen) == 0) { const auto* p = prefName; // This avoids clang-format doing ugly things. return !(strncmp("services.settings.clock_skew_seconds", p, 36) == 0 || strncmp("services.settings.last_update_seconds", p, 37) == 0 || strncmp("services.settings.server", p, 24) == 0); } } // Then check if it's a dynamically named string preference and not // in the override list if (aPref->Type() == PrefType::String && !aPref->HasDefaultValue()) { for (const auto& entry : sDynamicPrefOverrideList) { if (strncmp(entry.mPrefBranch, prefName, entry.mLen) == 0) { return false; } } return true; } return false; } // Forward Declaration - it's not defined in the .h, because we don't need to; // it's only used here. template static bool IsPreferenceSanitized_Impl(const T& aPref); static bool IsPreferenceSanitized(const Pref* const aPref) { return IsPreferenceSanitized_Impl(*aPref); } static bool IsPreferenceSanitized(const PrefWrapper& aPref) { return IsPreferenceSanitized_Impl(aPref); } template static bool IsPreferenceSanitized_Impl(const T& aPref) { if (aPref.IsSanitized()) { MOZ_DIAGNOSTIC_ASSERT(!XRE_IsParentProcess()); MOZ_DIAGNOSTIC_ASSERT(XRE_IsContentProcess()); return true; } return false; } namespace mozilla { // This is the only Check Sanitization function exposed outside of // Preferences.cpp, because this is the only one ever called from // outside this file. bool IsPreferenceSanitized(const char* aPrefName) { // Perform this comparison (see notes above) early to avoid a lookup // if we can avoid it. if (strncmp(aPrefName, "$$$", 3) == 0) { return false; } if (!gContentProcessPrefsAreInited) { return false; } if (Maybe pref = pref_Lookup(aPrefName)) { if (pref.isNothing()) { return true; } return IsPreferenceSanitized(pref.value()); } return true; } Atomic sOmitBlocklistedPrefValues(false); Atomic sCrashOnBlocklistedPref(false); void OnFissionBlocklistPrefChange(const char* aPref, void* aData) { if (strcmp(aPref, kFissionEnforceBlockList) == 0) { sCrashOnBlocklistedPref = StaticPrefs::fission_enforceBlocklistedPrefsInSubprocesses(); } else if (strcmp(aPref, kFissionOmitBlockListValues) == 0) { sOmitBlocklistedPrefValues = StaticPrefs::fission_omitBlocklistedPrefsInSubprocesses(); } else { MOZ_CRASH("Unknown pref passed to callback"); } } } // namespace mozilla // This file contains the C wrappers for the C++ static pref getters, as used // by Rust code. #include "init/StaticPrefsCGetters.cpp"