Bug 1274075 - Throw UnknowError if the size of the serialized message is too large. r=janv
authorBevis Tseng <btseng@mozilla.com>
Mon, 17 Oct 2016 11:45:03 +0800
changeset 347363 1753c75341a6a238f7019aca461bc5eed7f006e7
parent 347362 150a4de2c5d9bd82d8fbb28fc7a3fb8d2949484f
child 347364 961a845748368c2d51a4c3fc97c55525e8cb7091
push id10298
push userraliiev@mozilla.com
push dateMon, 14 Nov 2016 12:33:03 +0000
treeherdermozilla-aurora@7e29173b1641 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjanv
bugs1274075
milestone52.0a1
Bug 1274075 - Throw UnknowError if the size of the serialized message is too large. r=janv
dom/indexedDB/IDBObjectStore.cpp
dom/indexedDB/IndexedDatabaseManager.cpp
dom/indexedDB/IndexedDatabaseManager.h
dom/indexedDB/test/mochitest.ini
dom/indexedDB/test/test_maximal_serialized_object_size.html
dom/indexedDB/test/unit/test_maximal_serialized_object_size.js
dom/indexedDB/test/unit/xpcshell-head-parent-process.js
dom/indexedDB/test/unit/xpcshell-parent-process.ini
--- a/dom/indexedDB/IDBObjectStore.cpp
+++ b/dom/indexedDB/IDBObjectStore.cpp
@@ -1424,16 +1424,44 @@ IDBObjectStore::AddOrPut(JSContext* aCx,
   StructuredCloneWriteInfo cloneWriteInfo(mTransaction->Database());
   nsTArray<IndexUpdateInfo> updateInfo;
 
   aRv = GetAddInfo(aCx, value, aKey, cloneWriteInfo, key, updateInfo);
   if (aRv.Failed()) {
     return nullptr;
   }
 
+  // Check the size limit of the serialized message which mainly consists of
+  // a StructuredCloneBuffer, an encoded object key, and the encoded index keys.
+  // kMaxIDBMsgOverhead covers the minor stuff not included in this calculation
+  // because the precise calculation would slow down this AddOrPut operation.
+  static const size_t kMaxIDBMsgOverhead = 1024 * 1024; // 1MB
+  const uint32_t maximalSizeFromPref =
+    IndexedDatabaseManager::MaxSerializedMsgSize();
+  MOZ_ASSERT(maximalSizeFromPref > kMaxIDBMsgOverhead);
+  const size_t kMaxMessageSize = maximalSizeFromPref - kMaxIDBMsgOverhead;
+
+  size_t indexUpdateInfoSize = 0;
+  for (size_t i = 0; i < updateInfo.Length(); i++) {
+    indexUpdateInfoSize += updateInfo[i].value().GetBuffer().Length();
+    indexUpdateInfoSize += updateInfo[i].localizedValue().GetBuffer().Length();
+  }
+
+  size_t messageSize = cloneWriteInfo.mCloneBuffer.data().Size() +
+    key.GetBuffer().Length() + indexUpdateInfoSize;
+
+  if (messageSize > kMaxMessageSize) {
+    IDB_REPORT_INTERNAL_ERR();
+    aRv.ThrowDOMException(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR,
+      nsPrintfCString("The serialized value is too large"
+                      " (size=%zu bytes, max=%zu bytes).",
+                      messageSize, kMaxMessageSize));
+    return nullptr;
+  }
+
   ObjectStoreAddPutParams commonParams;
   commonParams.objectStoreId() = Id();
   commonParams.cloneInfo().data().data = Move(cloneWriteInfo.mCloneBuffer.data());
   commonParams.cloneInfo().offsetToKeyProp() = cloneWriteInfo.mOffsetToKeyProp;
   commonParams.key() = key;
   commonParams.indexUpdateInfos().SwapElements(updateInfo);
 
   // Convert any blobs or mutable files into FileAddInfo.
--- a/dom/indexedDB/IndexedDatabaseManager.cpp
+++ b/dom/indexedDB/IndexedDatabaseManager.cpp
@@ -1,16 +1,17 @@
 /* -*- 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 "IndexedDatabaseManager.h"
 
+#include "chrome/common/ipc_channel.h" // for IPC::Channel::kMaximumMessageSize
 #include "nsIConsoleService.h"
 #include "nsIDiskSpaceWatcher.h"
 #include "nsIDOMWindow.h"
 #include "nsIEventTarget.h"
 #include "nsIFile.h"
 #include "nsIObserverService.h"
 #include "nsIScriptError.h"
 #include "nsIScriptGlobalObject.h"
@@ -133,22 +134,26 @@ NS_DEFINE_IID(kIDBRequestIID, PRIVATE_ID
 
 const uint32_t kDeleteTimeoutMs = 1000;
 
 // The threshold we use for structured clone data storing.
 // Anything smaller than the threshold is compressed and stored in the database.
 // Anything larger is compressed and stored outside the database.
 const int32_t kDefaultDataThresholdBytes = 1024 * 1024; // 1MB
 
+// The maximal size of a serialized object to be transfered through IPC.
+const int32_t kDefaultMaxSerializedMsgSize = IPC::Channel::kMaximumMessageSize;
+
 #define IDB_PREF_BRANCH_ROOT "dom.indexedDB."
 
 const char kTestingPref[] = IDB_PREF_BRANCH_ROOT "testing";
 const char kPrefExperimental[] = IDB_PREF_BRANCH_ROOT "experimental";
 const char kPrefFileHandle[] = "dom.fileHandle.enabled";
 const char kDataThresholdPref[] = IDB_PREF_BRANCH_ROOT "dataThreshold";
+const char kPrefMaxSerilizedMsgSize[] = IDB_PREF_BRANCH_ROOT "maxSerializedMsgSize";
 
 #define IDB_PREF_LOGGING_BRANCH_ROOT IDB_PREF_BRANCH_ROOT "logging."
 
 const char kPrefLoggingEnabled[] = IDB_PREF_LOGGING_BRANCH_ROOT "enabled";
 const char kPrefLoggingDetails[] = IDB_PREF_LOGGING_BRANCH_ROOT "details";
 
 #if defined(DEBUG) || defined(MOZ_ENABLE_PROFILER_SPS)
 const char kPrefLoggingProfiler[] =
@@ -161,16 +166,17 @@ const char kPrefLoggingProfiler[] =
 StaticRefPtr<IndexedDatabaseManager> gDBManager;
 
 Atomic<bool> gInitialized(false);
 Atomic<bool> gClosed(false);
 Atomic<bool> gTestingMode(false);
 Atomic<bool> gExperimentalFeaturesEnabled(false);
 Atomic<bool> gFileHandleEnabled(false);
 Atomic<int32_t> gDataThresholdBytes(0);
+Atomic<int32_t> gMaxSerializedMsgSize(0);
 
 class DeleteFilesRunnable final
   : public nsIRunnable
   , public OpenDirectoryListener
 {
   typedef mozilla::dom::quota::DirectoryLock DirectoryLock;
 
   enum State
@@ -264,16 +270,28 @@ DataThresholdPrefChangedCallback(const c
   // The magic -1 is for use only by tests that depend on stable blob file id's.
   if (dataThresholdBytes == -1) {
     dataThresholdBytes = INT32_MAX;
   }
 
   gDataThresholdBytes = dataThresholdBytes;
 }
 
+void
+MaxSerializedMsgSizePrefChangeCallback(const char* aPrefName, void* aClosure)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(!strcmp(aPrefName, kPrefMaxSerilizedMsgSize));
+  MOZ_ASSERT(!aClosure);
+
+  gMaxSerializedMsgSize =
+    Preferences::GetInt(aPrefName, kDefaultMaxSerializedMsgSize);
+  MOZ_ASSERT(gMaxSerializedMsgSize > 0);
+}
+
 } // namespace
 
 IndexedDatabaseManager::IndexedDatabaseManager()
   : mFileMutex("IndexedDatabaseManager.mFileMutex")
   , mBackgroundActor(nullptr)
 {
   NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
 }
@@ -404,16 +422,19 @@ IndexedDatabaseManager::Init()
                                 kPrefLoggingProfiler);
 #endif
   Preferences::RegisterCallbackAndCall(LoggingModePrefChangedCallback,
                                        kPrefLoggingEnabled);
 
   Preferences::RegisterCallbackAndCall(DataThresholdPrefChangedCallback,
                                        kDataThresholdPref);
 
+  Preferences::RegisterCallbackAndCall(MaxSerializedMsgSizePrefChangeCallback,
+                                       kPrefMaxSerilizedMsgSize);
+
 #ifdef ENABLE_INTL_API
   const nsAdoptingCString& acceptLang =
     Preferences::GetLocalizedCString("intl.accept_languages");
 
   // Split values on commas.
   nsCCharSeparatedTokenizer langTokenizer(acceptLang, ',');
   while (langTokenizer.hasMoreTokens()) {
     nsAutoCString lang(langTokenizer.nextToken());
@@ -467,16 +488,19 @@ IndexedDatabaseManager::Destroy()
                                   kPrefLoggingProfiler);
 #endif
   Preferences::UnregisterCallback(LoggingModePrefChangedCallback,
                                   kPrefLoggingEnabled);
 
   Preferences::UnregisterCallback(DataThresholdPrefChangedCallback,
                                   kDataThresholdPref);
 
+  Preferences::UnregisterCallback(MaxSerializedMsgSizePrefChangeCallback,
+                                  kPrefMaxSerilizedMsgSize);
+
   delete this;
 }
 
 // static
 nsresult
 IndexedDatabaseManager::CommonPostHandleEvent(EventChainPostVisitor& aVisitor,
                                               IDBFactory* aFactory)
 {
@@ -778,16 +802,27 @@ uint32_t
 IndexedDatabaseManager::DataThreshold()
 {
   MOZ_ASSERT(gDBManager,
              "DataThreshold() called before indexedDB has been initialized!");
 
   return gDataThresholdBytes;
 }
 
+// static
+uint32_t
+IndexedDatabaseManager::MaxSerializedMsgSize()
+{
+  MOZ_ASSERT(gDBManager,
+             "MaxSerializedMsgSize() called before indexedDB has been initialized!");
+  MOZ_ASSERT(gMaxSerializedMsgSize > 0);
+
+  return gMaxSerializedMsgSize;
+}
+
 void
 IndexedDatabaseManager::ClearBackgroundActor()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   mBackgroundActor = nullptr;
 }
 
--- a/dom/indexedDB/IndexedDatabaseManager.h
+++ b/dom/indexedDB/IndexedDatabaseManager.h
@@ -129,16 +129,19 @@ public:
   ExperimentalFeaturesEnabled(JSContext* aCx, JSObject* aGlobal);
 
   static bool
   IsFileHandleEnabled();
 
   static uint32_t
   DataThreshold();
 
+  static uint32_t
+  MaxSerializedMsgSize();
+
   void
   ClearBackgroundActor();
 
   void
   NoteLiveQuotaManager(QuotaManager* aQuotaManager);
 
   void
   NoteShuttingDownQuotaManager();
--- a/dom/indexedDB/test/mochitest.ini
+++ b/dom/indexedDB/test/mochitest.ini
@@ -62,16 +62,17 @@ support-files =
   unit/test_invalid_version.js
   unit/test_invalidate.js
   unit/test_key_requirements.js
   unit/test_keys.js
   unit/test_locale_aware_indexes.js
   unit/test_locale_aware_index_getAll.js
   unit/test_locale_aware_index_getAllObjects.js
   unit/test_lowDiskSpace.js
+  unit/test_maximal_serialized_object_size.js
   unit/test_multientry.js
   unit/test_names_sorted.js
   unit/test_objectCursors.js
   unit/test_objectStore_getAllKeys.js
   unit/test_objectStore_inline_autoincrement_key_added_on_put.js
   unit/test_objectStore_openKeyCursor.js
   unit/test_objectStore_remove_values.js
   unit/test_object_identity.js
@@ -290,16 +291,17 @@ skip-if = true
 [test_key_requirements.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_keys.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_leaving_page.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_lowDiskSpace.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
+[test_maximal_serialized_object_size.html]
 [test_message_manager_ipc.html]
 # This test is only supposed to run in the main process.
 skip-if = buildapp == 'b2g' || buildapp == 'mulet' || e10s
 [test_multientry.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_names_sorted.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_objectCursors.html]
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/test_maximal_serialized_object_size.html
@@ -0,0 +1,18 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+  <title>Test Maximal Size of a Serialized Object</title>
+
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+  <script type="text/javascript;version=1.7" src="unit/test_maximal_serialized_object_size.js"></script>
+  <script type="text/javascript;version=1.7" src="helpers.js"></script>
+</head>
+
+<body onload="runTest();"></body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/unit/test_maximal_serialized_object_size.js
@@ -0,0 +1,95 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var disableWorkerTest = "Need a way to set temporary prefs from a worker";
+
+var testGenerator = testSteps();
+
+function testSteps()
+{
+  const name = this.window ?
+    window.location.pathname : "test_maximal_serialized_object_size.js";
+  const megaBytes = 1024 * 1024;
+  const kMessageOverhead = 1; // in MB
+  const kMaxIpcMessageSize = 20; // in MB
+  const kMaxIdbMessageSize = kMaxIpcMessageSize - kMessageOverhead;
+
+  let chunks = new Array(kMaxIdbMessageSize);
+  for (let i = 0; i < kMaxIdbMessageSize; i++) {
+    chunks[i] = new ArrayBuffer(1 * megaBytes);
+  }
+
+  if (this.window) {
+    SpecialPowers.pushPrefEnv(
+      { "set": [["dom.indexedDB.maxSerializedMsgSize",
+                 kMaxIpcMessageSize * megaBytes ]]
+      },
+      continueToNextStep
+    );
+    yield undefined;
+  } else {
+    setMaxSerializedMsgSize(kMaxIpcMessageSize * megaBytes);
+  }
+
+  let openRequest = indexedDB.open(name, 1);
+  openRequest.onerror = errorHandler;
+  openRequest.onupgradeneeded = grabEventAndContinueHandler;
+  openRequest.onsuccess = unexpectedSuccessHandler;
+  let event = yield undefined;
+
+  let db = event.target.result;
+  let txn = event.target.transaction;
+
+  is(db.objectStoreNames.length, 0, "Correct objectStoreNames list");
+
+  let objectStore = db.createObjectStore("test store", { keyPath: "id" });
+  is(db.objectStoreNames.length, 1, "Correct objectStoreNames list");
+  is(db.objectStoreNames.item(0), objectStore.name, "Correct object store name");
+
+  function testTooLargeError(aOperation, aObject) {
+    try {
+      objectStore[aOperation](aObject).onerror = errorHandler;
+      ok(false, "UnknownError is expected to be thrown!");
+    } catch (e) {
+      ok(e instanceof DOMException, "got a DOM exception");
+      is(e.name, "UnknownError", "correct error");
+      ok(!!e.message, "Error message: " + e.message);
+      ok(e.message.startsWith("The serialized value is too large"),
+         "Correct error message prefix.");
+    }
+  }
+
+  info("Verify IDBObjectStore.add() - object is too large");
+  testTooLargeError("add", { id: 1, data: chunks });
+
+  info("Verify IDBObjectStore.add() - object size is closed to the maximal size.");
+  chunks.length = chunks.length - 1;
+  let request = objectStore.add({ id: 1, data: chunks });
+  request.onerror = errorHandler;
+  request.onsuccess = grabEventAndContinueHandler;
+  yield undefined;
+
+  info("Verify IDBObjectStore.add() - object key is too large");
+  chunks.length = 10;
+  testTooLargeError("add", { id: chunks });
+
+  objectStore.createIndex("index name", "index");
+  ok(objectStore.index("index name"), "Index created.");
+
+  info("Verify IDBObjectStore.add() - index key is too large");
+  testTooLargeError("add", { id: 2, index: chunks });
+
+  info("Verify IDBObjectStore.add() - object key and index key are too large");
+  let indexChunks = chunks.splice(0, 5);
+  testTooLargeError("add", { id: chunks, index: indexChunks });
+
+  openRequest.onsuccess = continueToNextStep;
+  yield undefined;
+
+  db.close();
+
+  finishTest();
+  yield undefined;
+}
--- a/dom/indexedDB/test/unit/xpcshell-head-parent-process.js
+++ b/dom/indexedDB/test/unit/xpcshell-head-parent-process.js
@@ -525,16 +525,22 @@ function setTemporaryStorageLimit(limit)
 }
 
 function setDataThreshold(threshold)
 {
   info("Setting data threshold to " + threshold);
   SpecialPowers.setIntPref("dom.indexedDB.dataThreshold", threshold);
 }
 
+function setMaxSerializedMsgSize(aSize)
+{
+  info("Setting maximal size of a serialized message to " + aSize);
+  SpecialPowers.setIntPref("dom.indexedDB.maxSerializedMsgSize", aSize);
+}
+
 function getPrincipal(url)
 {
   let uri = Cc["@mozilla.org/network/io-service;1"]
               .getService(Ci.nsIIOService)
               .newURI(url, null, null);
   let ssm = Cc["@mozilla.org/scriptsecuritymanager;1"]
               .getService(Ci.nsIScriptSecurityManager);
   return ssm.createCodebasePrincipal(uri, {});
--- a/dom/indexedDB/test/unit/xpcshell-parent-process.ini
+++ b/dom/indexedDB/test/unit/xpcshell-parent-process.ini
@@ -39,16 +39,17 @@ support-files =
 [test_idbSubdirUpgrade.js]
 [test_globalObjects_ipc.js]
 skip-if = toolkit == 'android'
 [test_idle_maintenance.js]
 [test_invalidate.js]
 # disabled for the moment.
 skip-if = true
 [test_lowDiskSpace.js]
+[test_maximal_serialized_object_size.js]
 [test_metadata2Restore.js]
 [test_metadataRestore.js]
 [test_mutableFileUpgrade.js]
 [test_oldDirectories.js]
 [test_quotaExceeded_recovery.js]
 [test_readwriteflush_disabled.js]
 [test_schema18upgrade.js]
 [test_schema21upgrade.js]