Bug 1840135 - introduce rust implementation of nsIDataStorage r=nkulatova,nika,mak

Differential Revision: https://phabricator.services.mozilla.com/D181904
This commit is contained in:
Dana Keeler 2023-09-05 17:14:51 +00:00
parent 83b8777249
commit 58b11864ed
30 changed files with 1706 additions and 1551 deletions

18
Cargo.lock generated
View File

@ -1211,6 +1211,23 @@ dependencies = [
"nsstring",
]
[[package]]
name = "data_storage"
version = "0.0.1"
dependencies = [
"byteorder",
"cstr",
"firefox-on-glean",
"log",
"malloc_size_of_derive",
"moz_task",
"nserror",
"nsstring",
"thin-vec",
"wr_malloc_size_of",
"xpcom",
]
[[package]]
name = "dbus"
version = "0.6.5"
@ -2136,6 +2153,7 @@ dependencies = [
"cubeb-sys",
"dap_ffi",
"data-encoding-ffi",
"data_storage",
"detect_win32k_conflicts",
"dom",
"encoding_glue",

View File

@ -1,889 +0,0 @@
/* -*- 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/AppShutdown.h"
#include "mozilla/Assertions.h"
#include "mozilla/ClearOnShutdown.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 "nsIObserverService.h"
#include "nsISafeOutputStream.h"
#include "nsISerialEventTarget.h"
#include "nsIThread.h"
#include "nsITimer.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 {
NS_IMPL_ISUPPORTS(DataStorageManager, nsIDataStorageManager)
NS_IMPL_ISUPPORTS(DataStorageItem, nsIDataStorageItem)
NS_IMPL_ISUPPORTS(DataStorage, nsIDataStorage, nsIMemoryReporter, nsIObserver)
NS_IMETHODIMP
DataStorageManager::Get(nsIDataStorageManager::DataStorage aDataStorage,
nsIDataStorage** aResult) {
if (!NS_IsMainThread()) {
return NS_ERROR_NOT_SAME_THREAD;
}
nsAutoString filename;
switch (aDataStorage) {
case nsIDataStorageManager::AlternateServices:
if (mAlternateServicesCreated) {
return NS_ERROR_ALREADY_INITIALIZED;
}
mAlternateServicesCreated = true;
filename.Assign(u"AlternateServices.txt"_ns);
break;
case nsIDataStorageManager::ClientAuthRememberList:
if (mClientAuthRememberListCreated) {
return NS_ERROR_ALREADY_INITIALIZED;
}
mClientAuthRememberListCreated = true;
filename.Assign(u"ClientAuthRememberList.txt"_ns);
break;
case nsIDataStorageManager::SiteSecurityServiceState:
if (mSiteSecurityServiceStateCreated) {
return NS_ERROR_ALREADY_INITIALIZED;
}
mSiteSecurityServiceStateCreated = true;
filename.Assign(u"SiteSecurityServiceState.txt"_ns);
break;
default:
return NS_ERROR_INVALID_ARG;
}
RefPtr<mozilla::DataStorage> dataStorage(new mozilla::DataStorage(filename));
nsresult rv = dataStorage->Init();
if (NS_FAILED(rv)) {
return rv;
}
nsCOMPtr<nsIMemoryReporter> memoryReporter(dataStorage.get());
RegisterStrongMemoryReporter(memoryReporter);
*aResult = dataStorage.forget().take();
return NS_OK;
}
NS_IMETHODIMP
DataStorageItem::GetKey(nsACString& aKey) {
aKey.Assign(key);
return NS_OK;
}
NS_IMETHODIMP
DataStorageItem::GetValue(nsACString& aValue) {
aValue.Assign(value);
return NS_OK;
}
NS_IMETHODIMP
DataStorageItem::GetType(nsIDataStorage::DataType* aType) {
if (!aType) {
return NS_ERROR_INVALID_ARG;
}
*aType = type;
return NS_OK;
}
DataStorage::DataStorage(const nsString& aFilename)
: mMutex("DataStorage::mMutex"),
mPendingWrite(false),
mTimerArmed(false),
mShuttingDown(false),
mInitCalled(false),
mReadyMonitor("DataStorage::mReadyMonitor"),
mReady(false),
mFilename(aFilename) {}
nsresult DataStorage::Init() {
// 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 (!XRE_IsParentProcess()) {
MOZ_ASSERT_UNREACHABLE("DataStorage used in non-parent process");
return NS_ERROR_NOT_AVAILABLE;
}
if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
// 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;
nsCOMPtr<nsISerialEventTarget> target;
nsresult rv = NS_CreateBackgroundTaskQueue(
"DataStorage::mBackgroundTaskQueue", getter_AddRefs(target));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
mBackgroundTaskQueue = TaskQueue::Create(target.forget(), "PSM DataStorage");
// For test purposes, we can set the write timer to be very fast.
mTimerDelayMS = Preferences::GetInt("test.datastorage.write_timer_ms",
sDataStorageDefaultTimerDelay);
rv = AsyncReadData(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.
// 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);
// This is a backstop for xpcshell and other cases where
// profile-before-change might not get sent.
os->AddObserver(this, "xpcom-shutdown-threads", false);
// On mobile, if the app is backgrounded, it may be killed. Observe this
// notification to kick off an asynchronous write to avoid losing data.
os->AddObserver(this, "application-background", false);
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_NOT_FOUND)) {
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.
mDataStorage->mPersistentDataTable.LookupOrInsert(key,
std::move(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 nsACString& aKey,
const nsACString& 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*/) {
mMutex.AssertCurrentThreadOwns();
// 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;
}
NS_IMETHODIMP
DataStorage::IsReady(bool* aReady) {
if (!aReady) {
return NS_ERROR_INVALID_ARG;
}
MonitorAutoLock readyLock(mReadyMonitor);
*aReady = mReady;
return NS_OK;
}
void DataStorage::WaitForReady() {
MOZ_DIAGNOSTIC_ASSERT(mInitCalled, "Waiting before Init() has been called?");
MonitorAutoLock readyLock(mReadyMonitor);
while (!mReady) {
readyLock.Wait();
}
MOZ_ASSERT(mReady);
}
NS_IMETHODIMP
DataStorage::Get(const nsACString& aKey, nsIDataStorage::DataType aType,
nsACString& aValue) {
WaitForReady();
MutexAutoLock lock(mMutex);
Entry entry;
bool foundValue = GetInternal(aKey, &entry, aType, lock);
if (!foundValue) {
return NS_ERROR_NOT_AVAILABLE;
}
// If we're here, we found a value. Maybe update its score.
if (entry.UpdateScore()) {
PutInternal(aKey, entry, aType, lock);
}
aValue.Assign(entry.mValue);
return NS_OK;
}
bool DataStorage::GetInternal(const nsACString& aKey, Entry* aEntry,
nsIDataStorage::DataType aType,
const MutexAutoLock& aProofOfLock) {
DataStorageTable& table = GetTableForType(aType, aProofOfLock);
bool foundValue = table.Get(aKey, aEntry);
return foundValue;
}
DataStorage::DataStorageTable& DataStorage::GetTableForType(
nsIDataStorage::DataType aType, const MutexAutoLock& /*aProofOfLock*/) {
switch (aType) {
case nsIDataStorage::DataType::Persistent:
return mPersistentDataTable;
case nsIDataStorage::DataType::Temporary:
return mTemporaryDataTable;
case nsIDataStorage::DataType::Private:
return mPrivateDataTable;
}
MOZ_CRASH("given bad DataStorage storage type");
}
void DataStorage::ReadAllFromTable(nsIDataStorage::DataType aType,
nsTArray<RefPtr<nsIDataStorageItem>>& aItems,
const MutexAutoLock& aProofOfLock) {
for (auto iter = GetTableForType(aType, aProofOfLock).Iter(); !iter.Done();
iter.Next()) {
nsCOMPtr<nsIDataStorageItem> item(
new DataStorageItem(iter.Key(), iter.Data().mValue, aType));
aItems.AppendElement(item);
}
}
NS_IMETHODIMP
DataStorage::GetAll(nsTArray<RefPtr<nsIDataStorageItem>>& aItems) {
WaitForReady();
MutexAutoLock lock(mMutex);
aItems.SetCapacity(mPersistentDataTable.Count() +
mTemporaryDataTable.Count() + mPrivateDataTable.Count());
ReadAllFromTable(nsIDataStorage::DataType::Persistent, aItems, lock);
ReadAllFromTable(nsIDataStorage::DataType::Temporary, aItems, lock);
ReadAllFromTable(nsIDataStorage::DataType::Private, aItems, lock);
return NS_OK;
}
// 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(nsIDataStorage::DataType 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);
}
}
NS_IMETHODIMP
DataStorage::Put(const nsACString& aKey, const nsACString& aValue,
nsIDataStorage::DataType 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;
return PutInternal(aKey, entry, aType, lock);
}
nsresult DataStorage::PutInternal(const nsACString& aKey, Entry& aEntry,
nsIDataStorage::DataType aType,
const MutexAutoLock& aProofOfLock) {
mMutex.AssertCurrentThreadOwns();
DataStorageTable& table = GetTableForType(aType, aProofOfLock);
table.InsertOrUpdate(aKey, aEntry);
if (aType == nsIDataStorage::DataType::Persistent) {
mPendingWrite = true;
ArmTimer(aProofOfLock);
}
return NS_OK;
}
NS_IMETHODIMP
DataStorage::Remove(const nsACString& aKey, nsIDataStorage::DataType aType) {
WaitForReady();
MutexAutoLock lock(mMutex);
DataStorageTable& table = GetTableForType(aType, lock);
table.Remove(aKey);
if (aType == nsIDataStorage::DataType::Persistent) {
mPendingWrite = true;
ArmTimer(lock);
}
return NS_OK;
}
class DataStorage::Writer final : public Runnable {
public:
Writer(nsCString& aData, DataStorage* aDataStorage)
: Runnable("DataStorage::Writer"),
mData(aData),
mDataStorage(aDataStorage) {}
protected:
NS_DECL_NSIRUNNABLE
nsCString mData;
RefPtr<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_NewSafeLocalFileOutputStream(getter_AddRefs(outputStream), file,
PR_CREATE_FILE | PR_TRUNCATE | PR_WRONLY);
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;
}
nsCOMPtr<nsISafeOutputStream> safeOutputStream =
do_QueryInterface(outputStream);
if (!safeOutputStream) {
return NS_ERROR_FAILURE;
}
rv = safeOutputStream->Finish();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// 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*/) {
mMutex.AssertCurrentThreadOwns();
if (!mPendingWrite || mShuttingDown || !mBackingFile) {
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 (mTimerArmed) {
rv = mTimer->Cancel();
Unused << NS_WARN_IF(NS_FAILED(rv));
mTimerArmed = false;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
NS_IMETHODIMP
DataStorage::Clear() {
WaitForReady();
MutexAutoLock lock(mMutex);
mPersistentDataTable.Clear();
mTemporaryDataTable.Clear();
mPrivateDataTable.Clear();
mPendingWrite = true;
// 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.
return AsyncWriteData(lock);
}
/* static */
void DataStorage::TimerCallback(nsITimer* aTimer, void* aClosure) {
RefPtr<DataStorage> aDataStorage = (DataStorage*)aClosure;
MutexAutoLock lock(aDataStorage->mMutex);
aDataStorage->mTimerArmed = false;
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::ArmTimer(const MutexAutoLock& /*aProofOfLock*/) {
mMutex.AssertCurrentThreadOwns();
if (mTimerArmed) {
return;
}
if (!mTimer) {
mTimer = NS_NewTimer(mBackgroundTaskQueue);
if (NS_WARN_IF(!mTimer)) {
return;
}
}
nsresult rv = mTimer->InitWithNamedFuncCallback(
DataStorage::TimerCallback, this, mTimerDelayMS, nsITimer::TYPE_ONE_SHOT,
"DataStorageTimer");
Unused << NS_WARN_IF(NS_FAILED(rv));
mTimerArmed = true;
}
void DataStorage::ShutdownTimer() {
MOZ_ASSERT(NS_IsMainThread());
if (mTimer) {
nsresult rv = mTimer->Cancel();
Unused << NS_WARN_IF(NS_FAILED(rv));
mTimer = nullptr;
}
}
//------------------------------------------------------------
// DataStorage::nsIMemoryReporter
//------------------------------------------------------------
NS_IMETHODIMP
DataStorage::CollectReports(nsIHandleReportCallback* aHandleReport,
nsISupports* aData, bool aAnonymize) {
MutexAutoLock lock(mMutex);
size_t sizeOfExcludingThis =
mPersistentDataTable.ShallowSizeOfExcludingThis(MallocSizeOf) +
mTemporaryDataTable.ShallowSizeOfExcludingThis(MallocSizeOf) +
mPrivateDataTable.ShallowSizeOfExcludingThis(MallocSizeOf) +
mFilename.SizeOfExcludingThisIfUnshared(MallocSizeOf);
size_t amount = MallocSizeOf(this) + sizeOfExcludingThis;
nsPrintfCString path("explicit/data-storage/%s",
NS_ConvertUTF16toUTF8(mFilename).get());
return aHandleReport->Callback(""_ns, path, KIND_HEAP, UNITS_BYTES, amount,
"Memory used by PSM data storage cache."_ns,
aData);
}
//------------------------------------------------------------
// 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 (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();
}
// On mobile, if the app is backgrounded, it may be killed. Kick off an
// asynchronous write to avoid losing data.
if (strcmp(aTopic, "application-background") == 0) {
MutexAutoLock lock(mMutex);
if (!mShuttingDown) {
nsresult rv = AsyncWriteData(lock);
Unused << NS_WARN_IF(NS_FAILED(rv));
}
}
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

View File

@ -1,205 +0,0 @@
/* -*- 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/Atomics.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/Monitor.h"
#include "mozilla/Mutex.h"
#include "mozilla/StaticPtr.h"
#include "nsCOMPtr.h"
#include "nsTHashMap.h"
#include "nsIDataStorage.h"
#include "nsIMemoryReporter.h"
#include "nsIObserver.h"
#include "nsITimer.h"
#include "nsRefPtrHashtable.h"
#include "nsString.h"
namespace mozilla {
class TaskQueue;
/**
* 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.
* - A timer periodically fires on a background thread that checks if any
* persistent data has changed, and if so writes 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 eventually be
* written when the timer fires again, and eventually another
* "data-storage-written" event will be sent.
* - When a DataStorage instance observes the topic "profile-before-change" in
* anticipation of shutdown, all persistent data for that DataStorage is
* written to the backing file (this blocks the main thread). In the process
* of doing this, the background serial event target responsible for these
* writes is then shut down to prevent further writes to that file (the
* background timer is also cancelled when this happens).
* If "profile-before-change" is not observed, this happens upon observing
* "xpcom-shutdown-threads".
* - 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)
*/
class DataStorageManager final : public nsIDataStorageManager {
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIDATASTORAGEMANAGER
private:
~DataStorageManager() = default;
bool mAlternateServicesCreated = false;
bool mClientAuthRememberListCreated = false;
bool mSiteSecurityServiceStateCreated = false;
};
class DataStorageItem final : public nsIDataStorageItem {
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIDATASTORAGEITEM
DataStorageItem(const nsACString& aKey, const nsACString& aValue,
nsIDataStorage::DataType aType)
: key(aKey), value(aValue), type(aType) {}
private:
~DataStorageItem() = default;
nsAutoCString key;
nsAutoCString value;
nsIDataStorage::DataType type;
};
class DataStorage final : public nsIDataStorage,
public nsIMemoryReporter,
public nsIObserver {
MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIDATASTORAGE
NS_DECL_NSIMEMORYREPORTER
NS_DECL_NSIOBSERVER
explicit DataStorage(const nsString& aFilename);
// Initializes the DataStorage. Must be called before using.
nsresult Init();
private:
~DataStorage() = default;
void ArmTimer(const MutexAutoLock& aProofOfLock);
void ShutdownTimer();
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 nsTHashMap<nsCStringHashKey, Entry> DataStorageTable;
typedef nsRefPtrHashtable<nsStringHashKey, DataStorage> DataStorages;
void WaitForReady();
nsresult AsyncWriteData(const MutexAutoLock& aProofOfLock);
nsresult AsyncReadData(const MutexAutoLock& aProofOfLock);
static nsresult ValidateKeyAndValue(const nsACString& aKey,
const nsACString& aValue);
static void TimerCallback(nsITimer* aTimer, void* aClosure);
void NotifyObservers(const char* aTopic);
bool GetInternal(const nsACString& aKey, Entry* aEntry,
nsIDataStorage::DataType aType,
const MutexAutoLock& aProofOfLock);
nsresult PutInternal(const nsACString& aKey, Entry& aEntry,
nsIDataStorage::DataType aType,
const MutexAutoLock& aProofOfLock);
void MaybeEvictOneEntry(nsIDataStorage::DataType aType,
const MutexAutoLock& aProofOfLock);
DataStorageTable& GetTableForType(nsIDataStorage::DataType aType,
const MutexAutoLock& aProofOfLock);
void ReadAllFromTable(nsIDataStorage::DataType aType,
nsTArray<RefPtr<nsIDataStorageItem>>& aItems,
const MutexAutoLock& aProofOfLock);
Mutex mMutex; // This mutex protects access to the following members:
DataStorageTable mPersistentDataTable MOZ_GUARDED_BY(mMutex);
DataStorageTable mTemporaryDataTable MOZ_GUARDED_BY(mMutex);
DataStorageTable mPrivateDataTable MOZ_GUARDED_BY(mMutex);
nsCOMPtr<nsIFile> mBackingFile MOZ_GUARDED_BY(mMutex);
bool mPendingWrite MOZ_GUARDED_BY(
mMutex); // true if a write is needed but hasn't been dispatched
bool mTimerArmed MOZ_GUARDED_BY(mMutex);
bool mShuttingDown MOZ_GUARDED_BY(mMutex);
RefPtr<TaskQueue> mBackgroundTaskQueue MOZ_GUARDED_BY(mMutex);
// (End list of members protected by mMutex)
nsCOMPtr<nsITimer> mTimer;
mozilla::Atomic<bool> mInitCalled; // Indicates that Init() has been called.
uint32_t mTimerDelayMS;
Monitor mReadyMonitor; // Do not acquire this at the same time as mMutex.
bool mReady MOZ_GUARDED_BY(mReadyMonitor); // Indicates that saved data has
// been read and Get can proceed.
const nsString mFilename;
static StaticAutoPtr<DataStorages> sDataStorages;
};
} // namespace mozilla
#endif // mozilla_DataStorage_h

View File

@ -0,0 +1,68 @@
/* -*- 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 "DataStorageManager.h"
#include "MainThreadUtils.h"
#include "nsIMemoryReporter.h"
#include "nsString.h"
using VoidPtrToSizeFn = uintptr_t (*)(const void* ptr);
extern "C" nsresult make_data_storage(const nsAString* basename,
size_t valueLength,
VoidPtrToSizeFn sizeOfOp,
VoidPtrToSizeFn enclosingSizeOfOp,
nsIDataStorage** result);
MOZ_DEFINE_MALLOC_SIZE_OF(DataStorageMallocSizeOf)
MOZ_DEFINE_MALLOC_ENCLOSING_SIZE_OF(DataStorageMallocEnclosingSizeOf)
namespace mozilla {
NS_IMPL_ISUPPORTS(DataStorageManager, nsIDataStorageManager)
NS_IMETHODIMP
DataStorageManager::Get(nsIDataStorageManager::DataStorage aDataStorage,
nsIDataStorage** aResult) {
if (!NS_IsMainThread()) {
return NS_ERROR_NOT_SAME_THREAD;
}
nsAutoString filename;
size_t valueLength = 1024;
switch (aDataStorage) {
case nsIDataStorageManager::AlternateServices:
if (mAlternateServicesCreated) {
return NS_ERROR_ALREADY_INITIALIZED;
}
mAlternateServicesCreated = true;
filename.Assign(u"AlternateServices"_ns);
break;
case nsIDataStorageManager::ClientAuthRememberList:
if (mClientAuthRememberListCreated) {
return NS_ERROR_ALREADY_INITIALIZED;
}
mClientAuthRememberListCreated = true;
filename.Assign(u"ClientAuthRememberList"_ns);
break;
case nsIDataStorageManager::SiteSecurityServiceState:
if (mSiteSecurityServiceStateCreated) {
return NS_ERROR_ALREADY_INITIALIZED;
}
mSiteSecurityServiceStateCreated = true;
filename.Assign(u"SiteSecurityServiceState"_ns);
// For most nsIDataStorage use cases, values can be quite long (1024
// bytes by default). For HSTS, much less information is stored, so save
// space by limiting values to 24 bytes.
valueLength = 24;
break;
default:
return NS_ERROR_INVALID_ARG;
}
return make_data_storage(&filename, valueLength, &DataStorageMallocSizeOf,
&DataStorageMallocEnclosingSizeOf, aResult);
}
} // namespace mozilla

View File

@ -0,0 +1,29 @@
/* -*- 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_DataStorageManager_h
#define mozilla_DataStorageManager_h
#include "nsIDataStorage.h"
namespace mozilla {
class DataStorageManager final : public nsIDataStorageManager {
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIDATASTORAGEMANAGER
private:
~DataStorageManager() = default;
bool mAlternateServicesCreated = false;
bool mClientAuthRememberListCreated = false;
bool mSiteSecurityServiceStateCreated = false;
};
} // namespace mozilla
#endif // mozilla_DataStorageManager_h

View File

@ -144,7 +144,7 @@ Classes = [
'cid': '{71b49926-fd4e-43e2-ab8d-d9b049413c0b}',
'contract_ids': ['@mozilla.org/security/datastoragemanager;1'],
'type': 'mozilla::DataStorageManager',
'headers': ['/security/manager/ssl//DataStorage.h'],
'headers': ['/security/manager/ssl/DataStorageManager.h'],
},
{
'cid': '{d7d2490d-2640-411b-9f09-a538803c11ee}',

View File

@ -0,0 +1,18 @@
[package]
name = "data_storage"
version = "0.0.1"
edition = "2021"
license = "MPL-2.0"
[dependencies]
byteorder = "1"
cstr = "0.2"
firefox-on-glean = { path = "../../../../toolkit/components/glean/api" }
log = "0.4"
malloc_size_of_derive = { path = "../../../../xpcom/rust/malloc_size_of_derive" }
moz_task = { path = "../../../../xpcom/rust/moz_task" }
nserror = { path = "../../../../xpcom/rust/nserror" }
nsstring = { path = "../../../../xpcom/rust/nsstring" }
thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
wr_malloc_size_of = { path = "../../../../gfx/wr/wr_malloc_size_of" }
xpcom = { path = "../../../../xpcom/rust/xpcom" }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,47 @@
# 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/.
# Adding a new metric? We have docs for that!
# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
---
$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
$tags:
- 'Core :: Security: PSM'
data_storage:
entries:
type: labeled_counter
description:
Counts the number of entries stored in each nsIDataStorage.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1840135
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1840135
data_sensitivity:
- interaction
notification_emails:
- dkeeler@mozilla.com
expires: never
labels:
- AlternateServices
- ClientAuthRememberList
- SiteSecurityServiceState
migration:
type: labeled_boolean
description:
Indicates whether or not migration was successful for each nsIDataStorage.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1840135
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1840135
data_sensitivity:
- interaction
notification_emails:
- dkeeler@mozilla.com
expires: 124
labels:
- AlternateServices
- ClientAuthRememberList
- SiteSecurityServiceState

View File

@ -105,7 +105,7 @@ UNIFIED_SOURCES += [
"CommonSocketControl.cpp",
"ContentSignatureVerifier.cpp",
"CryptoTask.cpp",
"DataStorage.cpp",
"DataStorageManager.cpp",
"EnterpriseRoots.cpp",
"IPCClientCertsChild.cpp",
"IPCClientCertsParent.cpp",

View File

@ -11,6 +11,10 @@ interface nsIDataStorageItem;
[scriptable, uuid(71b49926-fd4e-43e2-ab8d-d9b049413c0b)]
interface nsIDataStorageManager : nsISupports {
// Because of its specialized nature, nsIDataStorage instances are limited to
// the following pre-defined set. To add a new type of data storage, add an
// entry to the enum and get review from someone on the security and privacy
// engineering team.
cenum DataStorage : 8 {
AlternateServices,
ClientAuthRememberList,
@ -20,39 +24,90 @@ interface nsIDataStorageManager : nsISupports {
nsIDataStorage get(in nsIDataStorageManager_DataStorage dataStorage);
};
/**
* nsIDataStorage is a threadsafe, generic, narrow string-based hash map that
* persists data on disk and additionally handles private (temporary) data.
* The file format is portable across architectures. If used in a context where
* there is no profile directory, data will not be persisted.
*
* Its lifecycle is as follows:
* - Use nsIDataStorageManager to obtain the nsIDataStorage of a particular
* purpose. Its backing file will be read on a background thread.
* - Should the profile directory not be available, (e.g. in xpcshell),
* nsIDataStorage will not read any persistent data.
* - When data in the nsIDataStorage changes, those changes will be written
* to the backing file on a background thread. If the program crashes or is
* closed unexpectedly before the write completes, the changes may be lost.
* If the changes were an update to previously stored data, the original data
* may be lost as well. A checksum associated with each entry helps identify
* incompletely written entries.
* - nsIDataStorage does not support transactions. Each entry is independent of
* the others.
* - When an nsIDataStorage instance observes the topic "profile-before-change"
* in anticipation of shutdown, no more changes will be written to the
* backing file. To ensure no data is lost, users of nsIDataStorage should
* not attempt to change any data after this point.
* If "profile-before-change" is not observed, this happens upon observing
* "xpcom-shutdown-threads".
* - To prevent unbounded memory and disk use, the number of entries in each
* table is limited to 2048. Evictions are handled in by a modified LRU scheme
* (see implementation comments).
* - Note that instances of nsIDataStorage 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 have a length no more than 256.
* - The value have a length no more than 1024 (24 for the site security
* service state).
* The length limits are to prevent unbounded disk and memory usage, and
* nsIDataStorage will throw/return an error if given keys or values of
* excess length.
* Take care when storing data containing bytes that may be 0. When read
* from disk, all trailing 0 bytes from keys and values are stripped.
*/
[scriptable, uuid(fcbb5ec4-7134-4069-91c6-9378eff51e03)]
interface nsIDataStorage : nsISupports {
/**
* Data that is Persistent is saved on disk. Temporary and Private are not
* Data that is Persistent is saved on disk. Data that is Private is not
* saved. Private is meant to only be set and accessed from private contexts.
* It will be cleared upon observing the event "last-pb-context-exited".
*/
cenum DataType : 8 {
Persistent,
Temporary,
Private,
};
// Given a key and a type of data, returns a value. Returns
// NS_ERROR_NOT_AVAILABLE 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.
// NS_ERROR_NOT_AVAILABLE if the key is not present for that type of data.
// This operation may block the current thread until the background task
// reading the backing file from disk has completed.
ACString get(in ACString key, in nsIDataStorage_DataType type);
// Give a key, value, and type of data, adds an entry as appropriate.
// Updates existing entries.
// This operation may block the current thread until the background task
// reading the backing file from disk has completed.
void put(in ACString key, in ACString value, in nsIDataStorage_DataType type);
// Given a key and type of data, removes an entry if present.
// This operation may block the current thread until the background task
// reading the backing file from disk has completed.
void remove(in ACString key, in nsIDataStorage_DataType type);
// Removes all entries of all types of data.
// This operation may block the current thread until the background task
// reading the backing file from disk has completed.
void clear();
// Returns true if this data storage is ready to be used.
// Returns true if this data storage is ready to be used. To avoid blocking
// when calling other nsIDataStorage functions, callers may wish to first
// ensure this function returns true.
bool isReady();
// Read all of the data items.
// This operation may block the current thread until the background task
// reading the backing file from disk has completed.
Array<nsIDataStorageItem> getAll();
};

View File

@ -40,8 +40,9 @@ const isDebugBuild = Cc["@mozilla.org/xpcom/debug;1"].getService(
// The test EV roots are only enabled in debug builds as a security measure.
const gEVExpected = isDebugBuild;
const CLIENT_AUTH_FILE_NAME = "ClientAuthRememberList.txt";
const SSS_STATE_FILE_NAME = "SiteSecurityServiceState.txt";
const CLIENT_AUTH_FILE_NAME = "ClientAuthRememberList.bin";
const SSS_STATE_FILE_NAME = "SiteSecurityServiceState.bin";
const SSS_STATE_OLD_FILE_NAME = "SiteSecurityServiceState.txt";
const CERT_OVERRIDE_FILE_NAME = "cert_override.txt";
const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE;
@ -1194,3 +1195,56 @@ function run_certutil_on_directory(directory, args, expectSuccess = true) {
Assert.equal(process.exitValue, 0, "certutil should succeed");
}
}
function get_data_storage_contents(dataStorageFileName) {
let stateFile = do_get_profile();
stateFile.append(dataStorageFileName);
if (!stateFile.exists()) {
return undefined;
}
return readFile(stateFile);
}
function u16_to_big_endian_bytes(u16) {
Assert.less(u16, 65536);
return [u16 / 256, u16 % 256];
}
// Appends a line to the given data storage file (as an nsIOutputStream).
// score is an integer representing the number of unique days the item has been accessed.
// lastAccessed is the day since the epoch the item was last accessed.
// key and value are strings representing the key and value of the item.
function append_line_to_data_storage_file(
outputStream,
score,
lastAccessed,
key,
value,
valueLength = 24,
useBadChecksum = false
) {
let line = arrayToString(u16_to_big_endian_bytes(score));
line = line + arrayToString(u16_to_big_endian_bytes(lastAccessed));
line = line + key;
let keyPadding = [];
for (let i = 0; i < 256 - key.length; i++) {
keyPadding.push(0);
}
line = line + arrayToString(keyPadding);
line = line + value;
let valuePadding = [];
for (let i = 0; i < valueLength - value.length; i++) {
valuePadding.push(0);
}
line = line + arrayToString(valuePadding);
let checksum = 0;
Assert.equal(line.length % 2, 0);
for (let i = 0; i < line.length; i += 2) {
checksum ^= (line.charCodeAt(i) << 8) + line.charCodeAt(i + 1);
}
line =
arrayToString(
u16_to_big_endian_bytes(useBadChecksum ? ~checksum & 0xffff : checksum)
) + line;
outputStream.write(line, line.length);
}

View File

@ -1,3 +0,0 @@
example.com,C9:65:33:89:EE:DC:4D:05:DA:16:3D:D0:12:61:BC:61:21:51:AF:2B:CC:C6:E1:72:B3:78:23:0F:13:B1:C7:4D, 0 19486 AAAA
example.com,C9:65:33:89:EE:DC:4D:05:DA:16:3D:D0:12:61:BC:61:21:51:AF:2B:CC:C6:E1:72:B3:78:23:0F:13:B1:C7:4D,^partitionKey=%28https%2Cexample.com%29 0 19486 BBBB
example.test,, 0 19486 CCCC

View File

@ -7,11 +7,30 @@
// state file.
function run_test() {
let profile = do_get_profile();
let clientAuthRememberFile = do_get_file(
`test_client_auth_remember_service/${CLIENT_AUTH_FILE_NAME}`
);
clientAuthRememberFile.copyTo(profile, CLIENT_AUTH_FILE_NAME);
let stateFile = do_get_profile();
stateFile.append(CLIENT_AUTH_FILE_NAME);
let outputStream = FileUtils.openFileOutputStream(stateFile);
let keyValuePairs = [
{
key: "example.com,C9:65:33:89:EE:DC:4D:05:DA:16:3D:D0:12:61:BC:61:21:51:AF:2B:CC:C6:E1:72:B3:78:23:0F:13:B1:C7:4D,",
value: "AAAA",
},
{
key: "example.com,C9:65:33:89:EE:DC:4D:05:DA:16:3D:D0:12:61:BC:61:21:51:AF:2B:CC:C6:E1:72:B3:78:23:0F:13:B1:C7:4D,^partitionKey=%28https%2Cexample.com%29",
value: "BBBB",
},
{ key: "example.test,,", value: "CCCC" },
];
for (let keyValuePair of keyValuePairs) {
append_line_to_data_storage_file(
outputStream,
1,
1,
keyValuePair.key,
keyValuePair.value,
1024
);
}
let clientAuthRememberService = Cc[
"@mozilla.org/security/clientAuthRememberService;1"

View File

@ -19,23 +19,13 @@ add_task(function test_data_storage() {
Assert.equal(dataStorage.get("test", Ci.nsIDataStorage.Persistent), "value");
// Test that getting a value with the same key but of a different type throws.
Assert.throws(
() => dataStorage.get("test", Ci.nsIDataStorage.Temporary),
/NS_ERROR_NOT_AVAILABLE/,
"getting a value of a type that hasn't been set yet should throw"
);
Assert.throws(
() => dataStorage.get("test", Ci.nsIDataStorage.Private),
/NS_ERROR_NOT_AVAILABLE/,
"getting a value of a type that hasn't been set yet should throw"
);
// Put with Temporary/Private data shouldn't affect Persistent data
dataStorage.put("test", "temporary", Ci.nsIDataStorage.Temporary);
Assert.equal(
dataStorage.get("test", Ci.nsIDataStorage.Temporary),
"temporary"
);
// Put with Private data shouldn't affect Persistent data
dataStorage.put("test", "private", Ci.nsIDataStorage.Private);
Assert.equal(dataStorage.get("test", Ci.nsIDataStorage.Private), "private");
Assert.equal(dataStorage.get("test", Ci.nsIDataStorage.Persistent), "value");
@ -51,23 +41,79 @@ add_task(function test_data_storage() {
/NS_ERROR_NOT_AVAILABLE/,
"getting a removed value should throw"
);
// But removing one type shouldn't affect the others
Assert.equal(
dataStorage.get("test", Ci.nsIDataStorage.Temporary),
"temporary"
);
// But removing one type shouldn't affect the other
Assert.equal(dataStorage.get("test", Ci.nsIDataStorage.Private), "private");
// Test removing the other types as well
dataStorage.remove("test", Ci.nsIDataStorage.Temporary);
// Test removing the other type as well
dataStorage.remove("test", Ci.nsIDataStorage.Private);
Assert.throws(
() => dataStorage.get("test", Ci.nsIDataStorage.Temporary),
/NS_ERROR_NOT_AVAILABLE/,
"getting a removed value should throw"
);
Assert.throws(
() => dataStorage.get("test", Ci.nsIDataStorage.Private),
/NS_ERROR_NOT_AVAILABLE/,
"getting a removed value should throw"
);
// Saturate the storage tables (there is a maximum of 2048 entries for each
// type of data).
for (let i = 0; i < 2048; i++) {
let padded = i.toString().padStart(4, "0");
dataStorage.put(
`key${padded}`,
`value${padded}`,
Ci.nsIDataStorage.Persistent
);
dataStorage.put(
`key${padded}`,
`value${padded}`,
Ci.nsIDataStorage.Private
);
}
// Ensure the data can be read back.
for (let i = 0; i < 2048; i++) {
let padded = i.toString().padStart(4, "0");
let val = dataStorage.get(`key${padded}`, Ci.nsIDataStorage.Persistent);
Assert.equal(val, `value${padded}`);
val = dataStorage.get(`key${padded}`, Ci.nsIDataStorage.Private);
Assert.equal(val, `value${padded}`);
}
// Remove each entry.
for (let i = 0; i < 2048; i++) {
let padded = i.toString().padStart(4, "0");
dataStorage.remove(`key${padded}`, Ci.nsIDataStorage.Persistent);
dataStorage.remove(`key${padded}`, Ci.nsIDataStorage.Private);
}
// Ensure the entries are not present.
for (let i = 0; i < 2048; i++) {
let padded = i.toString().padStart(4, "0");
Assert.throws(
() => dataStorage.get(`key${padded}`, Ci.nsIDataStorage.Persistent),
/NS_ERROR_NOT_AVAILABLE/,
"getting a removed value should throw"
);
Assert.throws(
() => dataStorage.get(`key${padded}`, Ci.nsIDataStorage.Private),
/NS_ERROR_NOT_AVAILABLE/,
"getting a removed value should throw"
);
}
// Add new entries.
for (let i = 0; i < 2048; i++) {
let padded = i.toString().padStart(5, "1");
dataStorage.put(
`key${padded}`,
`value${padded}`,
Ci.nsIDataStorage.Persistent
);
dataStorage.put(
`key${padded}`,
`value${padded}`,
Ci.nsIDataStorage.Private
);
}
// Ensure each new entry was added.
for (let i = 0; i < 2048; i++) {
let padded = i.toString().padStart(5, "1");
let val = dataStorage.get(`key${padded}`, Ci.nsIDataStorage.Persistent);
Assert.equal(val, `value${padded}`);
val = dataStorage.get(`key${padded}`, Ci.nsIDataStorage.Private);
Assert.equal(val, `value${padded}`);
}
});

View File

@ -5,83 +5,37 @@
// The purpose of this test is to check that a frequently visited site
// will not be evicted over an infrequently visited site.
var gSSService = null;
var gProfileDir = null;
function do_state_written(aSubject, aTopic, aData) {
if (aData == CLIENT_AUTH_FILE_NAME) {
return;
}
equal(aData, SSS_STATE_FILE_NAME);
let stateFile = gProfileDir.clone();
stateFile.append(SSS_STATE_FILE_NAME);
ok(stateFile.exists());
let stateFileContents = readFile(stateFile);
// the last part is removed because it's the empty string after the final \n
let lines = stateFileContents.split("\n").slice(0, -1);
// We can receive multiple data-storage-written events. In particular, we
// may receive one where DataStorage wrote out data before we were done
// processing all of our headers. In this case, the data may not be
// as we expect. We only care about the final one being correct, however,
// so we return and wait for the next event if things aren't as we expect.
// There should be 1024 entries.
if (lines.length != 1024) {
return;
}
let foundLegitSite = false;
for (let line of lines) {
if (line.startsWith("frequentlyused.example.com")) {
foundLegitSite = true;
break;
}
}
ok(foundLegitSite);
do_test_finished();
}
function do_state_read(aSubject, aTopic, aData) {
if (aData == CLIENT_AUTH_FILE_NAME) {
return;
}
equal(aData, SSS_STATE_FILE_NAME);
ok(
gSSService.isSecureURI(
Services.io.newURI("https://frequentlyused.example.com")
)
);
for (let i = 0; i < 2000; i++) {
let uri = Services.io.newURI("http://bad" + i + ".example.com");
gSSService.processHeader(uri, "max-age=1000");
}
do_test_pending();
Services.obs.addObserver(do_state_written, "data-storage-written");
do_test_finished();
}
function run_test() {
Services.prefs.setIntPref("test.datastorage.write_timer_ms", 100);
gProfileDir = do_get_profile();
let stateFile = gProfileDir.clone();
let stateFile = do_get_profile();
stateFile.append(SSS_STATE_FILE_NAME);
// Assuming we're working with a clean slate, the file shouldn't exist
// until we create it.
ok(!stateFile.exists());
let outputStream = FileUtils.openFileOutputStream(stateFile);
let now = new Date().getTime();
let line = "frequentlyused.example.com\t4\t0\t" + (now + 100000) + ",1,0\n";
outputStream.write(line, line.length);
let key = "frequentlyused.example.com";
let value = `${now + 100000},1,0`;
append_line_to_data_storage_file(outputStream, 4, 1000, key, value);
outputStream.close();
Services.obs.addObserver(do_state_read, "data-storage-ready");
do_test_pending();
gSSService = Cc["@mozilla.org/ssservice;1"].getService(
let siteSecurityService = Cc["@mozilla.org/ssservice;1"].getService(
Ci.nsISiteSecurityService
);
notEqual(gSSService, null);
notEqual(siteSecurityService, null);
// isSecureURI blocks until the backing data is read.
ok(
siteSecurityService.isSecureURI(
Services.io.newURI("https://frequentlyused.example.com")
)
);
// The storage limit is currently 2048, so this should cause evictions.
for (let i = 0; i < 3000; i++) {
let uri = Services.io.newURI("http://bad" + i + ".example.com");
siteSecurityService.processHeader(uri, "max-age=1000");
}
// The frequently used entry should not be evicted.
ok(
siteSecurityService.isSecureURI(
Services.io.newURI("https://frequentlyused.example.com")
)
);
}

View File

@ -9,7 +9,7 @@
function run_test() {
let profileDir = do_get_profile();
let stateFile = profileDir.clone();
stateFile.append(SSS_STATE_FILE_NAME);
stateFile.append(SSS_STATE_OLD_FILE_NAME);
// Assuming we're working with a clean slate, the file shouldn't exist
// until we create it.
ok(!stateFile.exists());

View File

@ -6,118 +6,136 @@
// The purpose of this test is to create a site security service state file
// and see that the site security service reads it properly.
var gSSService = null;
function checkStateRead(aSubject, aTopic, aData) {
if (aData == CLIENT_AUTH_FILE_NAME) {
return;
}
equal(aData, SSS_STATE_FILE_NAME);
ok(
!gSSService.isSecureURI(Services.io.newURI("https://expired.example.com"))
);
ok(
gSSService.isSecureURI(Services.io.newURI("https://notexpired.example.com"))
);
ok(
gSSService.isSecureURI(
Services.io.newURI("https://includesubdomains.preloaded.test")
)
);
ok(
!gSSService.isSecureURI(
Services.io.newURI("https://sub.includesubdomains.preloaded.test")
)
);
ok(
gSSService.isSecureURI(
Services.io.newURI("https://incsubdomain.example.com")
)
);
ok(
gSSService.isSecureURI(
Services.io.newURI("https://sub.incsubdomain.example.com")
)
);
ok(
!gSSService.isSecureURI(
Services.io.newURI("https://includesubdomains2.preloaded.test")
)
);
ok(
!gSSService.isSecureURI(
Services.io.newURI("https://sub.includesubdomains2.preloaded.test")
)
);
// Clearing the data should make everything go back to default.
gSSService.clearAll();
ok(
!gSSService.isSecureURI(Services.io.newURI("https://expired.example.com"))
);
ok(
!gSSService.isSecureURI(
Services.io.newURI("https://notexpired.example.com")
)
);
ok(
gSSService.isSecureURI(
Services.io.newURI("https://includesubdomains.preloaded.test")
)
);
ok(
gSSService.isSecureURI(
Services.io.newURI("https://sub.includesubdomains.preloaded.test")
)
);
ok(
!gSSService.isSecureURI(
Services.io.newURI("https://incsubdomain.example.com")
)
);
ok(
!gSSService.isSecureURI(
Services.io.newURI("https://sub.incsubdomain.example.com")
)
);
ok(
gSSService.isSecureURI(
Services.io.newURI("https://includesubdomains2.preloaded.test")
)
);
ok(
gSSService.isSecureURI(
Services.io.newURI("https://sub.includesubdomains2.preloaded.test")
)
);
do_test_finished();
}
function run_test() {
let profileDir = do_get_profile();
let stateFile = profileDir.clone();
let stateFile = do_get_profile();
stateFile.append(SSS_STATE_FILE_NAME);
// Assuming we're working with a clean slate, the file shouldn't exist
// until we create it.
ok(!stateFile.exists());
let outputStream = FileUtils.openFileOutputStream(stateFile);
let now = Date.now();
let lines = [
`expired.example.com\t0\t0\t${now - 100000},1,0`,
`notexpired.example.com\t0\t0\t${now + 100000},1,0`,
let keyValuePairs = [
{ key: "expired.example.com", value: `${now - 100000},1,0` },
{ key: "notexpired.example.com", value: `${now + 100000},1,0` },
// This overrides an entry on the preload list.
`includesubdomains.preloaded.test\t0\t0\t${now + 100000},1,0`,
`incsubdomain.example.com\t0\t0\t${now + 100000},1,1`,
{ key: "includesubdomains.preloaded.test", value: `${now + 100000},1,0` },
{ key: "incsubdomain.example.com", value: `${now + 100000},1,1` },
// This overrides an entry on the preload list.
"includesubdomains2.preloaded.test\t0\t0\t0,2,0",
{ key: "includesubdomains2.preloaded.test", value: "0,2,0" },
];
writeLinesAndClose(lines, outputStream);
Services.obs.addObserver(checkStateRead, "data-storage-ready");
do_test_pending();
gSSService = Cc["@mozilla.org/ssservice;1"].getService(
for (let keyValuePair of keyValuePairs) {
append_line_to_data_storage_file(
outputStream,
1,
1,
keyValuePair.key,
keyValuePair.value
);
}
// Append a line with a bad checksum.
append_line_to_data_storage_file(
outputStream,
1,
1,
"badchecksum.example.com",
`${now + 100000},1,0`,
24,
true
);
outputStream.close();
let siteSecurityService = Cc["@mozilla.org/ssservice;1"].getService(
Ci.nsISiteSecurityService
);
notEqual(gSSService, null);
notEqual(siteSecurityService, null);
// The backing data storage will block until the background task that reads
// the backing file has finished.
ok(
!siteSecurityService.isSecureURI(
Services.io.newURI("https://expired.example.com")
)
);
ok(
siteSecurityService.isSecureURI(
Services.io.newURI("https://notexpired.example.com")
)
);
ok(
siteSecurityService.isSecureURI(
Services.io.newURI("https://includesubdomains.preloaded.test")
)
);
ok(
!siteSecurityService.isSecureURI(
Services.io.newURI("https://sub.includesubdomains.preloaded.test")
)
);
ok(
siteSecurityService.isSecureURI(
Services.io.newURI("https://incsubdomain.example.com")
)
);
ok(
siteSecurityService.isSecureURI(
Services.io.newURI("https://sub.incsubdomain.example.com")
)
);
ok(
!siteSecurityService.isSecureURI(
Services.io.newURI("https://includesubdomains2.preloaded.test")
)
);
ok(
!siteSecurityService.isSecureURI(
Services.io.newURI("https://sub.includesubdomains2.preloaded.test")
)
);
// Clearing the data should make everything go back to default.
siteSecurityService.clearAll();
ok(
!siteSecurityService.isSecureURI(
Services.io.newURI("https://expired.example.com")
)
);
ok(
!siteSecurityService.isSecureURI(
Services.io.newURI("https://notexpired.example.com")
)
);
ok(
siteSecurityService.isSecureURI(
Services.io.newURI("https://includesubdomains.preloaded.test")
)
);
ok(
siteSecurityService.isSecureURI(
Services.io.newURI("https://sub.includesubdomains.preloaded.test")
)
);
ok(
!siteSecurityService.isSecureURI(
Services.io.newURI("https://incsubdomain.example.com")
)
);
ok(
!siteSecurityService.isSecureURI(
Services.io.newURI("https://sub.incsubdomain.example.com")
)
);
ok(
siteSecurityService.isSecureURI(
Services.io.newURI("https://includesubdomains2.preloaded.test")
)
);
ok(
siteSecurityService.isSecureURI(
Services.io.newURI("https://sub.includesubdomains2.preloaded.test")
)
);
ok(
!siteSecurityService.isSecureURI(
Services.io.newURI("https://badchecksum.example.com")
)
);
}

View File

@ -6,30 +6,6 @@
// The purpose of this test is to create an empty site security service state
// file and see that the site security service doesn't fail when reading it.
var gSSService = null;
function checkStateRead(aSubject, aTopic, aData) {
// nonexistent.example.com should never be an HSTS host
ok(
!gSSService.isSecureURI(
Services.io.newURI("https://nonexistent.example.com")
)
);
ok(
gSSService.isSecureURI(
Services.io.newURI("https://includesubdomains.preloaded.test")
)
);
// notexpired.example.com is an HSTS host in a different test - we
// want to make sure that test hasn't interfered with this one.
ok(
!gSSService.isSecureURI(
Services.io.newURI("https://notexpired.example.com")
)
);
do_test_finished();
}
function run_test() {
let profileDir = do_get_profile();
let stateFile = profileDir.clone();
@ -41,10 +17,27 @@ function run_test() {
ok(stateFile.exists());
// Initialize nsISiteSecurityService after do_get_profile() so it
// can read the state file.
Services.obs.addObserver(checkStateRead, "data-storage-ready");
do_test_pending();
gSSService = Cc["@mozilla.org/ssservice;1"].getService(
let siteSecurityService = Cc["@mozilla.org/ssservice;1"].getService(
Ci.nsISiteSecurityService
);
notEqual(gSSService, null);
notEqual(siteSecurityService, null);
// nsISiteSecurityService.isSecureURI blocks until the backing file has been read.
// nonexistent.example.com should never be an HSTS host
ok(
!siteSecurityService.isSecureURI(
Services.io.newURI("https://nonexistent.example.com")
)
);
ok(
siteSecurityService.isSecureURI(
Services.io.newURI("https://includesubdomains.preloaded.test")
)
);
// notexpired.example.com is an HSTS host in a different test - we
// want to make sure that test hasn't interfered with this one.
ok(
!siteSecurityService.isSecureURI(
Services.io.newURI("https://notexpired.example.com")
)
);
}

View File

@ -3,57 +3,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// The purpose of this test is to create a mostly bogus site security service
// state file and see that the site security service handles it properly.
var gSSService = null;
function checkStateRead(aSubject, aTopic, aData) {
if (aData == CLIENT_AUTH_FILE_NAME) {
return;
}
equal(aData, SSS_STATE_FILE_NAME);
const HSTS_HOSTS = [
"https://example1.example.com",
"https://example2.example.com",
];
for (let host of HSTS_HOSTS) {
ok(
gSSService.isSecureURI(Services.io.newURI(host)),
`${host} should be HSTS enabled`
);
}
const NOT_HSTS_HOSTS = [
"https://example.com",
"https://example3.example.com",
"https://extra.comma.example.com",
"https://empty.statestring.example.com",
"https://rubbish.statestring.example.com",
"https://spaces.statestring.example.com",
"https://invalid.expirytime.example.com",
"https://text.securitypropertystate.example.com",
"https://invalid.securitypropertystate.example.com",
"https://text.includesubdomains.example.com",
"https://invalid.includesubdomains.example.com",
];
for (let host of NOT_HSTS_HOSTS) {
ok(
!gSSService.isSecureURI(Services.io.newURI(host)),
`${host} should not be HSTS enabled`
);
}
do_test_finished();
}
// The purpose of this test is to create a mostly bogus old site security
// service state file and see that the site security service migrates it
// to the new format properly, discarding invalid data.
function run_test() {
Services.prefs.setBoolPref("security.cert_pinning.hpkp.enabled", true);
let profileDir = do_get_profile();
let stateFile = profileDir.clone();
stateFile.append(SSS_STATE_FILE_NAME);
stateFile.append(SSS_STATE_OLD_FILE_NAME);
// Assuming we're working with a clean slate, the file shouldn't exist
// until we create it.
ok(!stateFile.exists());
@ -81,15 +38,40 @@ function run_test() {
`invalid.includesubdomains.example.com\t0\t0\t${expiryTime},1,0foo`,
];
writeLinesAndClose(lines, outputStream);
Services.obs.addObserver(checkStateRead, "data-storage-ready");
do_test_pending();
gSSService = Cc["@mozilla.org/ssservice;1"].getService(
let siteSecurityService = Cc["@mozilla.org/ssservice;1"].getService(
Ci.nsISiteSecurityService
);
notEqual(gSSService, null);
notEqual(siteSecurityService, null);
Services.prefs.setIntPref("security.cert_pinning.enforcement_level", 2);
registerCleanupFunction(() => {
Services.prefs.clearUserPref("security.cert_pinning.enforcement_level");
});
const HSTS_HOSTS = [
"https://example1.example.com",
"https://example2.example.com",
];
for (let host of HSTS_HOSTS) {
ok(
siteSecurityService.isSecureURI(Services.io.newURI(host)),
`${host} should be HSTS enabled`
);
}
const NOT_HSTS_HOSTS = [
"https://example.com",
"https://example3.example.com",
"https://extra.comma.example.com",
"https://empty.statestring.example.com",
"https://rubbish.statestring.example.com",
"https://spaces.statestring.example.com",
"https://invalid.expirytime.example.com",
"https://text.securitypropertystate.example.com",
"https://invalid.securitypropertystate.example.com",
"https://text.includesubdomains.example.com",
"https://invalid.includesubdomains.example.com",
];
for (let host of NOT_HSTS_HOSTS) {
ok(
!siteSecurityService.isSecureURI(Services.io.newURI(host)),
`${host} should not be HSTS enabled`
);
}
}

View File

@ -3,57 +3,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// The purpose of this test is to create a site security service state file
// that is too large and see that the site security service reads it properly
// (this means discarding all entries after the 1024th).
var gSSService = null;
function checkStateRead(aSubject, aTopic, aData) {
if (aData == CLIENT_AUTH_FILE_NAME) {
return;
}
equal(aData, SSS_STATE_FILE_NAME);
ok(
gSSService.isSecureURI(Services.io.newURI("https://example0.example.com"))
);
ok(
gSSService.isSecureURI(Services.io.newURI("https://example423.example.com"))
);
ok(
gSSService.isSecureURI(
Services.io.newURI("https://example1023.example.com")
)
);
ok(
!gSSService.isSecureURI(
Services.io.newURI("https://example1024.example.com")
)
);
ok(
!gSSService.isSecureURI(
Services.io.newURI("https://example1025.example.com")
)
);
ok(
!gSSService.isSecureURI(
Services.io.newURI("https://example9000.example.com")
)
);
ok(
!gSSService.isSecureURI(
Services.io.newURI("https://example99999.example.com")
)
);
do_test_finished();
}
// The purpose of this test is to create an old site security service state
// file that is too large and see that the site security service migrates it to
// the new format properly.
function run_test() {
let profileDir = do_get_profile();
let stateFile = profileDir.clone();
stateFile.append(SSS_STATE_FILE_NAME);
stateFile.append(SSS_STATE_OLD_FILE_NAME);
// Assuming we're working with a clean slate, the file shouldn't exist
// until we create it.
ok(!stateFile.exists());
@ -67,14 +24,49 @@ function run_test() {
`example${i}.example.com\t` +
"0000000000000000000000000000000000000000000000000\t" +
"00000000000000000000000000000000000000\t" +
`${expiryTime},1,0000000000000000000000000000000000000000000000000000000000000000000000000`
`${expiryTime},1,0`
);
}
writeLinesAndClose(lines, outputStream);
Services.obs.addObserver(checkStateRead, "data-storage-ready");
do_test_pending();
gSSService = Cc["@mozilla.org/ssservice;1"].getService(
let siteSecurityService = Cc["@mozilla.org/ssservice;1"].getService(
Ci.nsISiteSecurityService
);
notEqual(gSSService, null);
notEqual(siteSecurityService, null);
ok(
siteSecurityService.isSecureURI(
Services.io.newURI("https://example0.example.com")
)
);
ok(
siteSecurityService.isSecureURI(
Services.io.newURI("https://example423.example.com")
)
);
ok(
siteSecurityService.isSecureURI(
Services.io.newURI("https://example1023.example.com")
)
);
ok(
!siteSecurityService.isSecureURI(
Services.io.newURI("https://example1024.example.com")
)
);
ok(
!siteSecurityService.isSecureURI(
Services.io.newURI("https://example1025.example.com")
)
);
ok(
!siteSecurityService.isSecureURI(
Services.io.newURI("https://example9000.example.com")
)
);
ok(
!siteSecurityService.isSecureURI(
Services.io.newURI("https://example99999.example.com")
)
);
}

View File

@ -22,30 +22,19 @@ Cc["@mozilla.org/serviceworkers/manager;1"].getService(
Ci.nsIServiceWorkerManager
);
function getStateFileContents() {
let stateFile = do_get_profile();
stateFile.append(SSS_STATE_FILE_NAME);
ok(stateFile.exists());
return readFile(stateFile);
}
add_task(async function run_test() {
Services.prefs.setIntPref("test.datastorage.write_timer_ms", 100);
do_get_profile();
let SSService = Cc["@mozilla.org/ssservice;1"].getService(
Ci.nsISiteSecurityService
);
let header = "max-age=50000";
SSService.processHeader(Services.io.newURI("http://example.com"), header);
await TestUtils.topicObserved(
"data-storage-written",
(_, data) => data == SSS_STATE_FILE_NAME
);
let stateFileContents = getStateFileContents();
ok(
stateFileContents.includes("example.com"),
"should have written out state file"
);
SSService.processHeader(Services.io.newURI("https://example.com"), header);
await TestUtils.waitForCondition(() => {
let stateFileContents = get_data_storage_contents(SSS_STATE_FILE_NAME);
return stateFileContents
? stateFileContents.includes("example.com")
: false;
});
// Configure Firefox to clear this data on shutdown.
Services.prefs.setBoolPref(
@ -62,5 +51,9 @@ add_task(async function run_test() {
Services.startup.SHUTDOWN_PHASE_APPSHUTDOWN
);
equal(getStateFileContents(), "", "state file should be empty");
await TestUtils.waitForCondition(() => {
let stateFile = do_get_profile();
stateFile.append(SSS_STATE_FILE_NAME);
return !stateFile.exists();
});
});

View File

@ -6,92 +6,59 @@
// The purpose of this test is to see that the site security service properly
// writes its state file.
ChromeUtils.defineESModuleGetters(this, {
TestUtils: "resource://testing-common/TestUtils.sys.mjs",
});
const EXPECTED_ENTRIES = 5;
const EXPECTED_HSTS_COLUMNS = 3;
var gProfileDir = null;
var gExpectingWrites = true;
function contents_is_as_expected() {
// The file consists of a series of [score][last accessed][key][value], where
// score and last accessed are 2 bytes big-endian, key is 0-padded to 256
// bytes, and value is 0-padded to 24 bytes.
// Each score will be 1, and last accessed is some number of days (>255)
// since the epoch, so there will be 3 non-0 bytes just in front of the key.
// Splitting by 0 and filtering out zero-length strings will result in a series of
// [BBBkey1, value1, BBBkey2, value2, ...], where "BBB" are the score and
// last accessed bytes, which are ignored here.
let contents = get_data_storage_contents(SSS_STATE_FILE_NAME);
if (!contents) {
return false;
}
let keysAndValues = contents.split("\0").filter(s => !!s.length);
let keys = keysAndValues
.filter((_, i) => i % 2 == 0)
.map(key => key.substring(3));
let values = keysAndValues.filter((_, i) => i % 2 == 1);
// For reference, the format of the state file is a list of:
// <domain name> <expiration time in milliseconds>,<sts status>,<includeSubdomains>
// separated by newlines ('\n')
function checkStateWritten(aSubject, aTopic, aData) {
if (aData == CLIENT_AUTH_FILE_NAME) {
return;
if (keys.length != EXPECTED_ENTRIES || values.length != EXPECTED_ENTRIES) {
return false;
}
equal(aData, SSS_STATE_FILE_NAME);
ok(gExpectingWrites);
let stateFile = gProfileDir.clone();
stateFile.append(SSS_STATE_FILE_NAME);
ok(stateFile.exists());
let stateFileContents = readFile(stateFile);
// the last line is removed because it's just a trailing newline
let lines = stateFileContents.split("\n").slice(0, -1);
equal(lines.length, EXPECTED_ENTRIES);
let sites = {}; // a map of domain name -> [the entry in the state file]
for (let line of lines) {
let parts = line.split("\t");
let host = parts[0];
let entry = parts[3].split(",");
let expectedColumns = EXPECTED_HSTS_COLUMNS;
equal(entry.length, expectedColumns);
for (let i in keys) {
let host = keys[i];
let entry = values[i].split(",");
equal(entry.length, EXPECTED_HSTS_COLUMNS);
sites[host] = entry;
}
// While we're still processing headers, multiple writes of the backing data
// can be scheduled, and thus we can receive multiple data-storage-written
// notifications. In these cases, the data may not be as we expect. We only
// care about the final one being correct, however, so we return and wait for
// the next event if things aren't as we expect.
// each sites[url][1] should be SecurityPropertySet (i.e. 1).
// sites[url][2] corresponds to includeSubdomains, so every other one should
// be set (i.e. 1);
if (sites["includesubdomains.preloaded.test"][1] != 1) {
return;
}
if (sites["includesubdomains.preloaded.test"][2] != 0) {
return;
}
if (sites["a.example.com"][1] != 1) {
return;
}
if (sites["a.example.com"][2] != 1) {
return;
}
if (sites["b.example.com"][1] != 1) {
return;
}
if (sites["b.example.com"][2] != 0) {
return;
}
if (sites["c.c.example.com"][1] != 1) {
return;
}
if (sites["c.c.example.com"][2] != 1) {
return;
}
if (sites["d.example.com"][1] != 1) {
return;
}
if (sites["d.example.com"][2] != 0) {
return;
}
// If we get here, the file was as expected and we no longer expect any
// data-storage-written notifications.
gExpectingWrites = false;
// Process the headers again to test that seeing them again after such a
// short delay doesn't cause another write.
process_headers();
// Wait a bit before finishing the test, to see if another write happens.
do_timeout(2000, function () {
do_test_finished();
});
return (
sites["includesubdomains.preloaded.test"][1] == 1 &&
sites["includesubdomains.preloaded.test"][2] == 0 &&
sites["a.example.com"][1] == 1 &&
sites["a.example.com"][2] == 1 &&
sites["b.example.com"][1] == 1 &&
sites["b.example.com"][2] == 0 &&
sites["c.c.example.com"][1] == 1 &&
sites["c.c.example.com"][2] == 1 &&
sites["d.example.com"][1] == 1 &&
sites["d.example.com"][2] == 0
);
}
function process_headers() {
@ -118,9 +85,7 @@ function process_headers() {
}
function run_test() {
Services.prefs.setIntPref("test.datastorage.write_timer_ms", 100);
gProfileDir = do_get_profile();
do_get_profile();
process_headers();
do_test_pending();
Services.obs.addObserver(checkStateWritten, "data-storage-written");
TestUtils.waitForCondition(contents_is_as_expected);
}

View File

@ -23,7 +23,6 @@ support-files =
test_cert_utf8/**
test_cert_version/**
test_certDB_import/**
test_client_auth_remember_service/**
test_content_signing/**
test_crlite_filters/**
test_crlite_preexisting/**

View File

@ -23,6 +23,7 @@ gecko_metrics = [
"mobile/android/actors/metrics.yaml",
"netwerk/metrics.yaml",
"netwerk/protocol/http/metrics.yaml",
"security/manager/ssl/metrics.yaml",
"toolkit/components/cookiebanners/metrics.yaml",
"toolkit/components/extensions/metrics.yaml",
"toolkit/components/formautofill/metrics.yaml",

View File

@ -14239,15 +14239,6 @@
"releaseChannelCollection": "opt-out",
"description": "The submission status when main/plugin/content crashes are submitted. 1 is success, 0 is failure. Keyed on the CrashManager Crash.type."
},
"DATA_STORAGE_ENTRIES": {
"record_in_processes": ["main", "content"],
"products": ["firefox", "fennec"],
"expires_in_version": "default",
"kind": "linear",
"high": 1024,
"n_buckets": 16,
"description": "The number of entries in persistent DataStorage (HSTS and HPKP data, basically)"
},
"VIDEO_PLAY_TIME_MS": {
"record_in_processes": ["main", "content"],
"products": ["firefox"],

View File

@ -78,7 +78,6 @@
"CHECK_ADDONS_MODIFIED_MS",
"COMPONENTS_SHIM_ACCESSED_BY_CONTENT",
"CRASH_STORE_COMPRESSED_BYTES",
"DATA_STORAGE_ENTRIES",
"DEFECTIVE_PERMISSIONS_SQL_REMOVED",
"DEFERRED_FINALIZE_ASYNC",
"DENIED_TRANSLATION_OFFERS",
@ -402,7 +401,6 @@
"CYCLE_COLLECTOR_WORKER_VISITED_GCED",
"CYCLE_COLLECTOR_WORKER_VISITED_REF_COUNTED",
"D3D11_SYNC_HANDLE_FAILURE",
"DATA_STORAGE_ENTRIES",
"DEFECTIVE_PERMISSIONS_SQL_REMOVED",
"DEFERRED_FINALIZE_ASYNC",
"DENIED_TRANSLATION_OFFERS",
@ -942,7 +940,6 @@
"FX_SESSION_RESTORE_AUTO_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS",
"FX_SESSION_RESTORE_COLLECT_DATA_MS",
"FX_SESSION_RESTORE_FILE_SIZE_BYTES",
"DATA_STORAGE_ENTRIES",
"TRANSLATED_PAGES_BY_LANGUAGE",
"LOCALDOMSTORAGE_SHUTDOWN_DATABASE_MS",
"FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED",

View File

@ -39,6 +39,7 @@ jsrust_shared = { path = "../../../../js/src/rust/shared" }
cascade_bloom_filter = { path = "../../../components/cascade_bloom_filter" }
cert_storage = { path = "../../../../security/manager/ssl/cert_storage" }
crypto_hash = { path = "../../../../security/manager/ssl/crypto_hash" }
data_storage = { path = "../../../../security/manager/ssl/data_storage" }
bitsdownload = { path = "../../../components/bitsdownload", optional = true }
storage = { path = "../../../../storage/rust" }
bookmark_sync = { path = "../../../components/places/bookmark_sync", optional = true }

View File

@ -23,6 +23,7 @@ extern crate cosec;
extern crate cubeb_coreaudio;
#[cfg(feature = "cubeb_pulse_rust")]
extern crate cubeb_pulse;
extern crate data_storage;
extern crate encoding_glue;
extern crate fog_control;
extern crate gecko_profiler;

View File

@ -16,6 +16,7 @@ clippy:
- modules/libpref/init/static_prefs/
- mozglue/static/rust/
- netwerk/base/mozurl/
- security/manager/ssl/data_storage/
- servo/components/derive_common/
- servo/components/selectors/
- servo/components/servo_arc/