CVE-2026-106332
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
CC_PAINT_EXPORTcc/paint/paint_image.h |
modified |
Files Changed
third_party/blink/renderer/platform/graphics/bitmap_image.ccthird_party/blink/renderer/platform/graphics/image_decoding_store.hcc/paint/paint_image.hcc/paint/paint_image.cc
Patch
From 9abe6c18e0cfbbd6887b921e81d375ff7b88e316 Mon Sep 17 00:00:00 2001
From: Stefan Zager <szager@chromium.org>
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 <szager@chromium.org>
Reviewed-by: Philip Rogers <pdr@chromium.org>
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);
}
Original Bug Report
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.cccc/paint/paint_image.hcc/tiles/gpu_image_decode_cache.cccc/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
- 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).
- FrameKey Identity Keying in Shared Decode Caches:
PaintImage::FrameKeyis defined solely by(content_id_, frame_index_)(cc/paint/paint_image.h:189-209,cc/paint/paint_image.cc:501-503). InGpuImageDecodeCache::GetImageDataForDrawImage(cc/tiles/gpu_image_decode_cache.cc:2498-2516), lookup is performed onpersistent_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).
- 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 ordata:URL image passesWouldTaintCanvasOrigin(third_party/blink/renderer/modules/canvas/canvas2d/canvas_image_source_util.cc:89-104), keepingHTMLCanvasElement::OriginClean()astrue. When the canvas is subsequently rasterized incc,PlaybackImageProvider::GetRasterContentcallsGetDecodedImageForDraw(cc/raster/playback_image_provider.cc:76), which retrieves and renders the colliding cross-origin image’s texture. A subsequent call toctx.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.)
- 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 inGpuImageDecodeCacheunderFrameKey(C_v, 0). - The attacker reads the natural dimensions $W \times H$ via
img.naturalWidth/naturalHeight. - 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 ascreateImageBitmaptransformations viaStaticBitmapImageTransform::Apply). - 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_vsharesFrameKey(C_v, 0). - The attacker draws $B_i$ to a clean canvas $F$ at 1:1 scale. Blink marks canvas $F$ origin-clean. During rasterization,
GpuImageDecodeCacheserves victim image $V$’s cached texture. - The attacker reads out the victim’s cross-origin pixel data via
F.getContext('2d').getImageData(0, 0, W, H)without triggering aSecurityError.
Suggested Fix
- Widen
PaintImage::IdandPaintImage::ContentIdfromint(32-bit) touint64_torbase::IdType64, eliminating practical counter exhaustion. - Alternatively (or in addition as defense-in-depth), add an immediate
CHECKagainst counter overflow ing_next_image_content_idandg_next_image_id. - Include
PaintImage::Id(stable_id) and image dimension checks inGpuImageDecodeCache::IsCompatibleandSoftwareImageDecodeCache::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.