Medium webkit Integer Overflow 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactProcessing maliciously crafted web content may lead to memory corruption
DescriptionThe issue was addressed with improved memory handling.
ComponentWebCore
Bug ClassInteger Overflow
Tracker318405
Fix commit34249048d66d (webkit) +24/-13
CISA KEVNot listed
CreditedHenock Habte
Disclosed2026-09-14

Background

SVG SMIL animation
WebCore’s declarative-animation engine (<animate>, <set>, etc.). SVGSMILElement drives each animation element’s timing model (begin/end intervals, repeats) and is advanced by progress() on every sampling tick.
`repeatingDuration()`
the total time the animation is active across all repeats. With repeatCount="indefinite" this is SMILTime::indefinite(); otherwise it is repeatCount * simpleDuration.
`simpleDuration()`
the duration of one iteration (the dur attribute). It is fully page-controlled and may be arbitrarily small (dur="0.0001s").
`setCurrentTime()`
a scriptable DOM API that lets the page seek the SVG timeline to any elapsed value instantly (the PoC seeks to 400000 seconds).
`repeat` (local `unsigned`)
the integer index of the current repeat iteration, derived by dividing an elapsed/duration SMILTime (a double) by simpleDuration. It is later used to drive a loop that dispatches one repeatEvent per skipped iteration.

Root Cause Analysis

In SVGSMILElement::calculateAnimationPercentAndRepeat (Source/WebCore/svg/animation/SVGSMILElement.cpp), the repeat index was computed with a bare narrowing cast: repeat = static_cast<unsigned>(repeatingDuration.value() / simpleDuration.value()) (and the symmetric activeTime.value() / simpleDuration.value() branch). All operands are page-controlled doubles: by combining a tiny dur with a huge seek via setCurrentTime(400000), the quotient becomes an enormous value. Casting a double that exceeds UINT_MAX to unsigned is undefined behaviour in C++ and in practice yields a garbage/huge iteration count. The patch replaces both casts with clampTo<unsigned>(...), which saturates the value into the representable unsigned range, and additionally guards the --repeat decrement with if (repeat && ...) so it cannot wrap 0 to 0xFFFFFFFF.

The second hunk, in SVGSMILElement::progress, shows why the unbounded repeat was dangerous. The old code looped for (unsigned i = 0; i < repeat - 1; ++i) and called smilEventSender().dispatchEventSoon(*this, repeatEventEvent) once per iteration — so an attacker-controlled repeat of (effectively) billions enqueued that many DOM events in a single synchronous progress tick, an unbounded allocation/compute amplification driven entirely by page content. The patch removes the per-iteration loop entirely and coalesces the skipped iterations into a single event: if (repeat > 1 || (repeat && m_activeState == Inactive)) dispatchEventSoon(...). Note the advisory labels this “memory corruption”; the diff substantiates an unsigned integer overflow feeding an unbounded event-dispatch loop (resource exhaustion). A clean controlled write is not shown by the diff; any heap corruption would be a downstream consequence of the unbounded allocation (inferred).

Key insight
A page-controlled double iteration count was narrowed to unsigned with an unchecked static_cast and then used as a loop bound for synchronous DOM-event dispatch, so a single setCurrentTime seek on a near-zero-duration animation produced a practically unbounded number of event allocations.

Attack Path

  1. Setup Attacker serves a page containing an <svg> with <animate dur="0.0001s" repeatCount="indefinite"> on a child element.
  2. Trigger Script calls svg.setCurrentTime(400000), seeking elapsed far past the interval.
  3. Overflow calculateAnimationPercentAndRepeat divides the huge activeTime/repeatingDuration by the tiny simpleDuration; the double result vastly exceeds UINT_MAX and the static_cast<unsigned> yields a garbage/huge repeat.
  4. Amplification progress() executes for (i = 0; i < repeat - 1; ++i) dispatchEventSoon(repeatEvent), enqueuing an unbounded number of SMIL events in one tick.
  5. Primitive Unbounded memory/CPU consumption inside the renderer (denial of service); escalation to memory corruption is not demonstrated by the diff (inferred).

Impact Assessment

This executes in the renderer process with no special privilege — any web page that can create SVG and call setCurrentTime reaches the sink. The demonstrated primitive is an integer-overflow-driven unbounded event-dispatch loop: a renderer hang / out-of-memory denial of service. The Apple advisory’s “memory corruption” wording is not substantiated as a direct controlled write by this diff; the committed regression test only asserts “Passes if it does not crash,” consistent with a crash/DoS fix. Any further corruption would depend on allocator behaviour under exhaustion and is inferred, not shown.

Changed Functions

FunctionChangeNotes
if
Source/WebCore/svg/animation/SVGSMILElement.cpp
modified

Files Changed

  • LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash-expected.txt
  • LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash.html
  • Source/WebCore/svg/animation/SVGSMILElement.cpp

Audit Directions

  • Double→integer narrowing in SMIL timing
    Audit other static_cast<unsigned>/static_cast<int> of SMILTime::value() quotients across SVGSMILElement and SVGAnimationElement (interval/percent/next-progress-time math) for the same unchecked narrowing.
  • Loop bounds derived from page-controlled counts
    Look for other sites that loop 0..count dispatching events or allocating where count comes from durations, repeatCount, or setCurrentTime-seekable state without clampTo.
  • Event-sender amplification
    Review SMILEventSender/dispatchEventSoon call sites that can be driven in a single synchronous tick for unbounded enqueue.
diff --git a/Source/WebCore/svg/animation/SVGSMILElement.cpp b/Source/WebCore/svg/animation/SVGSMILElement.cpp
index 8bd88254df52..8a9d6c1670b1 100644
--- a/Source/WebCore/svg/animation/SVGSMILElement.cpp
+++ b/Source/WebCore/svg/animation/SVGSMILElement.cpp
@@ -1049,12 +1049,13 @@ float SVGSMILElement::calculateAnimationPercentAndRepeat(SMILTime elapsed, unsig
     SMILTime activeTime = elapsed - m_intervalBegin;
     SMILTime repeatingDuration = this->repeatingDuration();
 
+    // Clamp the page-controlled repeat count to prevent overflow.
     if ((elapsed >= m_intervalEnd && !repeatingDuration.isIndefinite()) || activeTime > repeatingDuration) {
-        repeat = static_cast<unsigned>(repeatingDuration.value() / simpleDuration.value());
-        if (!fmod(repeatingDuration.value(), simpleDuration.value()))
+        repeat = clampTo<unsigned>(repeatingDuration.value() / simpleDuration.value());
+        if (repeat && !fmod(repeatingDuration.value(), simpleDuration.value()))
             --repeat;
     } else
-        repeat = static_cast<unsigned>(activeTime.value() / simpleDuration.value());
+        repeat = clampTo<unsigned>(activeTime.value() / simpleDuration.value());
 
     double percent;
     if (elapsed >= m_intervalEnd || activeTime > repeatingDuration) {
@@ -1187,16 +1188,9 @@ bool SVGSMILElement::progress(SMILTime elapsed, SVGSMILElement& firstAnimation,
         if (m_activeState == Inactive || m_activeState == Frozen)
             smilEventSender().dispatchEventSoon(*this, eventNames().endEventEvent);
 
-        if (repeat) {
-            // We intentionally dispatch repeat - 1 events here because the first repeat
-            // event (for the initial loop) is sent elsewhere during continuous animation run.
-            // If repeat == 1, no events are dispatched here.
-            for (unsigned i = 0; i < repeat - 1; ++i)
-                smilEventSender().dispatchEventSoon(*this, eventNames().repeatEventEvent);
-
-            if (m_activeState == Inactive)
-                smilEventSender().dispatchEventSoon(*this, eventNames().repeatEventEvent);
-        }
+        // Coalesce the skipped repeat iterations into a single event instead of one per interval.
+        if (repeat > 1 || (repeat && m_activeState == Inactive))
+            smilEventSender().dispatchEventSoon(*this, eventNames().repeatEventEvent);
     }
 
     m_nextProgressTime = calculateNextProgressTime(elapsed);
diff --git a/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash-expected.txt b/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash-expected.txt
new file mode 100644
index 000000000000..cd68e3612acf
--- /dev/null
+++ b/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash-expected.txt
@@ -0,0 +1,3 @@
+Passes if it does not crash.
+
+
diff --git a/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash.html b/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash.html
new file mode 100644
index 000000000000..c30fa119a3e8
--- /dev/null
+++ b/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash.html
@@ -0,0 +1,14 @@
+<body>
+    <p>Passes if it does not crash.</p>
+    <svg id="svg">
+        <rect width="100" height="100" fill="green">
+            <animate attributeName="x" from="0" to="10" dur="0.0001s" repeatCount="indefinite"/>
+        </rect>
+    </svg>
+    <script>
+        if (window.testRunner)
+            testRunner.dumpAsText();
+
+        svg.setCurrentTime(400000);
+    </script>
+</body>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash-expected.txt b/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash-expected.txt
new file mode 100644
index 000000000000..cd68e3612acf
--- /dev/null
+++ b/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash-expected.txt
@@ -0,0 +1,3 @@
+Passes if it does not crash.
+
+
diff --git a/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash.html b/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash.html
new file mode 100644
index 000000000000..c30fa119a3e8
--- /dev/null
+++ b/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash.html
@@ -0,0 +1,14 @@
+<body>
+    <p>Passes if it does not crash.</p>
+    <svg id="svg">
+        <rect width="100" height="100" fill="green">
+            <animate attributeName="x" from="0" to="10" dur="0.0001s" repeatCount="indefinite"/>
+        </rect>
+    </svg>
+    <script>
+        if (window.testRunner)
+            testRunner.dumpAsText();
+
+        svg.setCurrentTime(400000);
+    </script>
+</body>
Loading diff…