High chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in Compositing
DescriptionInteger overflow in Compositing
ComponentCompositing
Sub-componentInternals>Compositing
Bug ClassInteger Overflow
Tracker553454734
Fix commit9abe6c18e0cf (chromium/src) +10/-9
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
CC_PAINT_EXPORT
cc/paint/paint_image.h
modified

Files Changed

  • third_party/blink/renderer/platform/graphics/bitmap_image.cc
  • third_party/blink/renderer/platform/graphics/image_decoding_store.h
  • cc/paint/paint_image.h
  • cc/paint/paint_image.cc
From 9abe6c18e0cfbbd6887b921e81d375ff7b88e316 Mon Sep 17 00:00:00 2001
From: Stefan Zager <[email protected]>
Date: Mon, 31 Aug 2026 19:49:13 -0700
Subject: [PATCH] Convert atomic sequences for cc::PaintImage to 64-bit integers

Fixed: 553454734
Change-Id: I7b1ed2117113df21db369ab7b7142f6955fa67c4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8311137
Commit-Queue: Stefan Zager <[email protected]>
Reviewed-by: Philip Rogers <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1689557}
---
diff --git a/cc/paint/paint_image.cc b/cc/paint/paint_image.cc
index 9f4c58a44..4ffe7f3 100644
--- a/cc/paint/paint_image.cc
+++ b/cc/paint/paint_image.cc
@@ -36,9 +36,9 @@
 
 namespace cc {
 namespace {
-base::AtomicSequenceNumber g_next_image_id;
-base::AtomicSequenceNumber g_next_image_content_id;
-base::AtomicSequenceNumber g_next_generator_client_id;
+base::AtomicSequenceNumberT<int64_t> g_next_image_id;
+base::AtomicSequenceNumberT<int64_t> g_next_image_content_id;
+base::AtomicSequenceNumberT<int64_t> g_next_generator_client_id;
 }  // namespace
 
 const PaintImage::Id PaintImage::kNonLazyStableId = -1;
diff --git a/cc/paint/paint_image.h b/cc/paint/paint_image.h
index 6e07ef5a..799b60b 100644
--- a/cc/paint/paint_image.h
+++ b/cc/paint/paint_image.h
@@ -150,7 +150,7 @@
 // scale or animation frame.
 class CC_PAINT_EXPORT PaintImage {
  public:
-  using Id = int;
+  using Id = int64_t;
   using AnimationSequenceId = uint32_t;
   enum class AnimationSyncSequence : AnimationSequenceId {
     // All instances of the image animation together on a shared timeline.
@@ -164,7 +164,7 @@
   // stays constant for the same image, the content id can be updated when the
   // backing encoded data for this image changes. For instance, in the case of
   // images which can be progressively updated as more encoded data is received.
-  using ContentId = int;
+  using ContentId = int64_t;
 
   // A GeneratorClientId can be used to namespace different clients that are
   // using the output of a PaintImageGenerator.
@@ -176,7 +176,7 @@
   // parallel. This is particularly important for animated images, where
   // compositors displaying the same image can request decodes for different
   // frames from this image.
-  using GeneratorClientId = int;
+  using GeneratorClientId = int64_t;
   static const GeneratorClientId kDefaultGeneratorClientId;
 
   // The default frame index to use if no index is provided. For multi-frame
diff --git a/third_party/blink/renderer/platform/graphics/bitmap_image.cc b/third_party/blink/renderer/platform/graphics/bitmap_image.cc
index 93e601c..57d62d4 100644
--- a/third_party/blink/renderer/platform/graphics/bitmap_image.cc
+++ b/third_party/blink/renderer/platform/graphics/bitmap_image.cc
@@ -365,7 +365,7 @@
     }
   }
 
-  uint32_t stable_id = image.stable_id();
+  PaintImage::Id stable_id = image.stable_id();
   bool is_lazy_generated = image.IsLazyGenerated();
 
   const cc::PaintFlags* image_flags = &flags;
diff --git a/third_party/blink/renderer/platform/graphics/image_decoding_store.h b/third_party/blink/renderer/platform/graphics/image_decoding_store.h
index 0e31f74..dfd3275d 100644
--- a/third_party/blink/renderer/platform/graphics/image_decoding_store.h
+++ b/third_party/blink/renderer/platform/graphics/image_decoding_store.h
@@ -173,8 +173,9 @@
     auto first =
         HashInts(blink::GetHash(const_cast<ImageFrameGenerator*>(p.gen_.get())),
                  blink::GetHash(p.size_));
-    auto second = HashInts(
-        blink::GetHash(static_cast<uint8_t>(p.alpha_option_)), p.client_id_);
+    auto second =
+        HashInts(blink::GetHash(static_cast<uint8_t>(p.alpha_option_)),
+                 blink::GetHash(p.client_id_));
     return HashInts(first, second);
   }
 
Loading diff…

Original Bug Report

reported by [email protected]

SOP bypass and pixel disclosure via PaintImage ContentId 32-bit wrap in decode cache

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: PaintImage::ContentId is minted using a 32-bit sequence number with no wraparound guard. Cycling the counter across 2^32 mints causes FrameKey collisions in shared image decode caches because cache lookups do not validate image origin or stable identity. This may allow unprivileged web content to substitute cached cross-origin image pixels into an origin-clean canvas and read them back via getImageData.

Affected files:

  • cc/paint/paint_image.cc
  • cc/paint/paint_image.h
  • cc/tiles/gpu_image_decode_cache.cc
  • cc/tiles/software_image_decode_cache_utils.h

Estimated timestamp from git blame: 2017-08-31

Summary

PaintImage::ContentId is a 32-bit signed integer (int) generated from a process-global base::AtomicSequenceNumber. When incremented past 32-bit capacity, it wraps around and repeats previously generated IDs. Image decode caches (GpuImageDecodeCache and SoftwareImageDecodeCache) key cached decoded textures solely by PaintImage::FrameKey (which is (ContentId, frame_index)) without verifying stable image IDs, origin, or source URLs. Because canvas tainting is enforced at draw-call recording time in Blink, a same-origin image colliding with a cached cross-origin image’s FrameKey will cause the victim image’s cached texture to be rendered into an origin-clean canvas, potentially allowing cross-origin pixel extraction via getImageData() or toDataURL().

Root Cause Analysis

  1. 32-Bit Sequence Generator Wraparound: In cc/paint/paint_image.cc (lines 40, 110-112):
base::AtomicSequenceNumber g_next_image_content_id;
...
PaintImage::ContentId PaintImage::GetNextContentId() {
  return g_next_image_content_id.GetNext();
}

base::AtomicSequenceNumber::GetNext() executes seq_.fetch_add(1, std::memory_order_relaxed) on a 32-bit signed integer (std::atomic_int) (base/atomic_sequence_num.h:23-26). After $2^{32}$ calls, the counter wraps around and repeats previously allocated IDs. Release builds contain no checks preventing wraparound (the DCHECK_NE checks at cc/paint/paint_image.cc:360 are compiled out in production).

  1. FrameKey Identity Keying in Shared Decode Caches: PaintImage::FrameKey is defined solely by (content_id_, frame_index_) (cc/paint/paint_image.h:189-209, cc/paint/paint_image.cc:501-503). In GpuImageDecodeCache::GetImageDataForDrawImage (cc/tiles/gpu_image_decode_cache.cc:2498-2516), lookup is performed on persistent_cache_.Get(draw_image.frame_key()).

GpuImageDecodeCache::IsCompatible (gpu_image_decode_cache.cc:2536-2565) validates scale and color space compatibility, but does not verify draw_image.paint_image().stable_id(), origin, or dimensions when upload_scale_mip_level == 0 (unscaled 1:1 draw). Similarly, SoftwareImageDecodeCache explicitly omits stable_id_ from key equality (cc/tiles/software_image_decode_cache_utils.h:108-112).

  1. Taint Decision Timing vs Raster Substitution: Canvas tainting is checked and recorded at API invocation time in Blink based on the CanvasImageSource (third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc:2322-2324). Drawing a same-origin or data: URL image passes WouldTaintCanvasOrigin (third_party/blink/renderer/modules/canvas/canvas2d/canvas_image_source_util.cc:89-104), keeping HTMLCanvasElement::OriginClean() as true. When the canvas is subsequently rasterized in cc, PlaybackImageProvider::GetRasterContent calls GetDecodedImageForDraw (cc/raster/playback_image_provider.cc:76), which retrieves and renders the colliding cross-origin image’s texture. A subsequent call to ctx.getImageData() (third_party/blink/renderer/modules/canvas/canvas2d/base_rendering_context_2d.cc:377) succeeds because the canvas remains marked clean.

Potential Attack Scenario

(Note: These are suggested/potential steps identified through source code tracing; our tooling agent does not have the capability to execute code or run an active PoC.)

  1. An attacker page loads a sensitive cross-origin image $V$ (e.g. <img src="https://victim.example.com/avatar.png">) and draws it into a temporary canvas $T$. The image is decoded and cached in GpuImageDecodeCache under FrameKey(C_v, 0).
  2. The attacker reads the natural dimensions $W \times H$ via img.naturalWidth/naturalHeight.
  3. The attacker uses background Web Workers to rapidly cycle g_next_image_content_id ~$2^{32}$ times using lightweight operations that mint ContentIds without filling the decode cache (such as createImageBitmap transformations via StaticBitmapImageTransform::Apply).
  4. Near wraparound back to $C_v$, the attacker creates same-origin candidate images $B_i$ sized $W \times H$. The candidate image receiving ContentId == C_v shares FrameKey(C_v, 0).
  5. The attacker draws $B_i$ to a clean canvas $F$ at 1:1 scale. Blink marks canvas $F$ origin-clean. During rasterization, GpuImageDecodeCache serves victim image $V$’s cached texture.
  6. The attacker reads out the victim’s cross-origin pixel data via F.getContext('2d').getImageData(0, 0, W, H) without triggering a SecurityError.

Suggested Fix

  1. Widen PaintImage::Id and PaintImage::ContentId from int (32-bit) to uint64_t or base::IdType64, eliminating practical counter exhaustion.
  2. Alternatively (or in addition as defense-in-depth), add an immediate CHECK against counter overflow in g_next_image_content_id and g_next_image_id.
  3. Include PaintImage::Id (stable_id) and image dimension checks in GpuImageDecodeCache::IsCompatible and SoftwareImageDecodeCache::CacheKey::operator== to ensure separate image instances cannot collide in the decode caches.

Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker