Crash in DOMTimer::fired
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 14 Feb 2019 06:43:13 +0000 (06:43 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 14 Feb 2019 06:43:13 +0000 (06:43 +0000)
https://bugs.webkit.org/show_bug.cgi?id=194638

Reviewed by Brent Fulgham.

Source/WebCore:

This patch continues the saga of hunting down timer related crashes after r239814, r225985, r227934.

The crash was caused by the bug that we don't remove a DOMTimer from NestedTimersMap if a DOMTimer
is created & installed inside another DOMTimer's callback (via execute call in DOMTimer::fired).

Fixed the crash by using a Ref in NestedTimersMap. This will keep the timer alive until we exit
from DOMTimer::fired. Because DOMTimer::fired always calls stopTracking() which clears the map
we would not leak these DOM timers.

We could, alternatively, use WeakPtr in NestedTimersMap but that would unnecessarily increase the
size of DOMTimer for a very marginal benefit of DOMTimer objcets being deleted slightly earlier.
Deleting itself in DOMTimer's destructor involves more logic & house keeping in the timer code,
and is no longer the preferred approach when dealing with these classes of bugs in WebKit.

Test: fast/dom/timer-destruction-during-firing.html

* page/DOMTimer.cpp:
(WebCore::NestedTimersMap::add):
(WebCore::DOMTimer::install):
(WebCore::DOMTimer::fired):

LayoutTests:

Added a regression test. It needs debug assertions without the fix.

* fast/dom/timer-destruction-during-firing-expected.txt: Added.
* fast/dom/timer-destruction-during-firing.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@241499 268f45cc-cd09-0410-ab3c-d52691b4dbfc

LayoutTests/ChangeLog
LayoutTests/fast/dom/timer-destruction-during-firing-expected.txt [new file with mode: 0644]
LayoutTests/fast/dom/timer-destruction-during-firing.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/page/DOMTimer.cpp

index bd9077b..cf31286 100644 (file)
@@ -1,3 +1,15 @@
+2019-02-13  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Crash in DOMTimer::fired
+        https://bugs.webkit.org/show_bug.cgi?id=194638
+
+        Reviewed by Brent Fulgham.
+
+        Added a regression test. It needs debug assertions without the fix.
+
+        * fast/dom/timer-destruction-during-firing-expected.txt: Added.
+        * fast/dom/timer-destruction-during-firing.html: Added.
+
 2019-02-13  Nikita Vasilyev  <nvasilyev@apple.com>
 
         Web Inspector: Styles: valid values in style attributes are reported as unsupported property values
diff --git a/LayoutTests/fast/dom/timer-destruction-during-firing-expected.txt b/LayoutTests/fast/dom/timer-destruction-during-firing-expected.txt
new file mode 100644 (file)
index 0000000..fa1b3eb
--- /dev/null
@@ -0,0 +1,3 @@
+This tests deleting DOMTimer inside another DOMTimer. WebKit should not hit any debug assertions.
+
+PASS
diff --git a/LayoutTests/fast/dom/timer-destruction-during-firing.html b/LayoutTests/fast/dom/timer-destruction-during-firing.html
new file mode 100644 (file)
index 0000000..7c8acd8
--- /dev/null
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<body>
+<p>This tests deleting DOMTimer inside another DOMTimer. WebKit should not hit any debug assertions.</p>
+<p id="result"></p>
+<script src="../../resources/gc.js"></script>
+<script>
+
+if (!window.testRunner)
+    document.getElementById('result').textContent = 'This test requires testRunner';
+else {
+    testRunner.dumpAsText();
+    testRunner.waitUntilDone();
+
+    setTimeout(() => {
+        for (let k = 0; k < 50; k++) {
+            const frames = [];
+            for (let i = 0; i < 1; i++)
+                frames[i] = createTimerInNewFrame();
+            for (const frame of frames)
+                frame.remove();
+            frames.length = 0;
+            gc();
+        }
+        self.postMessage('end', '*');
+    }, 0);
+
+    window.onmessage = () => {
+        document.getElementById('result').textContent = 'PASS';
+        testRunner.notifyDone();
+    }
+}
+
+function createTimerInNewFrame()
+{
+    const frame = document.createElement('iframe');
+    document.body.appendChild(frame);
+    frame.contentWindow.setTimeout(() => {}, 0);
+    return frame;
+}
+
+</script>
+</body>
+</html>
index b193533..d0130cd 100644 (file)
@@ -1,3 +1,31 @@
+2019-02-13  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Crash in DOMTimer::fired
+        https://bugs.webkit.org/show_bug.cgi?id=194638
+
+        Reviewed by Brent Fulgham.
+
+        This patch continues the saga of hunting down timer related crashes after r239814, r225985, r227934.
+
+        The crash was caused by the bug that we don't remove a DOMTimer from NestedTimersMap if a DOMTimer
+        is created & installed inside another DOMTimer's callback (via execute call in DOMTimer::fired).
+
+        Fixed the crash by using a Ref in NestedTimersMap. This will keep the timer alive until we exit
+        from DOMTimer::fired. Because DOMTimer::fired always calls stopTracking() which clears the map
+        we would not leak these DOM timers.
+
+        We could, alternatively, use WeakPtr in NestedTimersMap but that would unnecessarily increase the
+        size of DOMTimer for a very marginal benefit of DOMTimer objcets being deleted slightly earlier.
+        Deleting itself in DOMTimer's destructor involves more logic & house keeping in the timer code,
+        and is no longer the preferred approach when dealing with these classes of bugs in WebKit.
+
+        Test: fast/dom/timer-destruction-during-firing.html
+
+        * page/DOMTimer.cpp:
+        (WebCore::NestedTimersMap::add):
+        (WebCore::DOMTimer::install):
+        (WebCore::DOMTimer::fired):
+
 2019-02-13  Joseph Pecoraro  <pecoraro@apple.com>
 
         Web Inspector: Crash when inspecting an element that constantly changes visibility
index e61f7cb..e98f5f2 100644 (file)
@@ -113,7 +113,7 @@ private:
 DOMTimerFireState* DOMTimerFireState::current = nullptr;
 
 struct NestedTimersMap {
-    typedef HashMap<int, DOMTimer*>::const_iterator const_iterator;
+    typedef HashMap<int, Ref<DOMTimer>>::const_iterator const_iterator;
 
     static NestedTimersMap* instanceForContext(ScriptExecutionContext& context)
     {
@@ -139,10 +139,10 @@ struct NestedTimersMap {
         nestedTimers.clear();
     }
 
-    void add(int timeoutId, DOMTimer* timer)
+    void add(int timeoutId, Ref<DOMTimer>&& timer)
     {
         if (isTrackingNestedTimers)
-            nestedTimers.add(timeoutId, timer);
+            nestedTimers.add(timeoutId, WTFMove(timer));
     }
 
     void remove(int timeoutId)
@@ -162,7 +162,7 @@ private:
     }
 
     static bool isTrackingNestedTimers;
-    HashMap<int /* timeoutId */, DOMTimer*> nestedTimers;
+    HashMap<int /* timeoutId */, Ref<DOMTimer>> nestedTimers;
 };
 
 bool NestedTimersMap::isTrackingNestedTimers = false;
@@ -235,7 +235,7 @@ int DOMTimer::install(ScriptExecutionContext& context, std::unique_ptr<Scheduled
 
     // Keep track of nested timer installs.
     if (NestedTimersMap* nestedTimers = NestedTimersMap::instanceForContext(context))
-        nestedTimers->add(timer->m_timeoutId, timer);
+        nestedTimers->add(timer->m_timeoutId, *timer);
 
     return timer->m_timeoutId;
 }
@@ -390,8 +390,8 @@ void DOMTimer::fired()
 
     // Check if we should throttle nested single-shot timers.
     if (nestedTimers) {
-        for (auto& keyValue : *nestedTimers) {
-            auto* timer = keyValue.value;
+        for (auto& idAndTimer : *nestedTimers) {
+            auto& timer = idAndTimer.value;
             if (timer->isActive() && !timer->repeatInterval())
                 timer->updateThrottlingStateIfNecessary(fireState);
         }