Title: [292721] trunk
Revision
292721
Author
sihui_...@apple.com
Date
2022-04-11 13:59:13 -0700 (Mon, 11 Apr 2022)

Log Message

Fix size computation in WebCore::StorageMap
https://bugs.webkit.org/show_bug.cgi?id=239024
rdar://88249235

Reviewed by Chris Dumez.

Source/WebCore:

We use currentSize to track size for StorageMap. There are a few issues in current implementation that can make
currentSize incorrect and may lead to overflow:
1. When computing size of key, StorageMap uses parameter key instead of stored key. The problem is that
two Strings can be evaluated to equal while their sizeInBytes() value is different, when one String is 8-bit and
the other is 16-bit. That means removeItem() may decrease currentSize by wrong number (e.g setItem() with an
8-bit key, converting the key to 16-bit, removeItem() with the key). To fix this, StorageMap now always uses
stored key for computation.
2. When map.take(key) or map.get(key) returns null string, StorageMap takes it as the key does not exist and
will not correctly update currentSize, but user of WebCore::StorageMap may store null string as value. To fix
this, StorageMap now check if key exists with find() function.
3. StorageMap only uses CheckedUint32 in setItem(), but removeItem() and importItem() may cause overflow in
currentSize as mentioned above, and the error will not be caught until setItem() is called. To fix this,
StorageMap now uses CheckedUint32 in all places that update currentSize.

New test: WKWebView.LocalStorageNoSizeOverflow

* storage/StorageMap.cpp:
(WebCore::StorageMap::setItem):
(WebCore::StorageMap::removeItem):
(WebCore::StorageMap::importItems):

Tools:

* TestWebKitAPI/Tests/WebKitCocoa/LocalStoragePersistence.mm:
(TEST):

Modified Paths

Diff

Modified: trunk/Source/WebCore/ChangeLog (292720 => 292721)


--- trunk/Source/WebCore/ChangeLog	2022-04-11 20:49:06 UTC (rev 292720)
+++ trunk/Source/WebCore/ChangeLog	2022-04-11 20:59:13 UTC (rev 292721)
@@ -1,3 +1,32 @@
+2022-04-11  Sihui Liu  <sihui_...@apple.com>
+
+        Fix size computation in WebCore::StorageMap
+        https://bugs.webkit.org/show_bug.cgi?id=239024
+        rdar://88249235
+
+        Reviewed by Chris Dumez.
+
+        We use currentSize to track size for StorageMap. There are a few issues in current implementation that can make
+        currentSize incorrect and may lead to overflow:
+        1. When computing size of key, StorageMap uses parameter key instead of stored key. The problem is that
+        two Strings can be evaluated to equal while their sizeInBytes() value is different, when one String is 8-bit and 
+        the other is 16-bit. That means removeItem() may decrease currentSize by wrong number (e.g setItem() with an 
+        8-bit key, converting the key to 16-bit, removeItem() with the key). To fix this, StorageMap now always uses 
+        stored key for computation.
+        2. When map.take(key) or map.get(key) returns null string, StorageMap takes it as the key does not exist and 
+        will not correctly update currentSize, but user of WebCore::StorageMap may store null string as value. To fix 
+        this, StorageMap now check if key exists with find() function.
+        3. StorageMap only uses CheckedUint32 in setItem(), but removeItem() and importItem() may cause overflow in 
+        currentSize as mentioned above, and the error will not be caught until setItem() is called. To fix this, 
+        StorageMap now uses CheckedUint32 in all places that update currentSize.
+
+        New test: WKWebView.LocalStorageNoSizeOverflow
+
+        * storage/StorageMap.cpp:
+        (WebCore::StorageMap::setItem):
+        (WebCore::StorageMap::removeItem):
+        (WebCore::StorageMap::importItems):
+
 2022-04-11  Yusuke Suzuki  <ysuz...@apple.com>
 
         [JSC] Reduce use of unnecessary cryptographicallyRandom numbers

Modified: trunk/Source/WebCore/storage/StorageMap.cpp (292720 => 292721)


--- trunk/Source/WebCore/storage/StorageMap.cpp	2022-04-11 20:49:06 UTC (rev 292720)
+++ trunk/Source/WebCore/storage/StorageMap.cpp	2022-04-11 20:59:13 UTC (rev 292721)
@@ -88,30 +88,29 @@
 void StorageMap::setItem(const String& key, const String& value, String& oldValue, bool& quotaException)
 {
     ASSERT(!value.isNull());
+
     quotaException = false;
-
-    // Implement copy-on-write semantics.
-    if (m_impl->refCount() > 1)
-        m_impl = m_impl->copy();
-
-    oldValue = m_impl->map.get(key);
-
-    // Quota tracking. This is done in a couple of steps to keep the overflow tracking simple.
     CheckedUint32 newSize = m_impl->currentSize;
-    newSize -= oldValue.sizeInBytes();
+    auto iter = m_impl->map.find(key);
+    if (iter != m_impl->map.end()) {
+        oldValue = iter->value;
+        newSize -= oldValue.sizeInBytes();
+    } else {
+        oldValue = nullString();
+        newSize += key.sizeInBytes();
+    }
     newSize += value.sizeInBytes();
-    if (oldValue.isNull())
-        newSize += key.sizeInBytes();
     if (m_quotaSize != noQuota && (newSize.hasOverflowed() || newSize > m_quotaSize)) {
         quotaException = true;
         return;
     }
-    m_impl->currentSize = newSize;
 
-    HashMap<String, String>::AddResult addResult = m_impl->map.add(key, value);
-    if (!addResult.isNewEntry)
-        addResult.iterator->value = value;
+    // Implement copy-on-write semantics.
+    if (m_impl->refCount() > 1)
+        m_impl = m_impl->copy();
 
+    m_impl->map.set(key, value);
+    m_impl->currentSize = newSize;
     invalidateIterator();
 }
 
@@ -127,19 +126,21 @@
 
 void StorageMap::removeItem(const String& key, String& oldValue)
 {
+    oldValue = nullString();
+    CheckedUint32 newSize = m_impl->currentSize;
+    auto iter = m_impl->map.find(key);
+    if (iter == m_impl->map.end())
+        return;
+    oldValue = iter->value;
+    newSize = newSize - iter->key.sizeInBytes() - oldValue.sizeInBytes();
+
     // Implement copy-on-write semantics.
     if (m_impl->refCount() > 1)
         m_impl = m_impl->copy();
 
-    oldValue = m_impl->map.take(key);
-    if (oldValue.isNull())
-        return;
-
+    m_impl->map.remove(iter);
+    m_impl->currentSize = newSize;
     invalidateIterator();
-    ASSERT(m_impl->currentSize - key.sizeInBytes() <= m_impl->currentSize);
-    m_impl->currentSize -= key.sizeInBytes();
-    ASSERT(m_impl->currentSize - oldValue.sizeInBytes() <= m_impl->currentSize);
-    m_impl->currentSize -= oldValue.sizeInBytes();
 }
 
 void StorageMap::clear()
@@ -160,26 +161,24 @@
 
 void StorageMap::importItems(HashMap<String, String>&& items)
 {
+    CheckedUint32 newSize = m_impl->currentSize;
     if (m_impl->map.isEmpty()) {
-        // Fast path.
         m_impl->map = WTFMove(items);
-        for (auto& pair : m_impl->map) {
-            ASSERT(m_impl->currentSize + pair.key.sizeInBytes() + pair.value.sizeInBytes() >= m_impl->currentSize);
-            m_impl->currentSize += (pair.key.sizeInBytes() + pair.value.sizeInBytes());
+        for (auto& [key, value] : m_impl->map) {
+            newSize += key.sizeInBytes();
+            newSize += value.sizeInBytes();
         }
+        m_impl->currentSize = newSize;
         return;
     }
 
-    for (auto& item : items) {
-        auto& key = item.key;
-        auto& value = item.value;
-
-        ASSERT(m_impl->currentSize + key.sizeInBytes() + value.sizeInBytes() >= m_impl->currentSize);
-        m_impl->currentSize += (key.sizeInBytes() + value.sizeInBytes());
-
+    for (auto& [key, value] : items) {
+        newSize += key.sizeInBytes();
+        newSize += value.sizeInBytes();
         auto result = m_impl->map.add(WTFMove(key), WTFMove(value));
-        ASSERT_UNUSED(result, result.isNewEntry); // True if the key didn't exist previously.
+        ASSERT_UNUSED(result, result.isNewEntry);
     }
+    m_impl->currentSize = newSize;
 }
 
 Ref<StorageMap::Impl> StorageMap::Impl::copy() const

Modified: trunk/Tools/ChangeLog (292720 => 292721)


--- trunk/Tools/ChangeLog	2022-04-11 20:49:06 UTC (rev 292720)
+++ trunk/Tools/ChangeLog	2022-04-11 20:59:13 UTC (rev 292721)
@@ -1,3 +1,14 @@
+2022-04-11  Sihui Liu  <sihui_...@apple.com>
+
+        Fix size computation in WebCore::StorageMap
+        https://bugs.webkit.org/show_bug.cgi?id=239024
+        rdar://88249235
+
+        Reviewed by Chris Dumez.
+
+        * TestWebKitAPI/Tests/WebKitCocoa/LocalStoragePersistence.mm:
+        (TEST):
+
 2022-04-11  Wenson Hsieh  <wenson_hs...@apple.com>
 
         [Mail Compose] Preserve attachment identifiers when cloning attachment-backed images

Modified: trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/LocalStoragePersistence.mm (292720 => 292721)


--- trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/LocalStoragePersistence.mm	2022-04-11 20:49:06 UTC (rev 292720)
+++ trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/LocalStoragePersistence.mm	2022-04-11 20:59:13 UTC (rev 292721)
@@ -454,6 +454,62 @@
     runTest(false);
 }
 
+TEST(WKWebView, LocalStorageNoSizeOverflow)
+{
+    readyToContinue = false;
+    [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() {
+        readyToContinue = true;
+    }];
+    TestWebKitAPI::Util::run(&readyToContinue);
+
+    NSString *htmlString = @"<script> \
+        const key = '\u00FF\u00FF'; \
+        function onStorage() { window.webkit.messageHandlers.testHandler.postMessage('storage'); } \
+        function setItem() { localStorage.setItem(key, 'value'); } \
+        function removeItem() { localStorage.removeItem(localStorage.key(0)); } \
+        function getItem() { return localStorage.getItem(key) ? localStorage.getItem(key) : '[null]'; } \
+        window.addEventListener('storage', onStorage);\
+        window.webkit.messageHandlers.testHandler.postMessage(getItem()); \
+        </script>";
+    auto handler = adoptNS([[LocalStorageMessageHandler alloc] init]);
+    auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
+    [[configuration userContentController] addScriptMessageHandler:handler.get() name:@"testHandler"];
+
+    auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
+    [webView loadHTMLString:htmlString baseURL:[NSURL URLWithString:@"http://webkit.org"]];
+    TestWebKitAPI::Util::run(&receivedScriptMessage);
+    EXPECT_WK_STREQ(@"[null]", [lastScriptMessage body]);
+    receivedScriptMessage = false;
+
+    [webView evaluateJavaScript:@"setItem()" completionHandler:^(NSNumber *result, NSError *error) {
+        receivedScriptMessage = true;
+    }];
+    TestWebKitAPI::Util::run(&receivedScriptMessage);
+    receivedScriptMessage = false;
+    
+    auto secondWebView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
+    [secondWebView loadHTMLString:htmlString baseURL:[NSURL URLWithString:@"http://webkit.org"]];
+    TestWebKitAPI::Util::run(&receivedScriptMessage);
+    EXPECT_WK_STREQ(@"value", [lastScriptMessage body]);
+    receivedScriptMessage = false;
+
+    [secondWebView evaluateJavaScript:@"removeItem()" completionHandler:nil];
+    TestWebKitAPI::Util::run(&receivedScriptMessage);
+    EXPECT_WK_STREQ(@"storage", [lastScriptMessage body]);
+    receivedScriptMessage = false;
+
+    [secondWebView evaluateJavaScript:@"setItem()" completionHandler:nil];
+    TestWebKitAPI::Util::run(&receivedScriptMessage);
+    EXPECT_WK_STREQ(@"storage", [lastScriptMessage body]);
+    receivedScriptMessage = false;
+
+    [webView evaluateJavaScript:@"getItem()" completionHandler:^(NSString* result, NSError *error) {
+        EXPECT_WK_STREQ("value", [result UTF8String]);
+        receivedScriptMessage = true;
+    }];
+    TestWebKitAPI::Util::run(&receivedScriptMessage);
+}
+
 #if PLATFORM(IOS_FAMILY)
 
 TEST(WKWebView, LocalStorageDirectoryExcludedFromBackup)
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to