Released August 17, 2026

CVE-2026-65341

CVSS 5.4 severity medium CWE-119CWE-416 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N

Overview

Impact:

Description: Processing maliciously crafted web content may lead to memory corruption

Researchers: Henock Habte

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.

Patch Details

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);

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-65341
Bugzilla 318405
Component WebCore
Bug Class IntegerOverflow
Severity medium
Commit 34249048d66d342f…
Advisory Apple Advisory

Terminology

  • 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).

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.

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.

Changed Functions

Function File Change Note
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&lt;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

Source Files

  • Source/WebCore/svg/animation/SVGSMILElement.cpp

Test Files

  • LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash-expected.txt
  • LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash.html

Regression Test

The fix ships this proof-of-concept / regression test:

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>