Low watch CVSS 8.8 chrome Integer Overflow 🔧 Commit mapped

Overview

Low
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in Chromium
DescriptionInteger overflow in Chromium
ComponentChromium
Sub-componentSkottie
Bug ClassInteger Overflow
Action tierwatch — ub-class only — no demonstrated corruption path
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker530816571
Fix commit36e509ddad66 (skia) +66/-6
CWECWE-190 (Integer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedYoungjin Ju
Reported2026-07-03 (per vendor advisory)
Fix landed2026-07-13 (public git)
Disclosed2026-08-25
Patch gap43 days — fix public in source before the advisory

Background

Skottie
Skia’s Lottie animation renderer that parses attacker-controllable JSON animation files, including shape-layer gradient definitions.
Gradient stop array (`fStops`)
a flat buffer that encodes gradient stops as ColorRec entries (4 elements each) followed by OpacityRec entries (2 elements each), whose partition must be recomputed from fStopCount.
`SkSafeMath`
a Skia helper that performs size_t arithmetic while latching an fOK flag whenever an operation overflows or underflows.
32-bit build
a build where size_t is 32 bits, so multiplications by 4 and subtractions wrap silently at 2^32.

Root Cause Analysis

In Sync::onSync (the enclosing function is not named in the hunk, so this is (inferred)) the code partitions fStops into color stops and opacity stops by computing c_size = c_count * 4 and o_count = (fStops.size() - c_size) / 2 using ordinary size_t arithmetic, where c_count = fStopCount is derived from the animation JSON. On a 32-bit build a large fStopCount makes c_count * 4 wrap around modulo 2^32, and fStops.size() - c_size can underflow, so c_size becomes small while the derived counts are nonsensical. The pre-patch consistency check fStops.size() < c_size || fStops.size() != (c_count * 4 + o_count * 2) re-uses the same overflowing expressions, so a crafted fStopCount can make the recomputed total equal fStops.size() and pass validation despite the counts being wrong.

The fix routes every step (mul, sub, add) through an SkSafeMath safe instance and rejects the gradient when !safe.ok(), so any overflow or underflow now fails the check instead of silently producing an in-range but incorrect partition that downstream gradient construction would consume with mismatched color/opacity counts.

Key insight
The validation guarding the color/opacity split was computed with the same wrapping size_t arithmetic it was meant to detect, so an overflowing fStopCount could satisfy the equality check; the fix performs the arithmetic with SkSafeMath and bails on any overflow.

Attack Path

  1. Deliver malicious animation An attacker supplies a Lottie/Skottie JSON animation (e.g., via a web page or app that renders Skottie content) containing a shape-layer gradient with a crafted stop count.
  2. Overflow the count math On a 32-bit build, the large fStopCount causes c_count * 4 (and the fStops.size() - c_size subtraction) to wrap, producing a bogus but small c_size and inconsistent o_count.
  3. Bypass the consistency check Because the pre-patch guard re-evaluates the same overflowing expressions, the attacker tunes fStopCount so fStops.size() == c_count * 4 + o_count * 2 holds under wraparound, passing validation.
  4. Consume a mismatched partition Gradient construction then treats fStops with an incorrect color/opacity split, reading past or misinterpreting the buffer (specific out-of-bounds sink not shown in the diff) (inferred).

Impact Assessment

An attacker who controls a rendered Skottie animation can trigger an integer overflow in the gradient stop partitioning on 32-bit builds, leading to an inconsistent buffer interpretation and likely out-of-bounds access during gradient construction (inferred). This occurs in whichever process renders the animation (renderer or app process using Skia). The vendor advisory rates this low severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.8.

Changed Functions

FunctionChangeNotes
DEF_TEST
modules/skottie/src/SkottieTest.cpp
modified
TestMarkerObserver
modules/skottie/src/SkottieTest.cpp
modified
TestResourceProvider
modules/skottie/src/SkottieTest.cpp
modified
TestAsset
modules/skottie/src/SkottieTest.cpp
modified
DEF_TEST
modules/skottie/tests/SkottieTest.cpp
modified

Files Changed

  • modules/skottie/tests/SkottieTest.cpp
  • modules/skottie/BUILD.gn
  • src/core/SkSafeMath.h
  • modules/skottie/src/layers/shapelayer/Gradient.cpp

Audit Directions

  • Overflow-checked-with-itself validation
    Look for size/count consistency checks that recompute quantities using the same unchecked arithmetic they are supposed to validate, especially count * elem_size patterns.
  • 32-bit `size_t` arithmetic on parsed counts
    Audit parsers that multiply or subtract attacker-supplied element counts against buffer sizes without SkSafeMath/base::CheckedNumeric, since these wrap silently on 32-bit builds.
  • Flat heterogeneous buffers
    Review code that partitions a single flat array into differently sized record types based on a separately stored count, where a mismatched split leads to misread records.
From 36e509ddad66569e1a604816c1e47845950eee77 Mon Sep 17 00:00:00 2001
From: Florin Malita <fmalita@google.com>
Date: Mon, 13 Jul 2026 12:15:44 -0400
Subject: [PATCH] [skottie] Fix gradient count overflow in 32 bit builds

Use SkSafeMath to detect size_t overflows.

Also relocate SkottieTest.cpp from src/ to tests/.

Bug: b/530816571
Change-Id: I861fe30b5223e2fb074c37217afa7d3bd48419ca
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1292156
Commit-Queue: Florin Malita <fmalita@google.com>
Reviewed-by: Brandon Wilson <bwils@google.com>
---
diff --git a/modules/skottie/src/SkottieTest.cpp b/modules/skottie/src/SkottieTest.cpp
deleted file mode 100644
index 76bd5c3..0000000
--- a/modules/skottie/src/SkottieTest.cpp
+++ /dev/null
@@ -1,271 +0,0 @@
-/*
- * Copyright 2018 Google LLC
- *
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
-
-#include "include/core/SkStream.h"
-#include "include/core/SkSurface.h"
-#include "modules/skottie/include/Skottie.h"
-#include "tests/Test.h"
-
-#include <cmath>
-#include <string>
-#include <tuple>
-#include <vector>
-
-using namespace skottie;
-
-DEF_TEST(Skottie_OssFuzz8956, reporter) {
-    static constexpr char json[] =
-        "{\"v\":\" \",\"fr\":3,\"w\":4,\"h\":3,\"layers\":[{\"ty\": 1, \"sw\": 10, \"sh\": 10,"
-            " \"sc\":\"#ffffff\", \"ks\":{\"o\":{\"a\": true, \"k\":"
-            " [{\"t\": 0, \"s\": 0, \"e\": 1, \"i\": {\"x\":[]}}]}}}]}";
-
-    SkMemoryStream stream(json, strlen(json));
-
-    // Passes if parsing doesn't crash.
-    auto animation = Animation::Make(&stream);
-}
-
-DEF_TEST(Skottie_Annotations, reporter) {
-    static constexpr char json[] = R"({
-                                     "v": "5.2.1",
-                                     "w": 100,
-                                     "h": 100,
-                                     "fr": 10,
-                                     "ip": 0,
-                                     "op": 100,
-                                     "layers": [
-                                       {
-                                         "ty": 1,
-                                         "ind": 0,
-                                         "ip": 0,
-                                         "op": 1,
-                                         "ks": {
-                                           "o": { "a": 0, "k": 50 }
-                                         },
-                                         "sw": 100,
-                                         "sh": 100,
-                                         "sc": "#ffffff"
-                                       }
-                                     ],
-                                     "markers": [
-                                       {
-                                           "cm": "marker_1",
-                                           "dr": 25,
-                                           "tm": 25
-                                       },
-                                       {
-                                           "cm": "marker_2",
-                                           "dr": 0,
-                                           "tm": 75
-                                       }
-                                     ]
-                                   })";
-
-    class TestMarkerObserver final : public MarkerObserver {
-    public:
-        void onMarker(const char name[], float t0, float t1) override {
-            fMarkers.push_back(std::make_tuple(name, t0, t1));
-        }
-
-        std::vector<std::tuple<std::string, float, float>> fMarkers;
-    };
-
-    SkMemoryStream stream(json, strlen(json));
-    auto observer = sk_make_sp<TestMarkerObserver>();
-
-    auto animation = skottie::Animation::Builder()
-            .setMarkerObserver(observer)
-            .make(&stream);
-
-    REPORTER_ASSERT(reporter, animation);
-    REPORTER_ASSERT(reporter, animation->duration() == 10);
-    REPORTER_ASSERT(reporter, animation->inPoint()  == 0.0);
-    REPORTER_ASSERT(reporter, animation->outPoint() == 100.0);
-
-    REPORTER_ASSERT(reporter, observer->fMarkers.size() == 2ul);
-    REPORTER_ASSERT(reporter, std::get<0>(observer->fMarkers[0]) == "marker_1");
-    REPORTER_ASSERT(reporter, std::get<1>(observer->fMarkers[0]) == 0.25f);
-    REPORTER_ASSERT(reporter, std::get<2>(observer->fMarkers[0]) == 0.50f);
-    REPORTER_ASSERT(reporter, std::get<0>(observer->fMarkers[1]) == "marker_2");
-    REPORTER_ASSERT(reporter, std::get<1>(observer->fMarkers[1]) == 0.75f);
-    REPORTER_ASSERT(reporter, std::get<2>(observer->fMarkers[1]) == 0.75f);
-}
-
-DEF_TEST(Skottie_Image_Loading, reporter) {
-    class TestResourceProvider final : public skresources::ResourceProvider {
-    public:
-        TestResourceProvider(sk_sp<skresources::ImageAsset> single_asset,
-                             sk_sp<skresources::ImageAsset>  multi_asset)
-            : fSingleFrameAsset(std::move(single_asset))
-            , fMultiFrameAsset (std::move( multi_asset)) {}
-
-    private:
-        sk_sp<ImageAsset> loadImageAsset(const char path[],
-                                         const char name[],
-                                         const char id[]) const override {
-            return strcmp(id, "single_frame")
-                    ? fMultiFrameAsset
-                    : fSingleFrameAsset;
-        }
-
-        const sk_sp<skresources::ImageAsset> fSingleFrameAsset,
-                                             fMultiFrameAsset;
-    };
-
-    auto make_animation = [&reporter] (sk_sp<skresources::ImageAsset> single_asset,
-                                       sk_sp<skresources::ImageAsset>  multi_asset,
-                                       bool deferred_image_loading) {
-        static constexpr char json[] = R"({
-                                         "v": "5.2.1",
-                                         "w": 100,
-                                         "h": 100,
-                                         "fr": 10,
-                                         "ip": 0,
-                                         "op": 100,
-                                         "assets": [
-                                           {
-                                             "id": "single_frame",
-                                             "p" : "single_frame.png",
-                                             "u" : "images/",
-                                             "w" : 500,
-                                             "h" : 500
-                                           },
-                                           {
-                                             "id": "multi_frame",
-                                             "p" : "multi_frame.png",
-                                             "u" : "images/",
-                                             "w" : 500,
-                                             "h" : 500
-                                           }
-                                         ],
-                                         "layers": [
-                                           {
-                                             "ty": 2,
-                                             "refId": "single_frame",
-                                             "ind": 0,
-                                             "ip": 0,
-                                             "op": 100,
-                                             "ks": {}
-                                           },
-                                           {
-                                             "ty": 2,
-                                             "refId": "multi_frame",
-                                             "ind": 1,
-                                             "ip": 0,
-                                             "op": 100,
-                                             "ks": {}
-                                           }
-                                         ]
-                                       })";
-
-        SkMemoryStream stream(json, strlen(json));
-
-        const auto flags = deferred_image_loading
-            ? static_cast<uint32_t>(skottie::Animation::Builder::kDeferImageLoading)
-            : 0;
-        auto animation =
-            skottie::Animation::Builder(flags)
-                .setResourceProvider(sk_make_sp<TestResourceProvider>(std::move(single_asset),
-                                                                      std::move( multi_asset)))
-                .make(&stream);
-
-        REPORTER_ASSERT(reporter, animation);
-
-        return  animation;
-    };
-
-    class TestAsset final : public skresources::ImageAsset {
-    public:
-        explicit TestAsset(bool multi_frame) : fMultiFrame(multi_frame) {}
-
-        const std::vector<float>& requestedFrames() const { return fRequestedFrames; }
-
-    private:
-        bool isMultiFrame() override { return fMultiFrame; }
-
-        sk_sp<SkImage> getFrame(float t) override {
-            fRequestedFrames.push_back(t);
-
-            return SkSurfaces::Raster(SkImageInfo::MakeN32Premul(10, 10))->makeImageSnapshot();
-        }
-
-        const bool fMultiFrame;
-
-        std::vector<float> fRequestedFrames;
-    };
-
-    {
-        auto single_asset = sk_make_sp<TestAsset>(false),
-              multi_asset = sk_make_sp<TestAsset>(true);
-
-        // Default image loading: single-frame images are loaded upfront, multi-frame images are
-        // loaded on-demand.
-        auto animation = make_animation(single_asset, multi_asset, false);
-
-        REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
-        REPORTER_ASSERT(reporter,  multi_asset->requestedFrames().size() == 0);
-        REPORTER_ASSERT(reporter, SkScalarNearlyZero(single_asset->requestedFrames()[0]));
-
-        animation->seekFrameTime(1);
-        REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
-        REPORTER_ASSERT(reporter,  multi_asset->requestedFrames().size() == 1);
-        REPORTER_ASSERT(reporter, SkScalarNearlyEqual(multi_asset->requestedFrames()[0], 1));
-
-        animation->seekFrameTime(2);
-        REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
-        REPORTER_ASSERT(reporter,  multi_asset->requestedFrames().size() == 2);
-        REPORTER_ASSERT(reporter, SkScalarNearlyEqual(multi_asset->requestedFrames()[1], 2));
-    }
-
-    {
-        auto single_asset = sk_make_sp<TestAsset>(false),
-              multi_asset = sk_make_sp<TestAsset>(true);
-
-        // Deferred image loading: both single-frame and multi-frame images are loaded on-demand.
-        auto animation = make_animation(single_asset, multi_asset, true);
-
-        REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 0);
-        REPORTER_ASSERT(reporter,  multi_asset->requestedFrames().size() == 0);
-
-        animation->seekFrameTime(1);
-        REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
-        REPORTER_ASSERT(reporter,  multi_asset->requestedFrames().size() == 1);
-        REPORTER_ASSERT(reporter, SkScalarNearlyEqual(single_asset->requestedFrames()[0], 1));
-        REPORTER_ASSERT(reporter, SkScalarNearlyEqual (multi_asset->requestedFrames()[0], 1));
-
-        animation->seekFrameTime(2);
-        REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
-        REPORTER_ASSERT(reporter,  multi_asset->requestedFrames().size() == 2);
-        REPORTER_ASSERT(reporter, SkScalarNearlyEqual(multi_asset->requestedFrames()[1], 2));
-    }
-}
-
-DEF_TEST(Skottie_Layer_NoType, r) {
-    static constexpr char json[] =
-        R"({
-             "v": "5.2.1",
-             "w": 100,
-             "h": 100,
-             "fr": 10,
-             "ip": 0,
-             "op": 100,
-             "layers": [
-               {
-                 "ind": 0,
-                 "ip": 0,
-                 "op": 100,
-                 "ks": {}
-               }
-             ]
-           })";
-
-    SkMemoryStream stream(json, strlen(json));
-    auto anim = Animation::Make(&stream);
-
-    // passes if we don't crash
-    REPORTER_ASSERT(r, anim);
-}
diff --git a/modules/skottie/src/layers/shapelayer/Gradient.cpp b/modules/skottie/src/layers/shapelayer/Gradient.cpp
index 418ba63..069fb15 100644
--- a/modules/skottie/src/layers/shapelayer/Gradient.cpp
+++ b/modules/skottie/src/layers/shapelayer/Gradient.cpp
@@ -23,6 +23,7 @@
 #include "modules/sksg/include/SkSGGradient.h"
 #include "modules/sksg/include/SkSGPaint.h"
 #include "modules/sksg/include/SkSGRenderEffect.h"
+#include "src/core/SkSafeMath.h"
 
 #include <algorithm>
 #include <cstddef>
@@ -141,10 +142,13 @@
         // while the number of opacity stops is implicit (based on the size of fStops).
         //
         // |fStops| holds ColorRec x |fColorStopCount| + OpacityRec x N
-        const auto c_count = fStopCount,
-                   c_size  = c_count * 4,
-                   o_count = (fStops.size() - c_size) / 2;
-        if (fStops.size() < c_size || fStops.size() != (c_count * 4 + o_count * 2)) {
+        SkSafeMath safe;
+        const size_t c_count = fStopCount,
+                     c_size  = safe.mul(c_count, 4),
+                     o_count = safe.sub(fStops.size(), c_size) / 2,
+                     o_size  = safe.mul(o_count, 2),
+                  total_size = safe.add(c_size, o_size);
+        if (!safe.ok() || fStops.size() != total_size) {
             // apply() may get called before the stops are set, so only log when we have some stops.
             if (!fStops.empty()) {
                 SkDebugf("!! Invalid gradient stop array size: %zu\n", fStops.size());
diff --git a/src/core/SkSafeMath.h b/src/core/SkSafeMath.h
index 31a4144..74c5a5c 100644
--- a/src/core/SkSafeMath.h
+++ b/src/core/SkSafeMath.h
@@ -36,6 +36,11 @@
         return result;
     }
 
+    size_t sub(size_t x, size_t y) {
+        fOK &= x >= y;
+        return x - y;
+    }
+
     int addInt(int a, int b) {
         int64_t result = static_cast<int64_t>(a) + static_cast<int64_t>(b);
         if (!SkTFitsIn<int>(result)) {
diff --git a/modules/skottie/tests/SkottieTest.cpp b/modules/skottie/tests/SkottieTest.cpp
new file mode 100644
index 0000000..472c2bd
--- /dev/null
+++ b/modules/skottie/tests/SkottieTest.cpp
@@ -0,0 +1,322 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "include/core/SkStream.h"
+#include "include/core/SkSurface.h"
+#include "modules/skottie/include/Skottie.h"
+#include "tests/Test.h"
+
+#include <string>
+#include <tuple>
+#include <vector>
+
+using namespace skottie;
+
+DEF_TEST(Skottie_OssFuzz8956, reporter) {
+    static constexpr char json[] =
+        "{\"v\":\" \",\"fr\":3,\"w\":4,\"h\":3,\"layers\":[{\"ty\": 1, \"sw\": 10, \"sh\": 10,"
+            " \"sc\":\"#ffffff\", \"ks\":{\"o\":{\"a\": true, \"k\":"
+            " [{\"t\": 0, \"s\": 0, \"e\": 1, \"i\": {\"x\":[]}}]}}}]}";
+
+    SkMemoryStream stream(json, strlen(json));
+
+    // Passes if parsing doesn't crash.
+    auto animation = Animation::Make(&stream);
+}
+
+DEF_TEST(Skottie_Annotations, reporter) {
+    static constexpr char json[] = R"({
+                                     "v": "5.2.1",
+                                     "w": 100,
+                                     "h": 100,
+                                     "fr": 10,
+                                     "ip": 0,
+                                     "op": 100,
+                                     "layers": [
+                                       {
+                                         "ty": 1,
+                                         "ind": 0,
+                                         "ip": 0,
+                                         "op": 1,
+                                         "ks": {
+                                           "o": { "a": 0, "k": 50 }
+                                         },
+                                         "sw": 100,
+                                         "sh": 100,
+                                         "sc": "#ffffff"
+                                       }
+                                     ],
+                                     "markers": [
+                                       {
+                                           "cm": "marker_1",
+                                           "dr": 25,
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/modules/skottie/tests/SkottieTest.cpp b/modules/skottie/tests/SkottieTest.cpp
new file mode 100644
index 0000000..472c2bd
--- /dev/null
+++ b/modules/skottie/tests/SkottieTest.cpp
@@ -0,0 +1,322 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "include/core/SkStream.h"
+#include "include/core/SkSurface.h"
+#include "modules/skottie/include/Skottie.h"
+#include "tests/Test.h"
+
+#include <string>
+#include <tuple>
+#include <vector>
+
+using namespace skottie;
+
+DEF_TEST(Skottie_OssFuzz8956, reporter) {
+    static constexpr char json[] =
+        "{\"v\":\" \",\"fr\":3,\"w\":4,\"h\":3,\"layers\":[{\"ty\": 1, \"sw\": 10, \"sh\": 10,"
+            " \"sc\":\"#ffffff\", \"ks\":{\"o\":{\"a\": true, \"k\":"
+            " [{\"t\": 0, \"s\": 0, \"e\": 1, \"i\": {\"x\":[]}}]}}}]}";
+
+    SkMemoryStream stream(json, strlen(json));
+
+    // Passes if parsing doesn't crash.
+    auto animation = Animation::Make(&stream);
+}
+
+DEF_TEST(Skottie_Annotations, reporter) {
+    static constexpr char json[] = R"({
+                                     "v": "5.2.1",
+                                     "w": 100,
+                                     "h": 100,
+                                     "fr": 10,
+                                     "ip": 0,
+                                     "op": 100,
+                                     "layers": [
+                                       {
+                                         "ty": 1,
+                                         "ind": 0,
+                                         "ip": 0,
+                                         "op": 1,
+                                         "ks": {
+                                           "o": { "a": 0, "k": 50 }
+                                         },
+                                         "sw": 100,
+                                         "sh": 100,
+                                         "sc": "#ffffff"
+                                       }
+                                     ],
+                                     "markers": [
+                                       {
+                                           "cm": "marker_1",
+                                           "dr": 25,
+                                           "tm": 25
+                                       },
+                                       {
+                                           "cm": "marker_2",
+                                           "dr": 0,
+                                           "tm": 75
+                                       }
+                                     ]
+                                   })";
+
+    class TestMarkerObserver final : public MarkerObserver {
+    public:
+        void onMarker(const char name[], float t0, float t1) override {
+            fMarkers.push_back(std::make_tuple(name, t0, t1));
+        }
+
+        std::vector<std::tuple<std::string, float, float>> fMarkers;
+    };
+
+    SkMemoryStream stream(json, strlen(json));
+    auto observer = sk_make_sp<TestMarkerObserver>();
+
+    auto animation = skottie::Animation::Builder()
+            .setMarkerObserver(observer)
+            .make(&stream);
+
+    REPORTER_ASSERT(reporter, animation);
+    REPORTER_ASSERT(reporter, animation->duration() == 10);
+    REPORTER_ASSERT(reporter, animation->inPoint()  == 0.0);
+    REPORTER_ASSERT(reporter, animation->outPoint() == 100.0);
+
+    REPORTER_ASSERT(reporter, observer->fMarkers.size() == 2ul);
+    REPORTER_ASSERT(reporter, std::get<0>(observer->fMarkers[0]) == "marker_1");
+    REPORTER_ASSERT(reporter, std::get<1>(observer->fMarkers[0]) == 0.25f);
+    REPORTER_ASSERT(reporter, std::get<2>(observer->fMarkers[0]) == 0.50f);
+    REPORTER_ASSERT(reporter, std::get<0>(observer->fMarkers[1]) == "marker_2");
+    REPORTER_ASSERT(reporter, std::get<1>(observer->fMarkers[1]) == 0.75f);
+    REPORTER_ASSERT(reporter, std::get<2>(observer->fMarkers[1]) == 0.75f);
+}
+
+DEF_TEST(Skottie_Image_Loading, reporter) {
+    class TestResourceProvider final : public skresources::ResourceProvider {
+    public:
+        TestResourceProvider(sk_sp<skresources::ImageAsset> single_asset,
+                             sk_sp<skresources::ImageAsset>  multi_asset)
+            : fSingleFrameAsset(std::move(single_asset))
+            , fMultiFrameAsset (std::move( multi_asset)) {}
+
+    private:
+        sk_sp<ImageAsset> loadImageAsset(const char path[],
+                                         const char name[],
+                                         const char id[]) const override {
+            return strcmp(id, "single_frame")
+                    ? fMultiFrameAsset
+                    : fSingleFrameAsset;
+        }
+
+        const sk_sp<skresources::ImageAsset> fSingleFrameAsset,
+                                             fMultiFrameAsset;
+    };
+
+    auto make_animation = [&reporter] (sk_sp<skresources::ImageAsset> single_asset,
+                                       sk_sp<skresources::ImageAsset>  multi_asset,
+                                       bool deferred_image_loading) {
+        static constexpr char json[] = R"({
+                                         "v": "5.2.1",
+                                         "w": 100,
+                                         "h": 100,
+                                         "fr": 10,
+                                         "ip": 0,
+                                         "op": 100,
+                                         "assets": [
+                                           {
+                                             "id": "single_frame",
+                                             "p" : "single_frame.png",
+                                             "u" : "images/",
+                                             "w" : 500,
+                                             "h" : 500
+                                           },
+                                           {
+                                             "id": "multi_frame",
+                                             "p" : "multi_frame.png",
+                                             "u" : "images/",
+                                             "w" : 500,
+                                             "h" : 500
+                                           }
+                                         ],
+                                         "layers": [
+                                           {
+                                             "ty": 2,
+                                             "refId": "single_frame",
+                                             "ind": 0,
+                                             "ip": 0,
+                                             "op": 100,
+                                             "ks": {}
+                                           },
+                                           {
+                                             "ty": 2,
+                                             "refId": "multi_frame",
+                                             "ind": 1,
+                                             "ip": 0,
+                                             "op": 100,
+                                             "ks": {}
+                                           }
+                                         ]
+                                       })";
+
+        SkMemoryStream stream(json, strlen(json));
+
+        const auto flags = deferred_image_loading
+            ? static_cast<uint32_t>(skottie::Animation::Builder::kDeferImageLoading)
+            : 0;
+        auto animation =
+            skottie::Animation::Builder(flags)
+                .setResourceProvider(sk_make_sp<TestResourceProvider>(std::move(single_asset),
+                                                                      std::move( multi_asset)))
+                .make(&stream);
+
+        REPORTER_ASSERT(reporter, animation);
+
+        return  animation;
+    };
+
+    class TestAsset final : public skresources::ImageAsset {
+    public:
+        explicit TestAsset(bool multi_frame) : fMultiFrame(multi_frame) {}
+
+        const std::vector<float>& requestedFrames() const { return fRequestedFrames; }
+
+    private:
+        bool isMultiFrame() override { return fMultiFrame; }
+
+        sk_sp<SkImage> getFrame(float t) override {
+            fRequestedFrames.push_back(t);
+
+            return SkSurfaces::Raster(SkImageInfo::MakeN32Premul(10, 10))->makeImageSnapshot();
+        }
+
+        const bool fMultiFrame;
+
+        std::vector<float> fRequestedFrames;
+    };
+
+    {
+        auto single_asset = sk_make_sp<TestAsset>(false),
+              multi_asset = sk_make_sp<TestAsset>(true);
+
+        // Default image loading: single-frame images are loaded upfront, multi-frame images are
+        // loaded on-demand.
+        auto animation = make_animation(single_asset, multi_asset, false);
+
+        REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
+        REPORTER_ASSERT(reporter,  multi_asset->requestedFrames().size() == 0);
+        REPORTER_ASSERT(reporter, SkScalarNearlyZero(single_asset->requestedFrames()[0]));
+
+        animation->seekFrameTime(1);
+        REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
+        REPORTER_ASSERT(reporter,  multi_asset->requestedFrames().size() == 1);
+        REPORTER_ASSERT(reporter, SkScalarNearlyEqual(multi_asset->requestedFrames()[0], 1));
+
+        animation->seekFrameTime(2);
+        REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
+        REPORTER_ASSERT(reporter,  multi_asset->requestedFrames().size() == 2);
+        REPORTER_ASSERT(reporter, SkScalarNearlyEqual(multi_asset->requestedFrames()[1], 2));
+    }
+
+    {
+        auto single_asset = sk_make_sp<TestAsset>(false),
+              multi_asset = sk_make_sp<TestAsset>(true);
+
+        // Deferred image loading: both single-frame and multi-frame images are loaded on-demand.
+        auto animation = make_animation(single_asset, multi_asset, true);
+
+        REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 0);
+        REPORTER_ASSERT(reporter,  multi_asset->requestedFrames().size() == 0);
+
+        animation->seekFrameTime(1);
+        REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
+        REPORTER_ASSERT(reporter,  multi_asset->requestedFrames().size() == 1);
+        REPORTER_ASSERT(reporter, SkScalarNearlyEqual(single_asset->requestedFrames()[0], 1));
+        REPORTER_ASSERT(reporter, SkScalarNearlyEqual (multi_asset->requestedFrames()[0], 1));
+
+        animation->seekFrameTime(2);
+        REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
+        REPORTER_ASSERT(reporter,  multi_asset->requestedFrames().size() == 2);
+        REPORTER_ASSERT(reporter, SkScalarNearlyEqual(multi_asset->requestedFrames()[1], 2));
+    }
+}
+
+DEF_TEST(Skottie_Layer_NoType, r) {
+    static constexpr char json[] =
+        R"({
+             "v": "5.2.1",
+             "w": 100,
+             "h": 100,
+             "fr": 10,
+             "ip": 0,
+             "op": 100,
+             "layers": [
+               {
+                 "ind": 0,
+                 "ip": 0,
+                 "op": 100,
+                 "ks": {}
+               }
+             ]
+           })";
+
+    SkMemoryStream stream(json, strlen(json));
+    auto anim = Animation::Make(&stream);
+
+    // passes if we don't crash
+    REPORTER_ASSERT(r, anim);
+}
+
+DEF_TEST(Skottie_Gradient_InvalidCount, r) {
+    static constexpr char json[] = R"({
+      "v": "5.12.0",
+      "fr": 60,
+      "ip": 0,
+      "op": 1,
+      "w": 64,
+      "h": 64,
+      "layers": [
+        {
+          "ind": 1,
+          "ty": 4,
+          "sr": 1,
+          "ks": {},
+          "shapes": [
+            {
+              "ty": "rc",
+              "d": 1,
+              "p": { "a": 0, "k": [0, 0] },
+              "s": { "a": 0, "k": [32, 32] },
+              "r": { "a": 0, "k": 0 }
+            },
+            {
... (truncated)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.