Bug 775370 - Part 1: Introduce DataStorage. r=froydnj, r=mmc, a=lmandel
authorDavid Keeler <dkeeler@mozilla.com>
Mon, 09 Sep 2013 13:37:21 -0700
changeset 224843 b339d53f95f289b2d0a9197c95d3535e218a233f
parent 224842 41e56fdebf65461747a18511899bbfe371d1da76
child 224844 bd7532ac742caabd63900e3a729e29f85cb32ca0
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfroydnj, mmc, lmandel
bugs775370
milestone34.0a2
Bug 775370 - Part 1: Introduce DataStorage. r=froydnj, r=mmc, a=lmandel
security/manager/boot/src/DataStorage.cpp
security/manager/boot/src/DataStorage.h
security/manager/boot/src/moz.build
security/manager/ssl/tests/gtest/DataStorageTest.cpp
security/manager/ssl/tests/gtest/moz.build
new file mode 100644
--- /dev/null
+++ b/security/manager/boot/src/DataStorage.cpp
@@ -0,0 +1,793 @@
+/* -*- 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/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/unused.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIObserverService.h"
+#include "nsITimer.h"
+#include "nsNetUtil.h"
+#include "nsStreamUtils.h"
+#include "nsThreadUtils.h"
+#include "prprf.h"
+
+// 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 {
+
+NS_IMPL_ISUPPORTS(DataStorage,
+                  nsIObserver)
+
+DataStorage::DataStorage(const nsString& aFilename)
+  : mMutex("DataStorage::mMutex")
+  , mPendingWrite(false)
+  , mShuttingDown(false)
+  , mReadyMonitor("DataStorage::mReadyMonitor")
+  , mReady(false)
+  , mFilename(aFilename)
+{
+}
+
+DataStorage::~DataStorage()
+{
+}
+
+nsresult
+DataStorage::Init(bool& aDataWillPersist)
+{
+  // Don't access the observer service or preferences off the main thread.
+  if (!NS_IsMainThread()) {
+    NS_NOTREACHED("DataStorage::Init called off main thread");
+    return NS_ERROR_NOT_SAME_THREAD;
+  }
+
+  MutexAutoLock lock(mMutex);
+
+  nsresult rv;
+  rv = NS_NewThread(getter_AddRefs(mWorkerThread));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = AsyncReadData(aDataWillPersist, lock);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  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);
+  // Observe shutdown; save data and prevent any further writes.
+  os->AddObserver(this, "profile-before-change", false);
+
+  // For test purposes, we can set the write timer to be very fast.
+  mTimerDelay = Preferences::GetInt("test.datastorage.write_timer_ms",
+                                    sDataStorageDefaultTimerDelay);
+  rv = Preferences::AddStrongObserver(this, "test.datastorage.write_timer_ms");
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+class DataStorage::Reader : public nsRunnable
+{
+public:
+  explicit Reader(DataStorage* aDataStorage)
+    : mDataStorage(aDataStorage)
+  {
+  }
+  ~Reader();
+
+private:
+  NS_DECL_NSIRUNNABLE
+
+  static nsresult ParseLine(nsDependentCSubstring& aLine, nsCString& aKeyOut,
+                            Entry& aEntryOut);
+
+  nsRefPtr<DataStorage> mDataStorage;
+};
+
+DataStorage::Reader::~Reader()
+{
+  // Notify that calls to Get can proceed.
+  {
+    MonitorAutoLock readyLock(mDataStorage->mReadyMonitor);
+    mDataStorage->mReady = true;
+    nsresult rv = mDataStorage->mReadyMonitor.NotifyAll();
+    unused << NS_WARN_IF(NS_FAILED(rv));
+  }
+
+  // This is for tests.
+  nsCOMPtr<nsIRunnable> job =
+    NS_NewRunnableMethodWithArg<const char*>(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);
+  }
+
+  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(bool& aHaveProfileDir,
+                           const MutexAutoLock& /*aProofOfLock*/)
+{
+  aHaveProfileDir = false;
+  // 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).
+  nsRefPtr<Reader> 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 = mWorkerThread->Dispatch(job, NS_DISPATCH_NORMAL);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  aHaveProfileDir = true;
+  return NS_OK;
+}
+
+void
+DataStorage::WaitForReady()
+{
+  MonitorAutoLock readyLock(mReadyMonitor);
+  while (!mReady) {
+    nsresult rv = readyLock.Wait();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      break;
+    }
+  }
+  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 EmptyCString();
+  }
+
+  // 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");
+}
+
+// NB: The lock must be held when calling this function.
+/* static */
+PLDHashOperator
+DataStorage::EvictCallback(const nsACString& aKey, Entry aEntry, void* aArg)
+{
+  KeyAndEntry* toEvict = (KeyAndEntry*)aArg;
+  if (aEntry.mScore < toEvict->mEntry.mScore) {
+    toEvict->mKey = aKey;
+    toEvict->mEntry = aEntry;
+  }
+  return PLDHashOperator::PL_DHASH_NEXT;
+}
+
+// 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;
+    table.EnumerateRead(EvictCallback, (void*)&toEvict);
+    table.Remove(toEvict.mKey);
+  }
+}
+
+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;
+  }
+
+  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) {
+    return AsyncSetTimer(aProofOfLock);
+  }
+
+  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) {
+    unused << AsyncSetTimer(lock);
+  }
+}
+
+class DataStorage::Writer : public nsRunnable
+{
+public:
+  Writer(nsCString& aData, DataStorage* aDataStorage)
+    : mData(aData)
+    , mDataStorage(aDataStorage)
+  {
+  }
+
+private:
+  NS_DECL_NSIRUNNABLE
+
+  nsCString mData;
+  nsRefPtr<DataStorage> mDataStorage;
+};
+
+NS_IMETHODIMP
+DataStorage::Writer::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<nsIOutputStream> outputStream;
+  rv = NS_NewLocalFileOutputStream(getter_AddRefs(outputStream), file,
+                                   PR_CREATE_FILE | PR_TRUNCATE | PR_WRONLY);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  const char* ptr = mData.get();
+  int32_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 =
+    NS_NewRunnableMethodWithArg<const char*>(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;
+}
+
+// NB: The lock must be held when calling this function.
+/* static */
+PLDHashOperator
+DataStorage::WriteDataCallback(const nsACString& aKey, Entry aEntry, void* aArg)
+{
+  nsCString* output = (nsCString*)aArg;
+  output->Append(aKey);
+  output->Append('\t');
+  output->AppendInt(aEntry.mScore);
+  output->Append('\t');
+  output->AppendInt(aEntry.mLastAccessed);
+  output->Append('\t');
+  output->Append(aEntry.mValue);
+  output->Append('\n');
+  return PLDHashOperator::PL_DHASH_NEXT;
+}
+
+nsresult
+DataStorage::AsyncWriteData(const MutexAutoLock& /*aProofOfLock*/)
+{
+  if (mShuttingDown || !mBackingFile) {
+    return NS_OK;
+  }
+
+  nsCString output;
+  mPersistentDataTable.EnumerateRead(WriteDataCallback, (void*)&output);
+
+  nsRefPtr<Writer> job(new Writer(output, this));
+  nsresult rv = mWorkerThread->Dispatch(job, NS_DISPATCH_NORMAL);
+  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();
+
+  // 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;
+  }
+  return NS_OK;
+}
+
+/* static */
+void
+DataStorage::TimerCallback(nsITimer* aTimer, void* aClosure)
+{
+  nsRefPtr<DataStorage> aDataStorage = (DataStorage*)aClosure;
+  MutexAutoLock lock(aDataStorage->mMutex);
+  unused << aDataStorage->AsyncWriteData(lock);
+}
+
+// We only initialize the timer on the worker thread because it's not safe
+// to mix what threads are operating on the timer.
+nsresult
+DataStorage::AsyncSetTimer(const MutexAutoLock& /*aProofOfLock*/)
+{
+  if (mShuttingDown) {
+    return NS_OK;
+  }
+
+  mPendingWrite = true;
+  nsCOMPtr<nsIRunnable> job =
+    NS_NewRunnableMethod(this, &DataStorage::SetTimer);
+  nsresult rv = mWorkerThread->Dispatch(job, NS_DISPATCH_NORMAL);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+  return NS_OK;
+}
+
+void
+DataStorage::SetTimer()
+{
+  MOZ_ASSERT(!NS_IsMainThread());
+  MutexAutoLock lock(mMutex);
+
+  nsresult rv;
+  if (!mTimer) {
+    mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return;
+    }
+  }
+
+  rv = mTimer->InitWithFuncCallback(TimerCallback, this,
+                                    mTimerDelay, nsITimer::TYPE_ONE_SHOT);
+  unused << NS_WARN_IF(NS_FAILED(rv));
+}
+
+void
+DataStorage::NotifyObservers(const char* aTopic)
+{
+  // Don't access the observer service off the main thread.
+  if (!NS_IsMainThread()) {
+    NS_NOTREACHED("DataStorage::NotifyObservers called off main thread");
+    return;
+  }
+
+  nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+  if (os) {
+    os->NotifyObservers(nullptr, aTopic, mFilename.get());
+  }
+}
+
+nsresult
+DataStorage::DispatchShutdownTimer(const MutexAutoLock& /*aProofOfLock*/)
+{
+  nsCOMPtr<nsIRunnable> job =
+    NS_NewRunnableMethod(this, &DataStorage::ShutdownTimer);
+  nsresult rv = mWorkerThread->Dispatch(job, NS_DISPATCH_NORMAL);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+  return NS_OK;
+}
+
+void
+DataStorage::ShutdownTimer()
+{
+  MOZ_ASSERT(!NS_IsMainThread());
+  MutexAutoLock lock(mMutex);
+  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)
+{
+  // Don't access preferences off the main thread.
+  if (!NS_IsMainThread()) {
+    NS_NOTREACHED("DataStorage::Observe called off main thread");
+    return NS_ERROR_NOT_SAME_THREAD;
+  }
+
+  nsresult rv;
+  if (strcmp(aTopic, "last-pb-context-exited") == 0) {
+    MutexAutoLock lock(mMutex);
+    mPrivateDataTable.Clear();
+  } else if (strcmp(aTopic, "profile-before-change") == 0) {
+    {
+      MutexAutoLock lock(mMutex);
+      rv = AsyncWriteData(lock);
+      mShuttingDown = true;
+      unused << NS_WARN_IF(NS_FAILED(rv));
+      if (mTimer) {
+        rv = DispatchShutdownTimer(lock);
+        unused << NS_WARN_IF(NS_FAILED(rv));
+      }
+    }
+    // Run the thread to completion and prevent any further events
+    // being scheduled to it. The thread may need the lock, so we can't
+    // hold it here.
+    rv = mWorkerThread->Shutdown();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+  } else if (strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) == 0) {
+    MutexAutoLock lock(mMutex);
+    mTimerDelay = Preferences::GetInt("test.datastorage.write_timer_ms",
+                                      sDataStorageDefaultTimerDelay);
+  }
+
+  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
new file mode 100644
--- /dev/null
+++ b/security/manager/boot/src/DataStorage.h
@@ -0,0 +1,185 @@
+/* -*- 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/. */
+
+#ifndef mozilla_DataStorage_h
+#define mozilla_DataStorage_h
+
+#include "mozilla/Monitor.h"
+#include "mozilla/Mutex.h"
+#include "nsCOMPtr.h"
+#include "nsDataHashtable.h"
+#include "nsIObserver.h"
+#include "nsIThread.h"
+#include "nsITimer.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+/**
+ * DataStorage is a threadsafe, generic, narrow string-based hash map that
+ * persists data on disk and additionally handles temporary and private data.
+ * However, if used in a context where there is no profile directory, data
+ * will not be persisted.
+ *
+ * Its lifecycle is as follows:
+ * - Allocate with a filename (this is or will eventually be a file in the
+ *   profile directory, if the profile exists).
+ * - Call Init() from the main thread. This spins off an asynchronous read
+ *   of the backing file.
+ * - Eventually observers of the topic "data-storage-ready" will be notified
+ *   with the backing filename as the data in the notification when this
+ *   has completed.
+ * - Should the profile directory not be available, (e.g. in xpcshell),
+ *   DataStorage will not initially read any persistent data. The
+ *   "data-storage-ready" event will still be emitted. This follows semantics
+ *   similar to the permission manager and allows tests that test
+ *   unrelated components to proceed without a profile.
+ * - When any persistent data changes, a timer is initialized that will
+ *   eventually asynchronously write all persistent data to the backing file.
+ *   When this happens, observers will be notified with the topic
+ *   "data-storage-written" and the backing filename as the data.
+ *   It is possible to receive a "data-storage-written" event while there exist
+ *   pending persistent data changes. However, those changes will cause the
+ *   timer to be reinitialized and another "data-storage-written" event will
+ *   be sent.
+ * - When DataStorage observes the topic "profile-before-change" in
+ *   anticipation of shutdown, all persistent data is synchronously written to
+ *   the backing file. The worker thread responsible for these writes is then
+ *   disabled to prevent further writes to that file (the delayed-write timer
+ *   is cancelled when this happens).
+ * - For testing purposes, the preference "test.datastorage.write_timer_ms" can
+ *   be set to cause the asynchronous writing of data to happen more quickly.
+ * - To prevent unbounded memory and disk use, the number of entries in each
+ *   table is limited to 1024. Evictions are handled in by a modified LRU scheme
+ *   (see implementation comments).
+ * - NB: Instances of DataStorage have long lifetimes because they are strong
+ *   observers of events and won't go away until the observer service does.
+ *
+ * For each key/value:
+ * - The key must be a non-empty string containing no instances of '\t' or '\n'
+ *   (this is a limitation of how the data is stored and will be addressed in
+ *   the future).
+ * - The key 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 disk and memory usage)
+ */
+
+/**
+ * Data that is DataStorage_Persistent is saved on disk. DataStorage_Temporary
+ * and DataStorage_Private are not saved. DataStorage_Private is meant to
+ * only be set and accessed from private contexts. It will be cleared upon
+ * observing the event "last-pb-context-exited".
+ */
+enum DataStorageType {
+  DataStorage_Persistent,
+  DataStorage_Temporary,
+  DataStorage_Private
+};
+
+class DataStorage : public nsIObserver
+{
+public:
+  NS_DECL_THREADSAFE_ISUPPORTS
+  NS_DECL_NSIOBSERVER
+
+  // If there is a profile directory, there is or will eventually be a file
+  // by the name specified by aFilename there.
+  explicit DataStorage(const nsString& aFilename);
+
+  // Initializes the DataStorage. Must be called before using.
+  // aDataWillPersist returns whether or not data can be persistently saved.
+  nsresult Init(/*out*/bool& aDataWillPersist);
+  // Given a key and a type of data, returns a value. Returns an empty string if
+  // the key is not present for that type of data. If Get is called before the
+  // "data-storage-ready" event is observed, it will block. NB: It is not
+  // currently possible to differentiate between missing data and data that is
+  // the empty string.
+  nsCString Get(const nsCString& aKey, DataStorageType aType);
+  // Give a key, value, and type of data, adds an entry as appropriate.
+  // Updates existing entries.
+  nsresult Put(const nsCString& aKey, const nsCString& aValue,
+               DataStorageType aType);
+  // Given a key and type of data, removes an entry if present.
+  void Remove(const nsCString& aKey, DataStorageType aType);
+  // Removes all entries of all types of data.
+  nsresult Clear();
+
+private:
+  virtual ~DataStorage();
+
+  class Writer;
+  class Reader;
+
+  class Entry
+  {
+  public:
+    Entry();
+    bool UpdateScore();
+
+    uint32_t mScore;
+    int32_t mLastAccessed; // the last accessed time in days since the epoch
+    nsCString mValue;
+  };
+
+  // Utility class for scanning tables for an entry to evict.
+  class KeyAndEntry
+  {
+  public:
+    nsCString mKey;
+    Entry mEntry;
+  };
+
+  typedef nsDataHashtable<nsCStringHashKey, Entry> DataStorageTable;
+
+  void WaitForReady();
+  nsresult AsyncWriteData(const MutexAutoLock& aProofOfLock);
+  nsresult AsyncReadData(bool& aHaveProfileDir,
+                         const MutexAutoLock& aProofOfLock);
+  nsresult AsyncSetTimer(const MutexAutoLock& aProofOfLock);
+  nsresult DispatchShutdownTimer(const MutexAutoLock& aProofOfLock);
+
+  static nsresult ValidateKeyAndValue(const nsCString& aKey,
+                                      const nsCString& aValue);
+  static void TimerCallback(nsITimer* aTimer, void* aClosure);
+  static PLDHashOperator WriteDataCallback(const nsACString& aKey, Entry aEntry,
+                                           void* aArg);
+  static PLDHashOperator EvictCallback(const nsACString& aKey, Entry aEntry,
+                                       void* aArg);
+  void SetTimer();
+  void ShutdownTimer();
+  void NotifyObservers(const char* aTopic);
+
+  bool GetInternal(const nsCString& aKey, Entry* aEntry, DataStorageType aType,
+                   const MutexAutoLock& aProofOfLock);
+  nsresult PutInternal(const nsCString& aKey, Entry& aEntry,
+                       DataStorageType aType,
+                       const MutexAutoLock& aProofOfLock);
+  void MaybeEvictOneEntry(DataStorageType aType,
+                          const MutexAutoLock& aProofOfLock);
+  DataStorageTable& GetTableForType(DataStorageType aType,
+                                    const MutexAutoLock& aProofOfLock);
+
+  Mutex mMutex; // This mutex protects access to the following members:
+  DataStorageTable  mPersistentDataTable;
+  DataStorageTable  mTemporaryDataTable;
+  DataStorageTable  mPrivateDataTable;
+  nsCOMPtr<nsIThread> mWorkerThread;
+  nsCOMPtr<nsIFile> mBackingFile;
+  nsCOMPtr<nsITimer> mTimer; // All uses after init must be on the worker thread
+  uint32_t mTimerDelay; // in milliseconds
+  bool mPendingWrite; // true if a write is needed but hasn't been dispatched
+  bool mShuttingDown;
+  // (End list of members protected by mMutex)
+
+  Monitor mReadyMonitor; // Do not acquire this at the same time as mMutex.
+  bool mReady; // Indicates that saved data has been read and Get can proceed.
+
+  const nsString mFilename;
+};
+
+}; // namespace mozilla
+
+#endif // mozilla_DataStorage_h
--- a/security/manager/boot/src/moz.build
+++ b/security/manager/boot/src/moz.build
@@ -1,15 +1,20 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
+EXPORTS.mozilla += [
+    'DataStorage.h',
+]
+
 UNIFIED_SOURCES += [
+    'DataStorage.cpp',
     'nsBOOTModule.cpp',
     'nsEntropyCollector.cpp',
     'nsSecurityHeaderParser.cpp',
     'nsSecurityWarningDialogs.cpp',
     'nsSiteSecurityService.cpp',
     'PublicKeyPinningService.cpp',
 ]
 
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/gtest/DataStorageTest.cpp
@@ -0,0 +1,224 @@
+/* -*- 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 "gtest/gtest.h"
+
+#include "mozilla/DataStorage.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsNetUtil.h"
+#include "nsPrintfCString.h"
+#include "nsStreamUtils.h"
+
+using namespace mozilla;
+
+class DataStorageTest : public ::testing::Test
+{
+protected:
+  virtual void SetUp()
+  {
+    const ::testing::TestInfo* const testInfo =
+      ::testing::UnitTest::GetInstance()->current_test_info();
+    NS_ConvertUTF8toUTF16 testName(testInfo->name());
+    storage = new DataStorage(testName);
+    storage->Init(dataWillPersist);
+  }
+
+  nsRefPtr<DataStorage> storage;
+  bool dataWillPersist;
+};
+
+NS_NAMED_LITERAL_CSTRING(testKey, "test");
+NS_NAMED_LITERAL_CSTRING(testValue, "value");
+NS_NAMED_LITERAL_CSTRING(privateTestValue, "private");
+
+TEST_F(DataStorageTest, GetPutRemove)
+{
+  EXPECT_TRUE(dataWillPersist);
+
+  // Test Put/Get on Persistent data
+  EXPECT_EQ(NS_OK, storage->Put(testKey, testValue, DataStorage_Persistent));
+  // Don't re-use testKey / testValue here, to make sure that this works as
+  // expected with objects that have the same semantic value but are not
+  // literally the same object.
+  nsCString result = storage->Get(NS_LITERAL_CSTRING("test"),
+                                  DataStorage_Persistent);
+  EXPECT_STREQ("value", result.get());
+
+  // Get on Temporary/Private data with the same key should give nothing
+  result = storage->Get(testKey, DataStorage_Temporary);
+  EXPECT_TRUE(result.IsEmpty());
+  result = storage->Get(testKey, DataStorage_Private);
+  EXPECT_TRUE(result.IsEmpty());
+
+  // Put with Temporary/Private data shouldn't affect Persistent data
+  NS_NAMED_LITERAL_CSTRING(temporaryTestValue, "temporary");
+  EXPECT_EQ(NS_OK, storage->Put(testKey, temporaryTestValue,
+                                DataStorage_Temporary));
+  EXPECT_EQ(NS_OK, storage->Put(testKey, privateTestValue,
+                                DataStorage_Private));
+  result = storage->Get(testKey, DataStorage_Temporary);
+  EXPECT_STREQ("temporary", result.get());
+  result = storage->Get(testKey, DataStorage_Private);
+  EXPECT_STREQ("private", result.get());
+  result = storage->Get(testKey, DataStorage_Persistent);
+  EXPECT_STREQ("value", result.get());
+
+  // Put of a previously-present key overwrites it (if of the same type)
+  NS_NAMED_LITERAL_CSTRING(newValue, "new");
+  EXPECT_EQ(NS_OK, storage->Put(testKey, newValue, DataStorage_Persistent));
+  result = storage->Get(testKey, DataStorage_Persistent);
+  EXPECT_STREQ("new", result.get());
+
+  // Removal should work
+  storage->Remove(testKey, DataStorage_Temporary);
+  result = storage->Get(testKey, DataStorage_Temporary);
+  EXPECT_TRUE(result.IsEmpty());
+  // But removing one type shouldn't affect the others
+  result = storage->Get(testKey, DataStorage_Private);
+  EXPECT_STREQ("private", result.get());
+  result = storage->Get(testKey, DataStorage_Persistent);
+  EXPECT_STREQ("new", result.get());
+  // Test removing the other types as well
+  storage->Remove(testKey, DataStorage_Private);
+  result = storage->Get(testKey, DataStorage_Private);
+  EXPECT_TRUE(result.IsEmpty());
+  storage->Remove(testKey, DataStorage_Persistent);
+  result = storage->Get(testKey, DataStorage_Persistent);
+  EXPECT_TRUE(result.IsEmpty());
+}
+
+TEST_F(DataStorageTest, InputValidation)
+{
+  EXPECT_TRUE(dataWillPersist);
+
+  // Keys may not have tabs or newlines
+  EXPECT_EQ(NS_ERROR_INVALID_ARG,
+            storage->Put(NS_LITERAL_CSTRING("key\thas tab"), testValue,
+                   DataStorage_Persistent));
+  nsCString result = storage->Get(NS_LITERAL_CSTRING("key\thas tab"),
+                            DataStorage_Persistent);
+  EXPECT_TRUE(result.IsEmpty());
+  EXPECT_EQ(NS_ERROR_INVALID_ARG,
+            storage->Put(NS_LITERAL_CSTRING("key has\nnewline"), testValue,
+                   DataStorage_Persistent));
+  result = storage->Get(NS_LITERAL_CSTRING("keyhas\nnewline"),
+                  DataStorage_Persistent);
+  EXPECT_TRUE(result.IsEmpty());
+  // Values may not have newlines
+  EXPECT_EQ(NS_ERROR_INVALID_ARG,
+            storage->Put(testKey, NS_LITERAL_CSTRING("value\nhas newline"),
+                   DataStorage_Persistent));
+  result = storage->Get(testKey, DataStorage_Persistent);
+  // Values may have tabs
+  EXPECT_TRUE(result.IsEmpty());
+  EXPECT_EQ(NS_OK, storage->Put(testKey,
+                                NS_LITERAL_CSTRING("val\thas tab; this is ok"),
+                                DataStorage_Persistent));
+  result = storage->Get(testKey, DataStorage_Persistent);
+  EXPECT_STREQ("val\thas tab; this is ok", result.get());
+
+  nsCString longKey("a");
+  for (int i = 0; i < 8; i++) {
+    longKey.Append(longKey);
+  }
+  // A key of length 256 will work
+  EXPECT_EQ(NS_OK, storage->Put(longKey, testValue, DataStorage_Persistent));
+  result = storage->Get(longKey, DataStorage_Persistent);
+  EXPECT_STREQ("value", result.get());
+  longKey.Append("a");
+  // A key longer than that will not work
+  EXPECT_EQ(NS_ERROR_INVALID_ARG,
+            storage->Put(longKey, testValue, DataStorage_Persistent));
+  result = storage->Get(longKey, DataStorage_Persistent);
+  EXPECT_TRUE(result.IsEmpty());
+
+  nsCString longValue("a");
+  for (int i = 0; i < 10; i++) {
+    longValue.Append(longValue);
+  }
+  // A value of length 1024 will work
+  EXPECT_EQ(NS_OK, storage->Put(testKey, longValue, DataStorage_Persistent));
+  result = storage->Get(testKey, DataStorage_Persistent);
+  EXPECT_STREQ(longValue.get(), result.get());
+  longValue.Append("a");
+  // A value longer than that will not work
+  storage->Remove(testKey, DataStorage_Persistent);
+  EXPECT_EQ(NS_ERROR_INVALID_ARG,
+            storage->Put(testKey, longValue, DataStorage_Persistent));
+  result = storage->Get(testKey, DataStorage_Persistent);
+  EXPECT_TRUE(result.IsEmpty());
+}
+
+TEST_F(DataStorageTest, Eviction)
+{
+  EXPECT_TRUE(dataWillPersist);
+
+  // Eviction is on a per-table basis. Tables shouldn't affect each other.
+  EXPECT_EQ(NS_OK, storage->Put(testKey, testValue, DataStorage_Persistent));
+  for (int i = 0; i < 1025; i++) {
+    EXPECT_EQ(NS_OK, storage->Put(nsPrintfCString("%d", i),
+                                  nsPrintfCString("%d", i),
+                                  DataStorage_Temporary));
+    nsCString result = storage->Get(nsPrintfCString("%d", i),
+                                    DataStorage_Temporary);
+    EXPECT_STREQ(nsPrintfCString("%d", i).get(), result.get());
+  }
+  // We don't know which entry got evicted, but we can count them.
+  int entries = 0;
+  for (int i = 0; i < 1025; i++) {
+    nsCString result = storage->Get(nsPrintfCString("%d", i),
+                                    DataStorage_Temporary);
+    if (!result.IsEmpty()) {
+      entries++;
+    }
+  }
+  EXPECT_EQ(entries, 1024);
+  nsCString result = storage->Get(testKey, DataStorage_Persistent);
+  EXPECT_STREQ("value", result.get());
+}
+
+TEST_F(DataStorageTest, ClearPrivateData)
+{
+  EXPECT_TRUE(dataWillPersist);
+
+  EXPECT_EQ(NS_OK, storage->Put(testKey, privateTestValue,
+                                DataStorage_Private));
+  nsCString result = storage->Get(testKey, DataStorage_Private);
+  EXPECT_STREQ("private", result.get());
+  storage->Observe(nullptr, "last-pb-context-exited", nullptr);
+  result = storage->Get(testKey, DataStorage_Private);
+  EXPECT_TRUE(result.IsEmpty());
+}
+
+TEST_F(DataStorageTest, Shutdown)
+{
+  EXPECT_TRUE(dataWillPersist);
+
+  EXPECT_EQ(NS_OK, storage->Put(testKey, testValue, DataStorage_Persistent));
+  nsCString result = storage->Get(testKey, DataStorage_Persistent);
+  EXPECT_STREQ("value", result.get());
+  // Get "now" (in days) close to when the data was last touched, so we won't
+  // get intermittent failures with the day not matching.
+  int64_t microsecondsPerDay = 24 * 60 * 60 * int64_t(PR_USEC_PER_SEC);
+  int32_t nowInDays = int32_t(PR_Now() / microsecondsPerDay);
+  storage->Observe(nullptr, "profile-before-change", nullptr);
+  nsCOMPtr<nsIFile> backingFile;
+  EXPECT_EQ(NS_OK, NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+                                          getter_AddRefs(backingFile)));
+  const ::testing::TestInfo* const testInfo =
+    ::testing::UnitTest::GetInstance()->current_test_info();
+  NS_ConvertUTF8toUTF16 testName(testInfo->name());
+  EXPECT_EQ(NS_OK, backingFile->Append(testName));
+  nsCOMPtr<nsIInputStream> fileInputStream;
+  EXPECT_EQ(NS_OK, NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream),
+                                              backingFile));
+  nsCString data;
+  EXPECT_EQ(NS_OK, NS_ConsumeStream(fileInputStream, UINT32_MAX, data));
+  // The data will be of the form 'test\t0\t<days since the epoch>\tvalue'
+  EXPECT_STREQ(nsPrintfCString("test\t0\t%d\tvalue\n", nowInDays).get(),
+               data.get());
+}
--- a/security/manager/ssl/tests/gtest/moz.build
+++ b/security/manager/ssl/tests/gtest/moz.build
@@ -1,15 +1,16 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 SOURCES += [
+    'DataStorageTest.cpp',
     'OCSPCacheTest.cpp',
     'TLSIntoleranceTest.cpp',
 ]
 
 LOCAL_INCLUDES += [
     '../../../../certverifier',
     '../../../../pkix/include',
     '../../../../pkix/test/lib',