Medium CVSS 5.4 webkit Integer Overflow 🔧 Commit mapped

Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to memory corruption
ComponentWebCore SVG
Bug ClassInteger Overflow
Tracker318405
Fix commit34249048d66d (WebKit/WebKit) +24/-13
CWECWE-119, CWE-416 (Buffer bounds error, Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N
CISA KEVNot listed
CreditedHenock Habte
Disclosed2026-08-17

Background

SVG SMIL animation
Declarative SVG animation; a page controls attributes like dur (simpleDuration) and repeatCount, which drive timing math in SVGSMILElement.
clampTo vs static_cast
static_cast<unsigned> of a huge floating ratio wraps; clampTo<unsigned>() saturates to the max instead, preventing the overflow.
Repeat-event dispatch
progress() previously queued one repeat event per skipped iteration; a huge repeat count queued near-unbounded events.

Root Cause Analysis

This fixes an unsigned overflow / unbounded work driven by a page-controlled SMIL repeat count. In SVGSMILElement::calculateAnimationPercentAndRepeat, the repeat count was computed with static_cast<unsigned>(repeatingDuration/simpleDuration) (and activeTime/simpleDuration). For an animation with a tiny simpleDuration and an indefinite/huge repeat, seeking to a large currentTime makes that ratio enormous, and the raw cast overflows/wraps.

The fix uses clampTo<unsigned>(…) for both computations and guards the decrement with if (repeat && !fmod(...)) to avoid underflow when repeat is 0. More importantly, SVGSMILElement::progress() previously dispatched one repeat event per skipped iteration in a for (i = 0; i < repeat - 1; ++i) loop; with an astronomically large repeat this queued a near-unbounded number of events (and repeat-1 underflows when repeat==0), exhausting memory and corrupting state.

The fix coalesces that into a single dispatch: if (repeat > 1 || (repeat && m_activeState == Inactive)) dispatchEventSoon(repeatEvent).

The restored invariants are a bounded, non-wrapping repeat count and O(1) event dispatch regardless of repeat magnitude. The regression test animates dur=0.0001s repeatCount=indefinite and calls setCurrentTime(400000) to force a huge repeat. Established by the diff (advisory: memory corruption).

Key insight
A page-controlled ratio (repeatingDuration/simpleDuration) was cast to unsigned with static_cast, which wraps, and each skipped iteration dispatched its own event. The fix clamps the count with clampTo<unsigned>(), guards the decrement against underflow, and coalesces the per-iteration event loop into a single O(1) dispatch.

Attack Path

  1. Create a fast, indefinitely-repeating SMIL animation The page adds an <animate> with a very small dur (e.g. 0.0001s) and repeatCount=indefinite inside an SVG.
  2. Seek to a large time Script calls svg.setCurrentTime(400000), making elapsed/simpleDuration an astronomically large repeat count.
  3. Overflow the repeat count Pre-patch static_cast<unsigned> wraps the huge ratio, and repeat-1 can underflow.
  4. Dispatch unbounded repeat events progress() loops up to repeat-1 times queueing repeat events, exhausting memory / corrupting state and crashing.

Impact Assessment

Memory corruption / resource exhaustion driven entirely by page-controlled SVG animation parameters. A tiny duration plus an indefinite repeat and a large seek makes the repeat count overflow and queues an unbounded number of events, exhausting memory and corrupting state in the WebContent process — reachable from a static SVG with no user interaction.

Changed Functions

FunctionChangeNotes
SVGSMILElement::calculateAnimationPercentAndRepeat
Source/WebCore/svg/animation/SVGSMILElement.cpp
modified Uses clampTo<unsigned>() instead of static_cast<unsigned>() for the repeat count and guards the decrement with `if (repeat && !fmod(...))` to prevent overflow/underflow of a page-controlled value.
SVGSMILElement::progress
Source/WebCore/svg/animation/SVGSMILElement.cpp
modified Replaces the per-iteration `for (i<repeat-1) dispatchEventSoon` loop with a single coalesced repeat-event dispatch, so a huge repeat can no longer queue unbounded events or underflow when repeat==0.

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

  • Page-controlled counts cast to unsigned
    Audit SVG/animation timing for other static_cast<unsigned> of durations/ratios an author controls; use clampTo and validate against overflow/underflow.
  • Per-iteration work scaled by author input
    Find loops (event dispatch, allocation) whose iteration count derives from author-controlled repeat/duration values and bound or coalesce them.
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>
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);
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker.