Line data Source code
1 : /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 : /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 : /* This Source Code Form is subject to the terms of the Mozilla Public
4 : * License, v. 2.0. If a copy of the MPL was not distributed with this
5 : * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 :
7 : #include "DataStorage.h"
8 :
9 : #include "mozilla/Assertions.h"
10 : #include "mozilla/ClearOnShutdown.h"
11 : #include "mozilla/dom/PContent.h"
12 : #include "mozilla/dom/ContentChild.h"
13 : #include "mozilla/dom/ContentParent.h"
14 : #include "mozilla/Preferences.h"
15 : #include "mozilla/Services.h"
16 : #include "mozilla/Telemetry.h"
17 : #include "mozilla/Unused.h"
18 : #include "nsAppDirectoryServiceDefs.h"
19 : #include "nsDirectoryServiceUtils.h"
20 : #include "nsIMemoryReporter.h"
21 : #include "nsIObserverService.h"
22 : #include "nsITimer.h"
23 : #include "nsNetUtil.h"
24 : #include "nsPrintfCString.h"
25 : #include "nsStreamUtils.h"
26 : #include "nsThreadUtils.h"
27 :
28 : // NB: Read DataStorage.h first.
29 :
30 : // The default time between data changing and a write, in milliseconds.
31 : static const uint32_t sDataStorageDefaultTimerDelay = 5u * 60u * 1000u;
32 : // The maximum score an entry can have (prevents overflow)
33 : static const uint32_t sMaxScore = UINT32_MAX;
34 : // The maximum number of entries per type of data (limits resource use)
35 : static const uint32_t sMaxDataEntries = 1024;
36 : static const int64_t sOneDayInMicroseconds = int64_t(24 * 60 * 60) *
37 : PR_USEC_PER_SEC;
38 :
39 : namespace mozilla {
40 :
41 3 : class DataStorageMemoryReporter final : public nsIMemoryReporter
42 : {
43 0 : MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
44 : ~DataStorageMemoryReporter() = default;
45 :
46 : public:
47 : NS_DECL_ISUPPORTS
48 :
49 0 : NS_IMETHOD CollectReports(nsIHandleReportCallback* aHandleReport,
50 : nsISupports* aData, bool aAnonymize) final
51 : {
52 0 : nsTArray<nsString> fileNames;
53 0 : DataStorage::GetAllFileNames(fileNames);
54 0 : for (const auto& file: fileNames) {
55 0 : RefPtr<DataStorage> ds = DataStorage::GetFromRawFileName(file);
56 0 : size_t amount = ds->SizeOfIncludingThis(MallocSizeOf);
57 : nsPrintfCString path("explicit/data-storage/%s",
58 0 : NS_ConvertUTF16toUTF8(file).get());
59 0 : Unused << aHandleReport->Callback(EmptyCString(), path, KIND_HEAP,
60 : UNITS_BYTES, amount,
61 0 : NS_LITERAL_CSTRING("Memory used by PSM data storage cache."),
62 0 : aData);
63 : }
64 0 : return NS_OK;
65 : }
66 : };
67 :
68 39 : NS_IMPL_ISUPPORTS(DataStorageMemoryReporter, nsIMemoryReporter)
69 :
70 137 : NS_IMPL_ISUPPORTS(DataStorage, nsIObserver)
71 :
72 3 : StaticAutoPtr<DataStorage::DataStorages> DataStorage::sDataStorages;
73 :
74 9 : DataStorage::DataStorage(const nsString& aFilename)
75 : : mMutex("DataStorage::mMutex")
76 : , mPendingWrite(false)
77 : , mShuttingDown(false)
78 : , mInitCalled(false)
79 : , mReadyMonitor("DataStorage::mReadyMonitor")
80 : , mReady(false)
81 9 : , mFilename(aFilename)
82 : {
83 9 : }
84 :
85 0 : DataStorage::~DataStorage()
86 : {
87 0 : }
88 :
89 : // static
90 : already_AddRefed<DataStorage>
91 5 : DataStorage::Get(DataStorageClass aFilename)
92 : {
93 5 : switch (aFilename) {
94 : #define DATA_STORAGE(_) \
95 : case DataStorageClass::_: \
96 : return GetFromRawFileName(NS_LITERAL_STRING(#_ ".txt"));
97 : #include "mozilla/DataStorageList.h"
98 : #undef DATA_STORAGE
99 : default:
100 0 : MOZ_ASSERT_UNREACHABLE("Invalid DataStorage type passed?");
101 : return nullptr;
102 : }
103 : }
104 :
105 : // static
106 : already_AddRefed<DataStorage>
107 14 : DataStorage::GetFromRawFileName(const nsString& aFilename)
108 : {
109 14 : MOZ_ASSERT(NS_IsMainThread());
110 14 : if (!sDataStorages) {
111 3 : sDataStorages = new DataStorages();
112 3 : ClearOnShutdown(&sDataStorages);
113 : }
114 28 : RefPtr<DataStorage> storage;
115 14 : if (!sDataStorages->Get(aFilename, getter_AddRefs(storage))) {
116 9 : storage = new DataStorage(aFilename);
117 9 : sDataStorages->Put(aFilename, storage);
118 : }
119 28 : return storage.forget();
120 : }
121 :
122 : // static
123 : already_AddRefed<DataStorage>
124 0 : DataStorage::GetIfExists(DataStorageClass aFilename)
125 : {
126 0 : MOZ_ASSERT(NS_IsMainThread());
127 0 : if (!sDataStorages) {
128 0 : sDataStorages = new DataStorages();
129 : }
130 0 : nsString name;
131 0 : switch (aFilename) {
132 : #define DATA_STORAGE(_) \
133 : case DataStorageClass::_: \
134 : name.AssignLiteral(#_ ".txt"); \
135 : break;
136 : #include "mozilla/DataStorageList.h"
137 : #undef DATA_STORAGE
138 : default:
139 0 : MOZ_ASSERT_UNREACHABLE("Invalid DataStorages type passed?");
140 : }
141 0 : RefPtr<DataStorage> storage;
142 0 : if (!name.IsEmpty()) {
143 0 : sDataStorages->Get(name, getter_AddRefs(storage));
144 : }
145 0 : return storage.forget();
146 : }
147 :
148 : // static
149 : void
150 2 : DataStorage::GetAllFileNames(nsTArray<nsString>& aItems)
151 : {
152 2 : MOZ_ASSERT(NS_IsMainThread());
153 2 : if (!sDataStorages) {
154 1 : return;
155 : }
156 : #define DATA_STORAGE(_) \
157 : aItems.AppendElement(NS_LITERAL_STRING(#_ ".txt"));
158 : #include "mozilla/DataStorageList.h"
159 : #undef DATA_STORAGE
160 : }
161 :
162 : // static
163 : void
164 2 : DataStorage::GetAllChildProcessData(
165 : nsTArray<mozilla::dom::DataStorageEntry>& aEntries)
166 : {
167 4 : nsTArray<nsString> storageFiles;
168 2 : GetAllFileNames(storageFiles);
169 5 : for (auto& file : storageFiles) {
170 6 : dom::DataStorageEntry entry;
171 3 : entry.filename() = file;
172 6 : RefPtr<DataStorage> storage = DataStorage::GetFromRawFileName(file);
173 3 : if (!storage->mInitCalled) {
174 : // Perhaps no consumer has initialized the DataStorage object yet,
175 : // so do that now!
176 0 : bool dataWillPersist = false;
177 0 : nsresult rv = storage->Init(dataWillPersist);
178 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
179 0 : return;
180 : }
181 : }
182 3 : storage->GetAll(&entry.items());
183 3 : aEntries.AppendElement(Move(entry));
184 : }
185 : }
186 :
187 : // static
188 : void
189 2 : DataStorage::SetCachedStorageEntries(
190 : const InfallibleTArray<mozilla::dom::DataStorageEntry>& aEntries)
191 : {
192 2 : MOZ_ASSERT(XRE_IsContentProcess());
193 :
194 : // Make sure to initialize all DataStorage classes.
195 : // For each one, we look through the list of our entries and if we find
196 : // a matching DataStorage object, we initialize it.
197 : //
198 : // Note that this is an O(n^2) operation, but the n here is very small
199 : // (currently 3). There is a comment in the DataStorageList.h header
200 : // about updating the algorithm here to something more fancy if the list
201 : // of DataStorage items grows some day.
202 4 : nsTArray<dom::DataStorageEntry> entries;
203 : #define DATA_STORAGE(_) \
204 : { \
205 : dom::DataStorageEntry entry; \
206 : entry.filename() = NS_LITERAL_STRING(#_ ".txt"); \
207 : for (auto& e : aEntries) { \
208 : if (entry.filename().Equals(e.filename())) { \
209 : entry.items() = Move(e.items()); \
210 : break; \
211 : } \
212 : } \
213 : entries.AppendElement(Move(entry)); \
214 : }
215 : #include "mozilla/DataStorageList.h"
216 : #undef DATA_STORAGE
217 :
218 8 : for (auto& entry : entries) {
219 : RefPtr<DataStorage> storage =
220 12 : DataStorage::GetFromRawFileName(entry.filename());
221 6 : bool dataWillPersist = false;
222 6 : storage->Init(dataWillPersist, &entry.items());
223 : }
224 2 : }
225 :
226 : size_t
227 0 : DataStorage::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
228 : {
229 : size_t sizeOfExcludingThis =
230 0 : mPersistentDataTable.ShallowSizeOfExcludingThis(aMallocSizeOf) +
231 0 : mTemporaryDataTable.ShallowSizeOfExcludingThis(aMallocSizeOf) +
232 0 : mPrivateDataTable.ShallowSizeOfExcludingThis(aMallocSizeOf) +
233 0 : mFilename.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
234 0 : return aMallocSizeOf(this) + sizeOfExcludingThis;
235 : }
236 :
237 : nsresult
238 11 : DataStorage::Init(bool& aDataWillPersist,
239 : const InfallibleTArray<mozilla::dom::DataStorageItem>* aItems)
240 : {
241 : // Don't access the observer service or preferences off the main thread.
242 11 : if (!NS_IsMainThread()) {
243 0 : MOZ_ASSERT_UNREACHABLE("DataStorage::Init called off main thread");
244 : return NS_ERROR_NOT_SAME_THREAD;
245 : }
246 :
247 22 : MutexAutoLock lock(mMutex);
248 :
249 : // Ignore attempts to initialize several times.
250 11 : if (mInitCalled) {
251 2 : return NS_OK;
252 : }
253 :
254 9 : mInitCalled = true;
255 :
256 : static bool memoryReporterRegistered = false;
257 9 : if (!memoryReporterRegistered) {
258 : nsresult rv =
259 3 : RegisterStrongMemoryReporter(new DataStorageMemoryReporter());
260 3 : if (NS_WARN_IF(NS_FAILED(rv))) {
261 0 : return rv;
262 : }
263 3 : memoryReporterRegistered = true;
264 : }
265 :
266 : nsresult rv;
267 9 : if (XRE_IsParentProcess()) {
268 3 : MOZ_ASSERT(!aItems);
269 :
270 3 : rv = NS_NewNamedThread("DataStorage", getter_AddRefs(mWorkerThread));
271 3 : if (NS_WARN_IF(NS_FAILED(rv))) {
272 0 : return rv;
273 : }
274 :
275 3 : rv = AsyncReadData(aDataWillPersist, lock);
276 3 : if (NS_FAILED(rv)) {
277 0 : return rv;
278 : }
279 : } else {
280 : // In the child process, we use the data passed to us by the parent process
281 : // to initialize.
282 6 : MOZ_ASSERT(XRE_IsContentProcess());
283 6 : MOZ_ASSERT(aItems);
284 :
285 6 : aDataWillPersist = false;
286 6 : for (auto& item : *aItems) {
287 0 : Entry entry;
288 0 : entry.mValue = item.value();
289 0 : rv = PutInternal(item.key(), entry, item.type(), lock);
290 0 : if (NS_FAILED(rv)) {
291 0 : return rv;
292 : }
293 : }
294 6 : mReady = true;
295 6 : NotifyObservers("data-storage-ready");
296 : }
297 :
298 18 : nsCOMPtr<nsIObserverService> os = services::GetObserverService();
299 9 : if (NS_WARN_IF(!os)) {
300 0 : return NS_ERROR_FAILURE;
301 : }
302 : // Clear private data as appropriate.
303 9 : os->AddObserver(this, "last-pb-context-exited", false);
304 : // Observe shutdown; save data and prevent any further writes.
305 : // In the parent process, we need to write to the profile directory, so
306 : // we should listen for profile-before-change so that we can safely
307 : // write to the profile. In the content process however we don't have
308 : // access to the profile directory and profile notifications are not
309 : // dispatched, so we need to clean up on xpcom-shutdown.
310 9 : if (XRE_IsParentProcess()) {
311 3 : os->AddObserver(this, "profile-before-change", false);
312 : }
313 : // In the Parent process, this is a backstop for xpcshell and other cases
314 : // where profile-before-change might not get sent.
315 9 : os->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
316 :
317 : // For test purposes, we can set the write timer to be very fast.
318 9 : mTimerDelay = Preferences::GetInt("test.datastorage.write_timer_ms",
319 : sDataStorageDefaultTimerDelay);
320 9 : rv = Preferences::AddStrongObserver(this, "test.datastorage.write_timer_ms");
321 9 : if (NS_WARN_IF(NS_FAILED(rv))) {
322 0 : return rv;
323 : }
324 :
325 9 : return NS_OK;
326 : }
327 :
328 : class DataStorage::Reader : public Runnable
329 : {
330 : public:
331 3 : explicit Reader(DataStorage* aDataStorage)
332 3 : : Runnable("DataStorage::Reader")
333 3 : , mDataStorage(aDataStorage)
334 : {
335 3 : }
336 : ~Reader();
337 :
338 : private:
339 : NS_DECL_NSIRUNNABLE
340 :
341 : static nsresult ParseLine(nsDependentCSubstring& aLine, nsCString& aKeyOut,
342 : Entry& aEntryOut);
343 :
344 : RefPtr<DataStorage> mDataStorage;
345 : };
346 :
347 9 : DataStorage::Reader::~Reader()
348 : {
349 : // Notify that calls to Get can proceed.
350 : {
351 6 : MonitorAutoLock readyLock(mDataStorage->mReadyMonitor);
352 3 : mDataStorage->mReady = true;
353 3 : nsresult rv = mDataStorage->mReadyMonitor.NotifyAll();
354 3 : Unused << NS_WARN_IF(NS_FAILED(rv));
355 : }
356 :
357 : // This is for tests.
358 : nsCOMPtr<nsIRunnable> job =
359 6 : NewRunnableMethod<const char*>("DataStorage::NotifyObservers",
360 : mDataStorage,
361 : &DataStorage::NotifyObservers,
362 6 : "data-storage-ready");
363 3 : nsresult rv = NS_DispatchToMainThread(job, NS_DISPATCH_NORMAL);
364 3 : Unused << NS_WARN_IF(NS_FAILED(rv));
365 9 : }
366 :
367 : NS_IMETHODIMP
368 3 : DataStorage::Reader::Run()
369 : {
370 : nsresult rv;
371 : // Concurrent operations on nsIFile objects are not guaranteed to be safe,
372 : // so we clone the file while holding the lock and then release the lock.
373 : // At that point, we can safely operate on the clone.
374 6 : nsCOMPtr<nsIFile> file;
375 : {
376 6 : MutexAutoLock lock(mDataStorage->mMutex);
377 : // If we don't have a profile, bail.
378 3 : if (!mDataStorage->mBackingFile) {
379 0 : return NS_OK;
380 : }
381 3 : rv = mDataStorage->mBackingFile->Clone(getter_AddRefs(file));
382 3 : if (NS_WARN_IF(NS_FAILED(rv))) {
383 0 : return rv;
384 : }
385 : }
386 6 : nsCOMPtr<nsIInputStream> fileInputStream;
387 3 : rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream), file);
388 : // If we failed for some reason other than the file doesn't exist, bail.
389 3 : if (NS_WARN_IF(NS_FAILED(rv) &&
390 : rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST && // on Unix
391 : rv != NS_ERROR_FILE_NOT_FOUND)) { // on Windows
392 0 : return rv;
393 : }
394 :
395 : // If there is a file with data in it, read it. If there isn't,
396 : // we'll essentially fall through to notifying that we're good to go.
397 6 : nsCString data;
398 3 : if (fileInputStream) {
399 : // Limit to 2MB of data, but only store sMaxDataEntries entries.
400 3 : rv = NS_ConsumeStream(fileInputStream, 1u << 21, data);
401 3 : if (NS_WARN_IF(NS_FAILED(rv))) {
402 0 : return rv;
403 : }
404 : }
405 :
406 : // Atomically parse the data and insert the entries read.
407 : // Don't clear existing entries - they may have been inserted between when
408 : // this read was kicked-off and when it was run.
409 : {
410 6 : MutexAutoLock lock(mDataStorage->mMutex);
411 : // The backing file consists of a list of
412 : // <key>\t<score>\t<last accessed time>\t<value>\n
413 : // The final \n is not optional; if it is not present the line is assumed
414 : // to be corrupt.
415 3 : int32_t currentIndex = 0;
416 3 : int32_t newlineIndex = 0;
417 : do {
418 3 : newlineIndex = data.FindChar('\n', currentIndex);
419 : // If there are no more newlines or the data table has too many
420 : // entries, we are done.
421 3 : if (newlineIndex < 0 ||
422 0 : mDataStorage->mPersistentDataTable.Count() >= sMaxDataEntries) {
423 3 : break;
424 : }
425 :
426 : nsDependentCSubstring line(data, currentIndex,
427 0 : newlineIndex - currentIndex);
428 0 : currentIndex = newlineIndex + 1;
429 0 : nsCString key;
430 0 : Entry entry;
431 0 : nsresult parseRV = ParseLine(line, key, entry);
432 0 : if (NS_SUCCEEDED(parseRV)) {
433 : // It could be the case that a newer entry was added before
434 : // we got around to reading the file. Don't overwrite new entries.
435 0 : Entry newerEntry;
436 0 : bool present = mDataStorage->mPersistentDataTable.Get(key, &newerEntry);
437 0 : if (!present) {
438 0 : mDataStorage->mPersistentDataTable.Put(key, entry);
439 : }
440 0 : }
441 : } while (true);
442 :
443 3 : Telemetry::Accumulate(Telemetry::DATA_STORAGE_ENTRIES,
444 6 : mDataStorage->mPersistentDataTable.Count());
445 : }
446 :
447 3 : return NS_OK;
448 : }
449 :
450 : // The key must be a non-empty string containing no instances of '\t' or '\n',
451 : // and must have a length no more than 256.
452 : // The value must not contain '\n' and must have a length no more than 1024.
453 : // The length limits are to prevent unbounded memory and disk usage.
454 : /* static */
455 : nsresult
456 0 : DataStorage::ValidateKeyAndValue(const nsCString& aKey, const nsCString& aValue)
457 : {
458 0 : if (aKey.IsEmpty()) {
459 0 : return NS_ERROR_INVALID_ARG;
460 : }
461 0 : if (aKey.Length() > 256) {
462 0 : return NS_ERROR_INVALID_ARG;
463 : }
464 0 : int32_t delimiterIndex = aKey.FindChar('\t', 0);
465 0 : if (delimiterIndex >= 0) {
466 0 : return NS_ERROR_INVALID_ARG;
467 : }
468 0 : delimiterIndex = aKey.FindChar('\n', 0);
469 0 : if (delimiterIndex >= 0) {
470 0 : return NS_ERROR_INVALID_ARG;
471 : }
472 0 : delimiterIndex = aValue.FindChar('\n', 0);
473 0 : if (delimiterIndex >= 0) {
474 0 : return NS_ERROR_INVALID_ARG;
475 : }
476 0 : if (aValue.Length() > 1024) {
477 0 : return NS_ERROR_INVALID_ARG;
478 : }
479 :
480 0 : return NS_OK;
481 : }
482 :
483 : // Each line is: <key>\t<score>\t<last accessed time>\t<value>
484 : // Where <score> is a uint32_t as a string, <last accessed time> is a
485 : // int32_t as a string, and the rest are strings.
486 : // <value> can contain anything but a newline.
487 : // Returns a successful status if the line can be decoded into a key and entry.
488 : // Otherwise, an error status is returned and the values assigned to the
489 : // output parameters are in an undefined state.
490 : /* static */
491 : nsresult
492 0 : DataStorage::Reader::ParseLine(nsDependentCSubstring& aLine, nsCString& aKeyOut,
493 : Entry& aEntryOut)
494 : {
495 : // First find the indices to each part of the line.
496 : int32_t scoreIndex;
497 0 : scoreIndex = aLine.FindChar('\t', 0) + 1;
498 0 : if (scoreIndex <= 0) {
499 0 : return NS_ERROR_UNEXPECTED;
500 : }
501 0 : int32_t accessedIndex = aLine.FindChar('\t', scoreIndex) + 1;
502 0 : if (accessedIndex <= 0) {
503 0 : return NS_ERROR_UNEXPECTED;
504 : }
505 0 : int32_t valueIndex = aLine.FindChar('\t', accessedIndex) + 1;
506 0 : if (valueIndex <= 0) {
507 0 : return NS_ERROR_UNEXPECTED;
508 : }
509 :
510 : // Now make substrings based on where each part is.
511 0 : nsDependentCSubstring keyPart(aLine, 0, scoreIndex - 1);
512 : nsDependentCSubstring scorePart(aLine, scoreIndex,
513 0 : accessedIndex - scoreIndex - 1);
514 : nsDependentCSubstring accessedPart(aLine, accessedIndex,
515 0 : valueIndex - accessedIndex - 1);
516 0 : nsDependentCSubstring valuePart(aLine, valueIndex);
517 :
518 : nsresult rv;
519 0 : rv = DataStorage::ValidateKeyAndValue(nsCString(keyPart),
520 0 : nsCString(valuePart));
521 0 : if (NS_FAILED(rv)) {
522 0 : return NS_ERROR_UNEXPECTED;
523 : }
524 :
525 : // Now attempt to decode the score part as a uint32_t.
526 : // XXX nsDependentCSubstring doesn't support ToInteger
527 0 : int32_t integer = nsCString(scorePart).ToInteger(&rv);
528 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
529 0 : return rv;
530 : }
531 0 : if (integer < 0) {
532 0 : return NS_ERROR_UNEXPECTED;
533 : }
534 0 : aEntryOut.mScore = (uint32_t)integer;
535 :
536 0 : integer = nsCString(accessedPart).ToInteger(&rv);
537 0 : if (NS_FAILED(rv)) {
538 0 : return rv;
539 : }
540 0 : if (integer < 0) {
541 0 : return NS_ERROR_UNEXPECTED;
542 : }
543 0 : aEntryOut.mLastAccessed = integer;
544 :
545 : // Now set the key and value.
546 0 : aKeyOut.Assign(keyPart);
547 0 : aEntryOut.mValue.Assign(valuePart);
548 :
549 0 : return NS_OK;
550 : }
551 :
552 : nsresult
553 3 : DataStorage::AsyncReadData(bool& aHaveProfileDir,
554 : const MutexAutoLock& /*aProofOfLock*/)
555 : {
556 3 : MOZ_ASSERT(XRE_IsParentProcess());
557 3 : aHaveProfileDir = false;
558 : // Allocate a Reader so that even if it isn't dispatched,
559 : // the data-storage-ready notification will be fired and Get
560 : // will be able to proceed (this happens in its destructor).
561 6 : RefPtr<Reader> job(new Reader(this));
562 : nsresult rv;
563 : // If we don't have a profile directory, this will fail.
564 : // That's okay - it just means there is no persistent state.
565 3 : rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
566 6 : getter_AddRefs(mBackingFile));
567 3 : if (NS_FAILED(rv)) {
568 0 : mBackingFile = nullptr;
569 0 : return NS_OK;
570 : }
571 :
572 3 : rv = mBackingFile->Append(mFilename);
573 3 : if (NS_WARN_IF(NS_FAILED(rv))) {
574 0 : return rv;
575 : }
576 :
577 3 : rv = mWorkerThread->Dispatch(job, NS_DISPATCH_NORMAL);
578 3 : if (NS_WARN_IF(NS_FAILED(rv))) {
579 0 : return rv;
580 : }
581 :
582 3 : aHaveProfileDir = true;
583 3 : return NS_OK;
584 : }
585 :
586 : void
587 25 : DataStorage::WaitForReady()
588 : {
589 25 : MOZ_DIAGNOSTIC_ASSERT(mInitCalled, "Waiting before Init() has been called?");
590 :
591 50 : MonitorAutoLock readyLock(mReadyMonitor);
592 29 : while (!mReady) {
593 2 : nsresult rv = readyLock.Wait();
594 2 : if (NS_WARN_IF(NS_FAILED(rv))) {
595 0 : break;
596 : }
597 : }
598 25 : MOZ_ASSERT(mReady);
599 25 : }
600 :
601 : nsCString
602 22 : DataStorage::Get(const nsCString& aKey, DataStorageType aType)
603 : {
604 22 : WaitForReady();
605 44 : MutexAutoLock lock(mMutex);
606 :
607 44 : Entry entry;
608 22 : bool foundValue = GetInternal(aKey, &entry, aType, lock);
609 22 : if (!foundValue) {
610 22 : return EmptyCString();
611 : }
612 :
613 : // If we're here, we found a value. Maybe update its score.
614 0 : if (entry.UpdateScore()) {
615 0 : PutInternal(aKey, entry, aType, lock);
616 : }
617 :
618 0 : return entry.mValue;
619 : }
620 :
621 : bool
622 22 : DataStorage::GetInternal(const nsCString& aKey, Entry* aEntry,
623 : DataStorageType aType,
624 : const MutexAutoLock& aProofOfLock)
625 : {
626 22 : DataStorageTable& table = GetTableForType(aType, aProofOfLock);
627 22 : bool foundValue = table.Get(aKey, aEntry);
628 22 : return foundValue;
629 : }
630 :
631 : DataStorage::DataStorageTable&
632 31 : DataStorage::GetTableForType(DataStorageType aType,
633 : const MutexAutoLock& /*aProofOfLock*/)
634 : {
635 31 : switch (aType) {
636 : case DataStorage_Persistent:
637 25 : return mPersistentDataTable;
638 : case DataStorage_Temporary:
639 3 : return mTemporaryDataTable;
640 : case DataStorage_Private:
641 3 : return mPrivateDataTable;
642 : }
643 :
644 0 : MOZ_CRASH("given bad DataStorage storage type");
645 : }
646 :
647 : void
648 9 : DataStorage::ReadAllFromTable(DataStorageType aType,
649 : InfallibleTArray<dom::DataStorageItem>* aItems,
650 : const MutexAutoLock& aProofOfLock)
651 : {
652 18 : for (auto iter = GetTableForType(aType, aProofOfLock).Iter();
653 9 : !iter.Done(); iter.Next()) {
654 0 : DataStorageItem* item = aItems->AppendElement();
655 0 : item->key() = iter.Key();
656 0 : item->value() = iter.Data().mValue;
657 0 : item->type() = aType;
658 : }
659 9 : }
660 :
661 : void
662 3 : DataStorage::GetAll(InfallibleTArray<dom::DataStorageItem>* aItems)
663 : {
664 3 : WaitForReady();
665 6 : MutexAutoLock lock(mMutex);
666 :
667 9 : aItems->SetCapacity(mPersistentDataTable.Count() +
668 6 : mTemporaryDataTable.Count() +
669 6 : mPrivateDataTable.Count());
670 3 : ReadAllFromTable(DataStorage_Persistent, aItems, lock);
671 3 : ReadAllFromTable(DataStorage_Temporary, aItems, lock);
672 3 : ReadAllFromTable(DataStorage_Private, aItems, lock);
673 3 : }
674 :
675 : // Limit the number of entries per table. This is to prevent unbounded
676 : // resource use. The eviction strategy is as follows:
677 : // - An entry's score is incremented once for every day it is accessed.
678 : // - Evict an entry with score no more than any other entry in the table
679 : // (this is the same as saying evict the entry with the lowest score,
680 : // except for when there are multiple entries with the lowest score,
681 : // in which case one of them is evicted - which one is not specified).
682 : void
683 0 : DataStorage::MaybeEvictOneEntry(DataStorageType aType,
684 : const MutexAutoLock& aProofOfLock)
685 : {
686 0 : DataStorageTable& table = GetTableForType(aType, aProofOfLock);
687 0 : if (table.Count() >= sMaxDataEntries) {
688 0 : KeyAndEntry toEvict;
689 : // If all entries have score sMaxScore, this won't actually remove
690 : // anything. This will never happen, however, because having that high
691 : // a score either means someone tampered with the backing file or every
692 : // entry has been accessed once a day for ~4 billion days.
693 : // The worst that will happen is there will be 1025 entries in the
694 : // persistent data table, with the 1025th entry being replaced every time
695 : // data with a new key is inserted into the table. This is bad but
696 : // ultimately not that concerning, considering that if an attacker can
697 : // modify data in the profile, they can cause much worse harm.
698 0 : toEvict.mEntry.mScore = sMaxScore;
699 :
700 0 : for (auto iter = table.Iter(); !iter.Done(); iter.Next()) {
701 0 : Entry entry = iter.UserData();
702 0 : if (entry.mScore < toEvict.mEntry.mScore) {
703 0 : toEvict.mKey = iter.Key();
704 0 : toEvict.mEntry = entry;
705 : }
706 : }
707 :
708 0 : table.Remove(toEvict.mKey);
709 : }
710 0 : }
711 :
712 : template <class Functor>
713 : static
714 : void
715 0 : RunOnAllContentParents(Functor func)
716 : {
717 0 : if (!XRE_IsParentProcess()) {
718 0 : return;
719 : }
720 : using dom::ContentParent;
721 0 : nsTArray<ContentParent*> parents;
722 0 : ContentParent::GetAll(parents);
723 0 : for (auto& parent: parents) {
724 0 : func(parent);
725 : }
726 : }
727 :
728 : nsresult
729 0 : DataStorage::Put(const nsCString& aKey, const nsCString& aValue,
730 : DataStorageType aType)
731 : {
732 0 : WaitForReady();
733 0 : MutexAutoLock lock(mMutex);
734 :
735 : nsresult rv;
736 0 : rv = ValidateKeyAndValue(aKey, aValue);
737 0 : if (NS_FAILED(rv)) {
738 0 : return rv;
739 : }
740 :
741 0 : Entry entry;
742 0 : bool exists = GetInternal(aKey, &entry, aType, lock);
743 0 : if (exists) {
744 0 : entry.UpdateScore();
745 : } else {
746 0 : MaybeEvictOneEntry(aType, lock);
747 : }
748 0 : entry.mValue = aValue;
749 0 : rv = PutInternal(aKey, entry, aType, lock);
750 0 : if (NS_FAILED(rv)) {
751 0 : return rv;
752 : }
753 :
754 0 : RunOnAllContentParents([&](dom::ContentParent* aParent) {
755 0 : DataStorageItem item;
756 0 : item.key() = aKey;
757 0 : item.value() = aValue;
758 0 : item.type() = aType;
759 0 : Unused << aParent->SendDataStoragePut(mFilename, item);
760 0 : });
761 :
762 0 : return NS_OK;
763 : }
764 :
765 : nsresult
766 0 : DataStorage::PutInternal(const nsCString& aKey, Entry& aEntry,
767 : DataStorageType aType,
768 : const MutexAutoLock& aProofOfLock)
769 : {
770 0 : DataStorageTable& table = GetTableForType(aType, aProofOfLock);
771 0 : table.Put(aKey, aEntry);
772 :
773 0 : if (aType == DataStorage_Persistent && !mPendingWrite) {
774 0 : return AsyncSetTimer(aProofOfLock);
775 : }
776 :
777 0 : return NS_OK;
778 : }
779 :
780 : void
781 0 : DataStorage::Remove(const nsCString& aKey, DataStorageType aType)
782 : {
783 0 : WaitForReady();
784 0 : MutexAutoLock lock(mMutex);
785 :
786 0 : DataStorageTable& table = GetTableForType(aType, lock);
787 0 : table.Remove(aKey);
788 :
789 0 : if (aType == DataStorage_Persistent && !mPendingWrite) {
790 0 : Unused << AsyncSetTimer(lock);
791 : }
792 :
793 0 : RunOnAllContentParents([&](dom::ContentParent* aParent) {
794 0 : Unused << aParent->SendDataStorageRemove(mFilename, aKey, aType);
795 0 : });
796 0 : }
797 :
798 0 : class DataStorage::Writer : public Runnable
799 : {
800 : public:
801 0 : Writer(nsCString& aData, DataStorage* aDataStorage)
802 0 : : Runnable("DataStorage::Writer")
803 : , mData(aData)
804 0 : , mDataStorage(aDataStorage)
805 : {
806 0 : }
807 :
808 : private:
809 : NS_DECL_NSIRUNNABLE
810 :
811 : nsCString mData;
812 : RefPtr<DataStorage> mDataStorage;
813 : };
814 :
815 : NS_IMETHODIMP
816 0 : DataStorage::Writer::Run()
817 : {
818 : nsresult rv;
819 : // Concurrent operations on nsIFile objects are not guaranteed to be safe,
820 : // so we clone the file while holding the lock and then release the lock.
821 : // At that point, we can safely operate on the clone.
822 0 : nsCOMPtr<nsIFile> file;
823 : {
824 0 : MutexAutoLock lock(mDataStorage->mMutex);
825 : // If we don't have a profile, bail.
826 0 : if (!mDataStorage->mBackingFile) {
827 0 : return NS_OK;
828 : }
829 0 : rv = mDataStorage->mBackingFile->Clone(getter_AddRefs(file));
830 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
831 0 : return rv;
832 : }
833 : }
834 :
835 0 : nsCOMPtr<nsIOutputStream> outputStream;
836 0 : rv = NS_NewLocalFileOutputStream(getter_AddRefs(outputStream), file,
837 0 : PR_CREATE_FILE | PR_TRUNCATE | PR_WRONLY);
838 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
839 0 : return rv;
840 : }
841 :
842 0 : const char* ptr = mData.get();
843 0 : int32_t remaining = mData.Length();
844 0 : uint32_t written = 0;
845 0 : while (remaining > 0) {
846 0 : rv = outputStream->Write(ptr, remaining, &written);
847 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
848 0 : return rv;
849 : }
850 0 : remaining -= written;
851 0 : ptr += written;
852 : }
853 :
854 : // Observed by tests.
855 : nsCOMPtr<nsIRunnable> job =
856 0 : NewRunnableMethod<const char*>("DataStorage::NotifyObservers",
857 : mDataStorage,
858 : &DataStorage::NotifyObservers,
859 0 : "data-storage-written");
860 0 : rv = NS_DispatchToMainThread(job, NS_DISPATCH_NORMAL);
861 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
862 0 : return rv;
863 : }
864 :
865 0 : return NS_OK;
866 : }
867 :
868 : nsresult
869 0 : DataStorage::AsyncWriteData(const MutexAutoLock& /*aProofOfLock*/)
870 : {
871 0 : MOZ_ASSERT(XRE_IsParentProcess());
872 :
873 0 : if (mShuttingDown || !mBackingFile) {
874 0 : return NS_OK;
875 : }
876 :
877 0 : nsCString output;
878 0 : for (auto iter = mPersistentDataTable.Iter(); !iter.Done(); iter.Next()) {
879 0 : Entry entry = iter.UserData();
880 0 : output.Append(iter.Key());
881 0 : output.Append('\t');
882 0 : output.AppendInt(entry.mScore);
883 0 : output.Append('\t');
884 0 : output.AppendInt(entry.mLastAccessed);
885 0 : output.Append('\t');
886 0 : output.Append(entry.mValue);
887 0 : output.Append('\n');
888 : }
889 :
890 0 : RefPtr<Writer> job(new Writer(output, this));
891 0 : nsresult rv = mWorkerThread->Dispatch(job, NS_DISPATCH_NORMAL);
892 0 : mPendingWrite = false;
893 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
894 0 : return rv;
895 : }
896 :
897 0 : return NS_OK;
898 : }
899 :
900 : nsresult
901 0 : DataStorage::Clear()
902 : {
903 0 : WaitForReady();
904 0 : MutexAutoLock lock(mMutex);
905 0 : mPersistentDataTable.Clear();
906 0 : mTemporaryDataTable.Clear();
907 0 : mPrivateDataTable.Clear();
908 :
909 0 : if (XRE_IsParentProcess()) {
910 : // Asynchronously clear the file. This is similar to the permission manager
911 : // in that it doesn't wait to synchronously remove the data from its backing
912 : // storage either.
913 0 : nsresult rv = AsyncWriteData(lock);
914 0 : if (NS_FAILED(rv)) {
915 0 : return rv;
916 : }
917 : }
918 :
919 0 : RunOnAllContentParents([&](dom::ContentParent* aParent) {
920 0 : Unused << aParent->SendDataStorageClear(mFilename);
921 0 : });
922 :
923 0 : return NS_OK;
924 : }
925 :
926 : /* static */
927 : void
928 0 : DataStorage::TimerCallback(nsITimer* aTimer, void* aClosure)
929 : {
930 0 : MOZ_ASSERT(XRE_IsParentProcess());
931 :
932 0 : RefPtr<DataStorage> aDataStorage = (DataStorage*)aClosure;
933 0 : MutexAutoLock lock(aDataStorage->mMutex);
934 0 : Unused << aDataStorage->AsyncWriteData(lock);
935 0 : }
936 :
937 : // We only initialize the timer on the worker thread because it's not safe
938 : // to mix what threads are operating on the timer.
939 : nsresult
940 0 : DataStorage::AsyncSetTimer(const MutexAutoLock& /*aProofOfLock*/)
941 : {
942 0 : if (mShuttingDown || !XRE_IsParentProcess()) {
943 0 : return NS_OK;
944 : }
945 :
946 0 : mPendingWrite = true;
947 : nsCOMPtr<nsIRunnable> job =
948 0 : NewRunnableMethod("DataStorage::SetTimer", this, &DataStorage::SetTimer);
949 0 : nsresult rv = mWorkerThread->Dispatch(job, NS_DISPATCH_NORMAL);
950 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
951 0 : return rv;
952 : }
953 0 : return NS_OK;
954 : }
955 :
956 : void
957 0 : DataStorage::SetTimer()
958 : {
959 0 : MOZ_ASSERT(!NS_IsMainThread());
960 0 : MOZ_ASSERT(XRE_IsParentProcess());
961 :
962 0 : MutexAutoLock lock(mMutex);
963 :
964 : nsresult rv;
965 0 : if (!mTimer) {
966 0 : mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
967 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
968 0 : return;
969 : }
970 : }
971 :
972 0 : rv = mTimer->InitWithNamedFuncCallback(TimerCallback,
973 : this,
974 : mTimerDelay,
975 : nsITimer::TYPE_ONE_SHOT,
976 0 : "DataStorage::SetTimer");
977 0 : Unused << NS_WARN_IF(NS_FAILED(rv));
978 : }
979 :
980 : void
981 9 : DataStorage::NotifyObservers(const char* aTopic)
982 : {
983 : // Don't access the observer service off the main thread.
984 9 : if (!NS_IsMainThread()) {
985 0 : MOZ_ASSERT_UNREACHABLE("DataStorage::NotifyObservers called off main thread");
986 : return;
987 : }
988 :
989 18 : nsCOMPtr<nsIObserverService> os = services::GetObserverService();
990 9 : if (os) {
991 9 : os->NotifyObservers(nullptr, aTopic, mFilename.get());
992 : }
993 9 : }
994 :
995 : nsresult
996 0 : DataStorage::DispatchShutdownTimer(const MutexAutoLock& /*aProofOfLock*/)
997 : {
998 0 : MOZ_ASSERT(XRE_IsParentProcess());
999 :
1000 0 : nsCOMPtr<nsIRunnable> job = NewRunnableMethod(
1001 0 : "DataStorage::ShutdownTimer", this, &DataStorage::ShutdownTimer);
1002 0 : nsresult rv = mWorkerThread->Dispatch(job, NS_DISPATCH_NORMAL);
1003 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
1004 0 : return rv;
1005 : }
1006 0 : return NS_OK;
1007 : }
1008 :
1009 : void
1010 0 : DataStorage::ShutdownTimer()
1011 : {
1012 0 : MOZ_ASSERT(XRE_IsParentProcess());
1013 0 : MOZ_ASSERT(!NS_IsMainThread());
1014 0 : MutexAutoLock lock(mMutex);
1015 0 : nsresult rv = mTimer->Cancel();
1016 0 : Unused << NS_WARN_IF(NS_FAILED(rv));
1017 0 : mTimer = nullptr;
1018 0 : }
1019 :
1020 : //------------------------------------------------------------
1021 : // DataStorage::nsIObserver
1022 : //------------------------------------------------------------
1023 :
1024 : NS_IMETHODIMP
1025 0 : DataStorage::Observe(nsISupports* /*aSubject*/, const char* aTopic,
1026 : const char16_t* /*aData*/)
1027 : {
1028 : // Don't access preferences off the main thread.
1029 0 : if (!NS_IsMainThread()) {
1030 0 : MOZ_ASSERT_UNREACHABLE("DataStorage::Observe called off main thread");
1031 : return NS_ERROR_NOT_SAME_THREAD;
1032 : }
1033 :
1034 : nsresult rv;
1035 0 : if (strcmp(aTopic, "last-pb-context-exited") == 0) {
1036 0 : MutexAutoLock lock(mMutex);
1037 0 : mPrivateDataTable.Clear();
1038 0 : } else if (strcmp(aTopic, "profile-before-change") == 0 ||
1039 0 : (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0 &&
1040 0 : XRE_IsParentProcess())) {
1041 0 : MOZ_ASSERT(XRE_IsParentProcess());
1042 : // per bug 1271402, this should be safe to run multiple times
1043 : {
1044 0 : MutexAutoLock lock(mMutex);
1045 0 : rv = AsyncWriteData(lock);
1046 0 : mShuttingDown = true;
1047 0 : Unused << NS_WARN_IF(NS_FAILED(rv));
1048 0 : if (mTimer) {
1049 0 : rv = DispatchShutdownTimer(lock);
1050 0 : Unused << NS_WARN_IF(NS_FAILED(rv));
1051 : }
1052 : }
1053 : // Run the thread to completion and prevent any further events
1054 : // being scheduled to it. The thread may need the lock, so we can't
1055 : // hold it here.
1056 0 : rv = mWorkerThread->Shutdown();
1057 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
1058 0 : return rv;
1059 : }
1060 :
1061 0 : sDataStorages->Clear();
1062 0 : } else if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
1063 0 : MOZ_ASSERT(!XRE_IsParentProcess());
1064 0 : sDataStorages->Clear();
1065 0 : } else if (strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) == 0) {
1066 0 : MutexAutoLock lock(mMutex);
1067 0 : mTimerDelay = Preferences::GetInt("test.datastorage.write_timer_ms",
1068 : sDataStorageDefaultTimerDelay);
1069 : }
1070 :
1071 0 : return NS_OK;
1072 : }
1073 :
1074 22 : DataStorage::Entry::Entry()
1075 : : mScore(0)
1076 22 : , mLastAccessed((int32_t)(PR_Now() / sOneDayInMicroseconds))
1077 : {
1078 22 : }
1079 :
1080 : // Updates this entry's score. Returns true if the score has actually changed.
1081 : // If it's been less than a day since this entry has been accessed, the score
1082 : // does not change. Otherwise, the score increases by 1.
1083 : // The default score is 0. The maximum score is the maximum value that can
1084 : // be represented by an unsigned 32 bit integer.
1085 : // This is to handle evictions from our tables, which in turn is to prevent
1086 : // unbounded resource use.
1087 : bool
1088 0 : DataStorage::Entry::UpdateScore()
1089 : {
1090 :
1091 0 : int32_t nowInDays = (int32_t)(PR_Now() / sOneDayInMicroseconds);
1092 0 : int32_t daysSinceAccessed = (nowInDays - mLastAccessed);
1093 :
1094 : // Update the last accessed time.
1095 0 : mLastAccessed = nowInDays;
1096 :
1097 : // If it's been less than a day since we've been accessed,
1098 : // the score isn't updated.
1099 0 : if (daysSinceAccessed < 1) {
1100 0 : return false;
1101 : }
1102 :
1103 : // Otherwise, increment the score (but don't overflow).
1104 0 : if (mScore < sMaxScore) {
1105 0 : mScore++;
1106 : }
1107 0 : return true;
1108 : }
1109 :
1110 : } // namespace mozilla
|