summaryrefslogtreecommitdiffstats
path: root/security/manager/ssl/DataStorage.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'security/manager/ssl/DataStorage.cpp')
-rw-r--r--security/manager/ssl/DataStorage.cpp1086
1 files changed, 1086 insertions, 0 deletions
diff --git a/security/manager/ssl/DataStorage.cpp b/security/manager/ssl/DataStorage.cpp
new file mode 100644
index 0000000000..4467df0eea
--- /dev/null
+++ b/security/manager/ssl/DataStorage.cpp
@@ -0,0 +1,1086 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DataStorage.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/AppShutdown.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/PContent.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/FileUtils.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Unused.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIFileStreams.h"
+#include "nsIMemoryReporter.h"
+#include "nsIObserverService.h"
+#include "nsISerialEventTarget.h"
+#include "nsITimer.h"
+#include "nsIThread.h"
+#include "nsNetUtil.h"
+#include "nsPrintfCString.h"
+#include "nsStreamUtils.h"
+#include "nsThreadUtils.h"
+#include "private/pprio.h"
+
+#if defined(XP_WIN)
+# include "nsILocalFileWin.h"
+#endif
+
+// NB: Read DataStorage.h first.
+
+// The default time between data changing and a write, in milliseconds.
+static const uint32_t sDataStorageDefaultTimerDelay = 5u * 60u * 1000u;
+// The maximum score an entry can have (prevents overflow)
+static const uint32_t sMaxScore = UINT32_MAX;
+// The maximum number of entries per type of data (limits resource use)
+static const uint32_t sMaxDataEntries = 1024;
+static const int64_t sOneDayInMicroseconds =
+ int64_t(24 * 60 * 60) * PR_USEC_PER_SEC;
+
+namespace mozilla {
+
+class DataStorageMemoryReporter final : public nsIMemoryReporter {
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
+ ~DataStorageMemoryReporter() = default;
+
+ public:
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize) final {
+ nsTArray<nsString> fileNames;
+ DataStorage::GetAllFileNames(fileNames);
+ for (const auto& file : fileNames) {
+ RefPtr<DataStorage> ds = DataStorage::GetFromRawFileName(file);
+ size_t amount = ds->SizeOfIncludingThis(MallocSizeOf);
+ nsPrintfCString path("explicit/data-storage/%s",
+ NS_ConvertUTF16toUTF8(file).get());
+ Unused << aHandleReport->Callback(
+ ""_ns, path, KIND_HEAP, UNITS_BYTES, amount,
+ "Memory used by PSM data storage cache."_ns, aData);
+ }
+ return NS_OK;
+ }
+};
+
+NS_IMPL_ISUPPORTS(DataStorageMemoryReporter, nsIMemoryReporter)
+
+NS_IMPL_ISUPPORTS(DataStorage, nsIObserver)
+
+mozilla::StaticAutoPtr<DataStorage::DataStorages> DataStorage::sDataStorages;
+
+DataStorage::DataStorage(const nsString& aFilename)
+ : mMutex("DataStorage::mMutex"),
+ mPendingWrite(false),
+ mShuttingDown(false),
+ mInitCalled(false),
+ mReadyMonitor("DataStorage::mReadyMonitor"),
+ mReady(false),
+ mFilename(aFilename) {}
+
+// static
+already_AddRefed<DataStorage> DataStorage::Get(DataStorageClass aFilename) {
+ switch (aFilename) {
+#define DATA_STORAGE(_) \
+ case DataStorageClass::_: \
+ return GetFromRawFileName(NS_LITERAL_STRING_FROM_CSTRING(#_ ".txt"));
+#include "mozilla/DataStorageList.h"
+#undef DATA_STORAGE
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid DataStorage type passed?");
+ return nullptr;
+ }
+}
+
+// static
+already_AddRefed<DataStorage> DataStorage::GetFromRawFileName(
+ const nsString& aFilename) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!sDataStorages) {
+ sDataStorages = new DataStorages();
+ ClearOnShutdown(&sDataStorages);
+ }
+ RefPtr<DataStorage> storage;
+ if (!sDataStorages->Get(aFilename, getter_AddRefs(storage))) {
+ storage = new DataStorage(aFilename);
+ sDataStorages->Put(aFilename, RefPtr{storage});
+ }
+ return storage.forget();
+}
+
+// static
+void DataStorage::GetAllFileNames(nsTArray<nsString>& aItems) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!sDataStorages) {
+ return;
+ }
+#define DATA_STORAGE(_) \
+ aItems.AppendElement(NS_LITERAL_STRING_FROM_CSTRING(#_ ".txt"));
+#include "mozilla/DataStorageList.h"
+#undef DATA_STORAGE
+}
+
+// static
+void DataStorage::GetAllChildProcessData(
+ nsTArray<mozilla::psm::DataStorageEntry>& aEntries) {
+ nsTArray<nsString> storageFiles;
+ GetAllFileNames(storageFiles);
+ for (auto& file : storageFiles) {
+ psm::DataStorageEntry entry;
+ entry.filename() = file;
+ RefPtr<DataStorage> storage = DataStorage::GetFromRawFileName(file);
+ if (!storage->mInitCalled) {
+ // Perhaps no consumer has initialized the DataStorage object yet,
+ // so do that now!
+ nsresult rv = storage->Init(nullptr);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ }
+ storage->GetAll(&entry.items());
+ aEntries.AppendElement(std::move(entry));
+ }
+}
+
+// static
+void DataStorage::SetCachedStorageEntries(
+ const nsTArray<mozilla::psm::DataStorageEntry>& aEntries) {
+ MOZ_ASSERT(XRE_IsContentProcess());
+
+ // Make sure to initialize all DataStorage classes.
+ // For each one, we look through the list of our entries and if we find
+ // a matching DataStorage object, we initialize it.
+ //
+ // Note that this is an O(n^2) operation, but the n here is very small
+ // (currently 3). There is a comment in the DataStorageList.h header
+ // about updating the algorithm here to something more fancy if the list
+ // of DataStorage items grows some day.
+ nsTArray<psm::DataStorageEntry> entries;
+#define DATA_STORAGE(_) \
+ { \
+ psm::DataStorageEntry entry; \
+ entry.filename() = NS_LITERAL_STRING_FROM_CSTRING(#_ ".txt"); \
+ for (auto& e : aEntries) { \
+ if (entry.filename().Equals(e.filename())) { \
+ entry.items() = e.items().Clone(); \
+ break; \
+ } \
+ } \
+ entries.AppendElement(std::move(entry)); \
+ }
+#include "mozilla/DataStorageList.h"
+#undef DATA_STORAGE
+
+ for (auto& entry : entries) {
+ RefPtr<DataStorage> storage =
+ DataStorage::GetFromRawFileName(entry.filename());
+ storage->Init(&entry.items());
+ }
+}
+
+size_t DataStorage::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t sizeOfExcludingThis =
+ mPersistentDataTable.ShallowSizeOfExcludingThis(aMallocSizeOf) +
+ mTemporaryDataTable.ShallowSizeOfExcludingThis(aMallocSizeOf) +
+ mPrivateDataTable.ShallowSizeOfExcludingThis(aMallocSizeOf) +
+ mFilename.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ return aMallocSizeOf(this) + sizeOfExcludingThis;
+}
+
+nsresult DataStorage::Init(const nsTArray<DataStorageItem>* aItems,
+ mozilla::ipc::FileDescriptor aWriteFd) {
+ // Don't access the observer service or preferences off the main thread.
+ if (!NS_IsMainThread()) {
+ MOZ_ASSERT_UNREACHABLE("DataStorage::Init called off main thread");
+ return NS_ERROR_NOT_SAME_THREAD;
+ }
+
+ if (AppShutdown::IsShuttingDown()) {
+ // Reject new DataStorage instances if the browser is shutting down. There
+ // is no guarantee that DataStorage writes will be able to be persisted if
+ // we init during shutdown, so we return an error here to hopefully make
+ // this more explicit and consistent.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ MutexAutoLock lock(mMutex);
+
+ // Ignore attempts to initialize several times.
+ if (mInitCalled) {
+ return NS_OK;
+ }
+
+ mInitCalled = true;
+
+ static bool memoryReporterRegistered = false;
+ if (!memoryReporterRegistered) {
+ nsresult rv = RegisterStrongMemoryReporter(new DataStorageMemoryReporter());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ memoryReporterRegistered = true;
+ }
+
+ if (XRE_IsParentProcess() || XRE_IsSocketProcess()) {
+ nsCOMPtr<nsISerialEventTarget> target;
+ nsresult rv = NS_CreateBackgroundTaskQueue(
+ "DataStorage::mBackgroundTaskQueue", getter_AddRefs(target));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ mBackgroundTaskQueue = new TaskQueue(target.forget());
+
+ // For test purposes, we can set the write timer to be very fast.
+ uint32_t timerDelayMS = Preferences::GetInt(
+ "test.datastorage.write_timer_ms", sDataStorageDefaultTimerDelay);
+ rv = NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mTimer), DataStorage::TimerCallback, this, timerDelayMS,
+ nsITimer::TYPE_REPEATING_SLACK_LOW_PRIORITY, "DataStorageTimer",
+ mBackgroundTaskQueue);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ if (XRE_IsParentProcess()) {
+ MOZ_ASSERT(!aItems);
+
+ nsresult rv = AsyncReadData(lock);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ } else {
+ // In the child process and socket process, we use the data passed to us by
+ // the parent process to initialize.
+ MOZ_ASSERT(XRE_IsContentProcess() || XRE_IsSocketProcess());
+ MOZ_ASSERT(aItems);
+
+ if (XRE_IsSocketProcess() && aWriteFd.IsValid()) {
+ mWriteFd = aWriteFd;
+ }
+
+ for (auto& item : *aItems) {
+ Entry entry;
+ entry.mValue = item.value();
+ nsresult rv = PutInternal(item.key(), entry, item.type(), lock);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ mReady = true;
+ NotifyObservers("data-storage-ready");
+ }
+
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ if (NS_WARN_IF(!os)) {
+ return NS_ERROR_FAILURE;
+ }
+ // Clear private data as appropriate.
+ os->AddObserver(this, "last-pb-context-exited", false);
+ if (XRE_IsParentProcess() || XRE_IsSocketProcess()) {
+ // Observe shutdown; save data and prevent any further writes.
+ // In the parent process, we need to write to the profile directory, so
+ // we should listen for profile-before-change so that we can safely write to
+ // the profile.
+ os->AddObserver(this, "profile-before-change", false);
+ // In the Parent process, this is a backstop for xpcshell and other cases
+ // where profile-before-change might not get sent.
+ os->AddObserver(this, "xpcom-shutdown-threads", false);
+ }
+
+ return NS_OK;
+}
+
+class DataStorage::Opener : public Runnable {
+ public:
+ explicit Opener(
+ nsIFile* aFile,
+ std::function<void(mozilla::ipc::FileDescriptor&&)>&& aResolver)
+ : Runnable("DataStorage::Opener"),
+ mFile(aFile),
+ mResolver(std::move(aResolver)) {
+ MOZ_ASSERT(mFile);
+ }
+ ~Opener() = default;
+
+ private:
+ NS_DECL_NSIRUNNABLE
+
+ void ResolveFD();
+
+ nsCOMPtr<nsIFile> mFile;
+ std::function<void(mozilla::ipc::FileDescriptor&&)> mResolver;
+ mozilla::ipc::FileDescriptor mFd;
+};
+
+void DataStorage::Opener::ResolveFD() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mResolver(std::move(mFd));
+}
+
+NS_IMETHODIMP
+DataStorage::Opener::Run() {
+ AutoFDClose prFileDesc;
+ nsresult rv;
+
+#if defined(XP_WIN)
+ nsCOMPtr<nsILocalFileWin> winFile = do_QueryInterface(mFile, &rv);
+ MOZ_ASSERT(winFile);
+ if (NS_SUCCEEDED(rv)) {
+ rv = winFile->OpenNSPRFileDescShareDelete(
+ PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, 0664, &prFileDesc.rwget());
+ }
+#else
+ rv = mFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, 0664,
+ &prFileDesc.rwget());
+#endif /* XP_WIN */
+
+ if (NS_SUCCEEDED(rv)) {
+ mFd = mozilla::ipc::FileDescriptor(
+ mozilla::ipc::FileDescriptor::PlatformHandleType(
+ PR_FileDesc2NativeHandle(prFileDesc)));
+ }
+
+ RefPtr<Opener> self = this;
+ rv = NS_DispatchToMainThread(
+ NS_NewRunnableFunction("DataStorage::Opener::ResolveFD",
+ [self]() { self->ResolveFD(); }),
+ NS_DISPATCH_NORMAL);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ return NS_OK;
+}
+
+nsresult DataStorage::AsyncTakeFileDesc(
+ std::function<void(mozilla::ipc::FileDescriptor&&)>&& aResolver) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ WaitForReady();
+ MutexAutoLock lock(mMutex);
+ if (!mBackingFile) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsCOMPtr<nsIRunnable> job(new Opener(mBackingFile, std::move(aResolver)));
+ nsresult rv = mBackgroundTaskQueue->Dispatch(job.forget());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mBackingFile = nullptr;
+ return NS_OK;
+}
+
+class DataStorage::Reader : public Runnable {
+ public:
+ explicit Reader(DataStorage* aDataStorage)
+ : Runnable("DataStorage::Reader"), mDataStorage(aDataStorage) {}
+ ~Reader();
+
+ private:
+ NS_DECL_NSIRUNNABLE
+
+ static nsresult ParseLine(nsDependentCSubstring& aLine, nsCString& aKeyOut,
+ Entry& aEntryOut);
+
+ RefPtr<DataStorage> mDataStorage;
+};
+
+DataStorage::Reader::~Reader() {
+ // Notify that calls to Get can proceed.
+ {
+ MonitorAutoLock readyLock(mDataStorage->mReadyMonitor);
+ mDataStorage->mReady = true;
+ mDataStorage->mReadyMonitor.NotifyAll();
+ }
+
+ // This is for tests.
+ nsCOMPtr<nsIRunnable> job = NewRunnableMethod<const char*>(
+ "DataStorage::NotifyObservers", mDataStorage,
+ &DataStorage::NotifyObservers, "data-storage-ready");
+ nsresult rv = NS_DispatchToMainThread(job, NS_DISPATCH_NORMAL);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+}
+
+NS_IMETHODIMP
+DataStorage::Reader::Run() {
+ nsresult rv;
+ // Concurrent operations on nsIFile objects are not guaranteed to be safe,
+ // so we clone the file while holding the lock and then release the lock.
+ // At that point, we can safely operate on the clone.
+ nsCOMPtr<nsIFile> file;
+ {
+ MutexAutoLock lock(mDataStorage->mMutex);
+ // If we don't have a profile, bail.
+ if (!mDataStorage->mBackingFile) {
+ return NS_OK;
+ }
+ rv = mDataStorage->mBackingFile->Clone(getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ nsCOMPtr<nsIInputStream> fileInputStream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream), file);
+ // If we failed for some reason other than the file doesn't exist, bail.
+ if (NS_WARN_IF(NS_FAILED(rv) &&
+ rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST && // on Unix
+ rv != NS_ERROR_FILE_NOT_FOUND)) { // on Windows
+ return rv;
+ }
+
+ // If there is a file with data in it, read it. If there isn't,
+ // we'll essentially fall through to notifying that we're good to go.
+ nsCString data;
+ if (fileInputStream) {
+ // Limit to 2MB of data, but only store sMaxDataEntries entries.
+ rv = NS_ConsumeStream(fileInputStream, 1u << 21, data);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ // Atomically parse the data and insert the entries read.
+ // Don't clear existing entries - they may have been inserted between when
+ // this read was kicked-off and when it was run.
+ {
+ MutexAutoLock lock(mDataStorage->mMutex);
+ // The backing file consists of a list of
+ // <key>\t<score>\t<last accessed time>\t<value>\n
+ // The final \n is not optional; if it is not present the line is assumed
+ // to be corrupt.
+ int32_t currentIndex = 0;
+ int32_t newlineIndex = 0;
+ do {
+ newlineIndex = data.FindChar('\n', currentIndex);
+ // If there are no more newlines or the data table has too many
+ // entries, we are done.
+ if (newlineIndex < 0 ||
+ mDataStorage->mPersistentDataTable.Count() >= sMaxDataEntries) {
+ break;
+ }
+
+ nsDependentCSubstring line(data, currentIndex,
+ newlineIndex - currentIndex);
+ currentIndex = newlineIndex + 1;
+ nsCString key;
+ Entry entry;
+ nsresult parseRV = ParseLine(line, key, entry);
+ if (NS_SUCCEEDED(parseRV)) {
+ // It could be the case that a newer entry was added before
+ // we got around to reading the file. Don't overwrite new entries.
+ Entry newerEntry;
+ bool present = mDataStorage->mPersistentDataTable.Get(key, &newerEntry);
+ if (!present) {
+ mDataStorage->mPersistentDataTable.Put(key, entry);
+ }
+ }
+ } while (true);
+
+ Telemetry::Accumulate(Telemetry::DATA_STORAGE_ENTRIES,
+ mDataStorage->mPersistentDataTable.Count());
+ }
+
+ return NS_OK;
+}
+
+// The key must be a non-empty string containing no instances of '\t' or '\n',
+// and must have a length no more than 256.
+// The value must not contain '\n' and must have a length no more than 1024.
+// The length limits are to prevent unbounded memory and disk usage.
+/* static */
+nsresult DataStorage::ValidateKeyAndValue(const nsCString& aKey,
+ const nsCString& aValue) {
+ if (aKey.IsEmpty()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ if (aKey.Length() > 256) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ int32_t delimiterIndex = aKey.FindChar('\t', 0);
+ if (delimiterIndex >= 0) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ delimiterIndex = aKey.FindChar('\n', 0);
+ if (delimiterIndex >= 0) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ delimiterIndex = aValue.FindChar('\n', 0);
+ if (delimiterIndex >= 0) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ if (aValue.Length() > 1024) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ return NS_OK;
+}
+
+// Each line is: <key>\t<score>\t<last accessed time>\t<value>
+// Where <score> is a uint32_t as a string, <last accessed time> is a
+// int32_t as a string, and the rest are strings.
+// <value> can contain anything but a newline.
+// Returns a successful status if the line can be decoded into a key and entry.
+// Otherwise, an error status is returned and the values assigned to the
+// output parameters are in an undefined state.
+/* static */
+nsresult DataStorage::Reader::ParseLine(nsDependentCSubstring& aLine,
+ nsCString& aKeyOut, Entry& aEntryOut) {
+ // First find the indices to each part of the line.
+ int32_t scoreIndex;
+ scoreIndex = aLine.FindChar('\t', 0) + 1;
+ if (scoreIndex <= 0) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ int32_t accessedIndex = aLine.FindChar('\t', scoreIndex) + 1;
+ if (accessedIndex <= 0) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ int32_t valueIndex = aLine.FindChar('\t', accessedIndex) + 1;
+ if (valueIndex <= 0) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // Now make substrings based on where each part is.
+ nsDependentCSubstring keyPart(aLine, 0, scoreIndex - 1);
+ nsDependentCSubstring scorePart(aLine, scoreIndex,
+ accessedIndex - scoreIndex - 1);
+ nsDependentCSubstring accessedPart(aLine, accessedIndex,
+ valueIndex - accessedIndex - 1);
+ nsDependentCSubstring valuePart(aLine, valueIndex);
+
+ nsresult rv;
+ rv = DataStorage::ValidateKeyAndValue(nsCString(keyPart),
+ nsCString(valuePart));
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // Now attempt to decode the score part as a uint32_t.
+ // XXX nsDependentCSubstring doesn't support ToInteger
+ int32_t integer = nsCString(scorePart).ToInteger(&rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ if (integer < 0) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ aEntryOut.mScore = (uint32_t)integer;
+
+ integer = nsCString(accessedPart).ToInteger(&rv);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (integer < 0) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ aEntryOut.mLastAccessed = integer;
+
+ // Now set the key and value.
+ aKeyOut.Assign(keyPart);
+ aEntryOut.mValue.Assign(valuePart);
+
+ return NS_OK;
+}
+
+nsresult DataStorage::AsyncReadData(const MutexAutoLock& /*aProofOfLock*/) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ // Allocate a Reader so that even if it isn't dispatched,
+ // the data-storage-ready notification will be fired and Get
+ // will be able to proceed (this happens in its destructor).
+ nsCOMPtr<nsIRunnable> job(new Reader(this));
+ nsresult rv;
+ // If we don't have a profile directory, this will fail.
+ // That's okay - it just means there is no persistent state.
+ rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(mBackingFile));
+ if (NS_FAILED(rv)) {
+ mBackingFile = nullptr;
+ return NS_OK;
+ }
+
+ rv = mBackingFile->Append(mFilename);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = mBackgroundTaskQueue->Dispatch(job.forget());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+bool DataStorage::IsReady() {
+ MonitorAutoLock readyLock(mReadyMonitor);
+ return mReady;
+}
+
+void DataStorage::WaitForReady() {
+ MOZ_DIAGNOSTIC_ASSERT(mInitCalled, "Waiting before Init() has been called?");
+
+ MonitorAutoLock readyLock(mReadyMonitor);
+ while (!mReady) {
+ readyLock.Wait();
+ }
+ MOZ_ASSERT(mReady);
+}
+
+nsCString DataStorage::Get(const nsCString& aKey, DataStorageType aType) {
+ WaitForReady();
+ MutexAutoLock lock(mMutex);
+
+ Entry entry;
+ bool foundValue = GetInternal(aKey, &entry, aType, lock);
+ if (!foundValue) {
+ return ""_ns;
+ }
+
+ // If we're here, we found a value. Maybe update its score.
+ if (entry.UpdateScore()) {
+ PutInternal(aKey, entry, aType, lock);
+ }
+
+ return entry.mValue;
+}
+
+bool DataStorage::GetInternal(const nsCString& aKey, Entry* aEntry,
+ DataStorageType aType,
+ const MutexAutoLock& aProofOfLock) {
+ DataStorageTable& table = GetTableForType(aType, aProofOfLock);
+ bool foundValue = table.Get(aKey, aEntry);
+ return foundValue;
+}
+
+DataStorage::DataStorageTable& DataStorage::GetTableForType(
+ DataStorageType aType, const MutexAutoLock& /*aProofOfLock*/) {
+ switch (aType) {
+ case DataStorage_Persistent:
+ return mPersistentDataTable;
+ case DataStorage_Temporary:
+ return mTemporaryDataTable;
+ case DataStorage_Private:
+ return mPrivateDataTable;
+ }
+
+ MOZ_CRASH("given bad DataStorage storage type");
+}
+
+void DataStorage::ReadAllFromTable(DataStorageType aType,
+ nsTArray<DataStorageItem>* aItems,
+ const MutexAutoLock& aProofOfLock) {
+ for (auto iter = GetTableForType(aType, aProofOfLock).Iter(); !iter.Done();
+ iter.Next()) {
+ DataStorageItem* item = aItems->AppendElement();
+ item->key() = iter.Key();
+ item->value() = iter.Data().mValue;
+ item->type() = aType;
+ }
+}
+
+void DataStorage::GetAll(nsTArray<DataStorageItem>* aItems) {
+ WaitForReady();
+ MutexAutoLock lock(mMutex);
+
+ aItems->SetCapacity(mPersistentDataTable.Count() +
+ mTemporaryDataTable.Count() + mPrivateDataTable.Count());
+ ReadAllFromTable(DataStorage_Persistent, aItems, lock);
+ ReadAllFromTable(DataStorage_Temporary, aItems, lock);
+ ReadAllFromTable(DataStorage_Private, aItems, lock);
+}
+
+// Limit the number of entries per table. This is to prevent unbounded
+// resource use. The eviction strategy is as follows:
+// - An entry's score is incremented once for every day it is accessed.
+// - Evict an entry with score no more than any other entry in the table
+// (this is the same as saying evict the entry with the lowest score,
+// except for when there are multiple entries with the lowest score,
+// in which case one of them is evicted - which one is not specified).
+void DataStorage::MaybeEvictOneEntry(DataStorageType aType,
+ const MutexAutoLock& aProofOfLock) {
+ DataStorageTable& table = GetTableForType(aType, aProofOfLock);
+ if (table.Count() >= sMaxDataEntries) {
+ KeyAndEntry toEvict;
+ // If all entries have score sMaxScore, this won't actually remove
+ // anything. This will never happen, however, because having that high
+ // a score either means someone tampered with the backing file or every
+ // entry has been accessed once a day for ~4 billion days.
+ // The worst that will happen is there will be 1025 entries in the
+ // persistent data table, with the 1025th entry being replaced every time
+ // data with a new key is inserted into the table. This is bad but
+ // ultimately not that concerning, considering that if an attacker can
+ // modify data in the profile, they can cause much worse harm.
+ toEvict.mEntry.mScore = sMaxScore;
+
+ for (auto iter = table.Iter(); !iter.Done(); iter.Next()) {
+ Entry entry = iter.UserData();
+ if (entry.mScore < toEvict.mEntry.mScore) {
+ toEvict.mKey = iter.Key();
+ toEvict.mEntry = entry;
+ }
+ }
+
+ table.Remove(toEvict.mKey);
+ }
+}
+
+// NB: Because this may cross a thread boundary, any variables captured by the
+// Functor must be captured by copy and not by reference.
+template <class Functor>
+static void RunOnAllContentParents(Functor func) {
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+ using dom::ContentParent;
+
+ nsCOMPtr<nsIRunnable> r =
+ NS_NewRunnableFunction("RunOnAllContentParents", [func]() {
+ nsTArray<ContentParent*> parents;
+ ContentParent::GetAll(parents);
+ for (auto& parent : parents) {
+ func(parent);
+ }
+ });
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(r));
+}
+
+nsresult DataStorage::Put(const nsCString& aKey, const nsCString& aValue,
+ DataStorageType aType) {
+ WaitForReady();
+ MutexAutoLock lock(mMutex);
+
+ nsresult rv;
+ rv = ValidateKeyAndValue(aKey, aValue);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ Entry entry;
+ bool exists = GetInternal(aKey, &entry, aType, lock);
+ if (exists) {
+ entry.UpdateScore();
+ } else {
+ MaybeEvictOneEntry(aType, lock);
+ }
+ entry.mValue = aValue;
+ rv = PutInternal(aKey, entry, aType, lock);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsString filename(mFilename);
+ RunOnAllContentParents(
+ [aKey, aValue, aType, filename](dom::ContentParent* aParent) {
+ DataStorageItem item;
+ item.key() = aKey;
+ item.value() = aValue;
+ item.type() = aType;
+ Unused << aParent->SendDataStoragePut(filename, item);
+ });
+
+ return NS_OK;
+}
+
+nsresult DataStorage::PutInternal(const nsCString& aKey, Entry& aEntry,
+ DataStorageType aType,
+ const MutexAutoLock& aProofOfLock) {
+ DataStorageTable& table = GetTableForType(aType, aProofOfLock);
+ table.Put(aKey, aEntry);
+
+ if (aType == DataStorage_Persistent) {
+ mPendingWrite = true;
+ }
+
+ return NS_OK;
+}
+
+void DataStorage::Remove(const nsCString& aKey, DataStorageType aType) {
+ WaitForReady();
+ MutexAutoLock lock(mMutex);
+
+ DataStorageTable& table = GetTableForType(aType, lock);
+ table.Remove(aKey);
+
+ if (aType == DataStorage_Persistent) {
+ mPendingWrite = true;
+ }
+
+ nsString filename(mFilename);
+ RunOnAllContentParents([filename, aKey, aType](dom::ContentParent* aParent) {
+ Unused << aParent->SendDataStorageRemove(filename, aKey, aType);
+ });
+}
+
+class DataStorage::Writer final : public Runnable {
+ public:
+ Writer(nsCString& aData, DataStorage* aDataStorage)
+ : Runnable("DataStorage::Writer"),
+ mData(aData),
+ mDataStorage(aDataStorage) {}
+
+ protected:
+ NS_DECL_NSIRUNNABLE
+ nsresult CreateOutputStream(nsIOutputStream** aResult);
+
+ nsCString mData;
+ RefPtr<DataStorage> mDataStorage;
+};
+
+nsresult DataStorage::Writer::CreateOutputStream(nsIOutputStream** aResult) {
+ nsresult rv;
+
+ if (XRE_IsSocketProcess()) {
+ mozilla::ipc::FileDescriptor fd;
+ {
+ MutexAutoLock lock(mDataStorage->mMutex);
+ fd = mDataStorage->mWriteFd;
+ }
+
+ if (!fd.IsValid()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return NS_NewLocalFileOutputStream(aResult, fd);
+ }
+
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ // Concurrent operations on nsIFile objects are not guaranteed to be safe,
+ // so we clone the file while holding the lock and then release the lock.
+ // At that point, we can safely operate on the clone.
+ nsCOMPtr<nsIFile> file;
+ {
+ MutexAutoLock lock(mDataStorage->mMutex);
+ // If we don't have a profile, bail.
+ if (!mDataStorage->mBackingFile) {
+ return NS_OK;
+ }
+ rv = mDataStorage->mBackingFile->Clone(getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ return NS_NewLocalFileOutputStream(aResult, file,
+ PR_CREATE_FILE | PR_TRUNCATE | PR_WRONLY);
+}
+
+NS_IMETHODIMP
+DataStorage::Writer::Run() {
+ nsCOMPtr<nsIOutputStream> outputStream;
+ nsresult rv = CreateOutputStream(getter_AddRefs(outputStream));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // When the output stream is null, it means we don't have a profile.
+ if (!outputStream) {
+ return NS_OK;
+ }
+
+ const char* ptr = mData.get();
+ uint32_t remaining = mData.Length();
+ uint32_t written = 0;
+ while (remaining > 0) {
+ rv = outputStream->Write(ptr, remaining, &written);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ remaining -= written;
+ ptr += written;
+ }
+
+ // Observed by tests.
+ nsCOMPtr<nsIRunnable> job = NewRunnableMethod<const char*>(
+ "DataStorage::NotifyObservers", mDataStorage,
+ &DataStorage::NotifyObservers, "data-storage-written");
+ rv = NS_DispatchToMainThread(job, NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult DataStorage::AsyncWriteData(const MutexAutoLock& /*aProofOfLock*/) {
+ MOZ_ASSERT(XRE_IsParentProcess() || XRE_IsSocketProcess());
+
+ if (!mPendingWrite || mShuttingDown ||
+ (!mBackingFile && !mWriteFd.IsValid())) {
+ return NS_OK;
+ }
+
+ nsCString output;
+ for (auto iter = mPersistentDataTable.Iter(); !iter.Done(); iter.Next()) {
+ Entry entry = iter.UserData();
+ output.Append(iter.Key());
+ output.Append('\t');
+ output.AppendInt(entry.mScore);
+ output.Append('\t');
+ output.AppendInt(entry.mLastAccessed);
+ output.Append('\t');
+ output.Append(entry.mValue);
+ output.Append('\n');
+ }
+
+ nsCOMPtr<nsIRunnable> job(new Writer(output, this));
+ nsresult rv = mBackgroundTaskQueue->Dispatch(job.forget());
+ mPendingWrite = false;
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult DataStorage::Clear() {
+ WaitForReady();
+ MutexAutoLock lock(mMutex);
+ mPersistentDataTable.Clear();
+ mTemporaryDataTable.Clear();
+ mPrivateDataTable.Clear();
+ mPendingWrite = true;
+
+ if (XRE_IsParentProcess() || XRE_IsSocketProcess()) {
+ // Asynchronously clear the file. This is similar to the permission manager
+ // in that it doesn't wait to synchronously remove the data from its backing
+ // storage either.
+ nsresult rv = AsyncWriteData(lock);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ nsString filename(mFilename);
+ RunOnAllContentParents([filename](dom::ContentParent* aParent) {
+ Unused << aParent->SendDataStorageClear(filename);
+ });
+
+ return NS_OK;
+}
+
+/* static */
+void DataStorage::TimerCallback(nsITimer* aTimer, void* aClosure) {
+ MOZ_ASSERT(XRE_IsParentProcess() || XRE_IsSocketProcess());
+
+ RefPtr<DataStorage> aDataStorage = (DataStorage*)aClosure;
+ MutexAutoLock lock(aDataStorage->mMutex);
+ Unused << aDataStorage->AsyncWriteData(lock);
+}
+
+void DataStorage::NotifyObservers(const char* aTopic) {
+ // Don't access the observer service off the main thread.
+ if (!NS_IsMainThread()) {
+ MOZ_ASSERT_UNREACHABLE(
+ "DataStorage::NotifyObservers called off main thread");
+ return;
+ }
+
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ if (os) {
+ os->NotifyObservers(nullptr, aTopic, mFilename.get());
+ }
+}
+
+void DataStorage::ShutdownTimer() {
+ MOZ_ASSERT(XRE_IsParentProcess() || XRE_IsSocketProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mTimer) {
+ nsresult rv = mTimer->Cancel();
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ mTimer = nullptr;
+ }
+}
+
+//------------------------------------------------------------
+// DataStorage::nsIObserver
+//------------------------------------------------------------
+
+NS_IMETHODIMP
+DataStorage::Observe(nsISupports* /*aSubject*/, const char* aTopic,
+ const char16_t* /*aData*/) {
+ if (!NS_IsMainThread()) {
+ MOZ_ASSERT_UNREACHABLE("DataStorage::Observe called off main thread");
+ return NS_ERROR_NOT_SAME_THREAD;
+ }
+
+ if (strcmp(aTopic, "last-pb-context-exited") == 0) {
+ MutexAutoLock lock(mMutex);
+ mPrivateDataTable.Clear();
+ return NS_OK;
+ }
+
+ if (!XRE_IsParentProcess() && !XRE_IsSocketProcess()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected observation topic for content proces");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (strcmp(aTopic, "profile-before-change") == 0 ||
+ strcmp(aTopic, "xpcom-shutdown-threads") == 0) {
+ RefPtr<TaskQueue> taskQueueToAwait;
+ {
+ MutexAutoLock lock(mMutex);
+ if (!mShuttingDown) {
+ nsresult rv = AsyncWriteData(lock);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ mShuttingDown = true;
+ mBackgroundTaskQueue->BeginShutdown();
+ taskQueueToAwait = mBackgroundTaskQueue;
+ }
+ }
+ // Tasks on the background queue may take the lock, so it can't be held
+ // while waiting for them to finish.
+ if (taskQueueToAwait) {
+ taskQueueToAwait->AwaitShutdownAndIdle();
+ }
+ ShutdownTimer();
+ }
+
+ return NS_OK;
+}
+
+DataStorage::Entry::Entry()
+ : mScore(0), mLastAccessed((int32_t)(PR_Now() / sOneDayInMicroseconds)) {}
+
+// Updates this entry's score. Returns true if the score has actually changed.
+// If it's been less than a day since this entry has been accessed, the score
+// does not change. Otherwise, the score increases by 1.
+// The default score is 0. The maximum score is the maximum value that can
+// be represented by an unsigned 32 bit integer.
+// This is to handle evictions from our tables, which in turn is to prevent
+// unbounded resource use.
+bool DataStorage::Entry::UpdateScore() {
+ int32_t nowInDays = (int32_t)(PR_Now() / sOneDayInMicroseconds);
+ int32_t daysSinceAccessed = (nowInDays - mLastAccessed);
+
+ // Update the last accessed time.
+ mLastAccessed = nowInDays;
+
+ // If it's been less than a day since we've been accessed,
+ // the score isn't updated.
+ if (daysSinceAccessed < 1) {
+ return false;
+ }
+
+ // Otherwise, increment the score (but don't overflow).
+ if (mScore < sMaxScore) {
+ mScore++;
+ }
+ return true;
+}
+
+} // namespace mozilla