Medium chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in Skia
DescriptionInteger overflow in Skia
ComponentSkia
Bug ClassInteger Overflow
Tracker500305404
Fix commit0f4027ff431c (skia) +127/-126
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
for
src/gpu/graphite/dawn/DawnBuffer.cpp
modified
if
src/gpu/graphite/dawn/DawnBuffer.cpp
modified
if
src/gpu/graphite/dawn/DawnCommandBuffer.cpp
modified
for
src/gpu/graphite/dawn/DawnCommandBuffer.cpp
modified

Files Changed

  • src/gpu/graphite/dawn/DawnBuffer.cpp
  • src/gpu/graphite/dawn/DawnBuffer.h
  • src/gpu/graphite/dawn/DawnCommandBuffer.cpp
  • src/gpu/graphite/dawn/DawnCommandBuffer.h
  • src/gpu/graphite/dawn/DawnGraphicsPipeline.h
  • src/gpu/graphite/dawn/DawnResourceProvider.cpp
From 0f4027ff431caf25d9e9289c2b40cbc150889ecf Mon Sep 17 00:00:00 2001
From: Nicolette Prevost <[email protected]>
Date: Wed, 13 May 2026 14:59:55 -0400
Subject: [PATCH] [graphite] Cache single-buffer BindGroups on DawnBuffer

* This follows the Vulkan backend in caching single-buffer bind groups on to Buffer implementations. This allows us to remove DawnResourceProvider's special caching of single-texture bindgroups and findOrCreateSingleTextureSamplerBindGroup(...).

* The DawnCommandBuffer instead defines the bind group entries for single-texture groups and uses the generic createBindGroup(...) method.

Bug: b/512814281, b/500305404
Change-Id: Iaf86d7751815806cba1353f74ee2ce3e49042b9d
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1232757
Commit-Queue: Nicolette Prevost <[email protected]>
Reviewed-by: Thomas Smith <[email protected]>
Reviewed-by: Michael Ludwig <[email protected]>
---

diff --git a/src/gpu/graphite/dawn/DawnBuffer.cpp b/src/gpu/graphite/dawn/DawnBuffer.cpp
index e390523..43124cc 100644
--- a/src/gpu/graphite/dawn/DawnBuffer.cpp
+++ b/src/gpu/graphite/dawn/DawnBuffer.cpp
@@ -319,4 +319,17 @@
     }
 }
 
+const wgpu::BindGroup* DawnBuffer::getCachedSingleBufferBindGroup(size_t bindingSize) const {
+    for (auto& cachedGroup : fCachedSingleBufferBindGroups) {
+        if (cachedGroup.first == bindingSize) {
+            return &cachedGroup.second;
+        }
+    }
+    return nullptr;
+}
+void DawnBuffer::addCachedSingleBufferBindGroup(wgpu::BindGroup bindGroup,
+                                                size_t bindingSize) const {
+    fCachedSingleBufferBindGroups.push_back({bindingSize, bindGroup});
+}
+
 } // namespace skgpu::graphite
diff --git a/src/gpu/graphite/dawn/DawnBuffer.h b/src/gpu/graphite/dawn/DawnBuffer.h
index 6ce2e31..be97eab 100644
--- a/src/gpu/graphite/dawn/DawnBuffer.h
+++ b/src/gpu/graphite/dawn/DawnBuffer.h
@@ -32,6 +32,9 @@
 
     const wgpu::Buffer& dawnBuffer() const { return fBuffer; }
 
+    const wgpu::BindGroup* getCachedSingleBufferBindGroup(size_t bindingSize) const;
+    void addCachedSingleBufferBindGroup(wgpu::BindGroup, size_t bindingSize) const;
+
 private:
     DawnBuffer(const DawnSharedContext*,
                size_t,
@@ -60,6 +63,10 @@
 
     // Ensure that only one thread can access fAsyncMapCallbacks.
     [[maybe_unused]] SingleOwner fSingleAsyncMapCallbacksOwner;
+
+    // By the time the command buffer requests a bind group, the provided Buffer pointer is const
+    // so this attribute must be mutable to avoid a const_cast.
+    mutable skia_private::TArray<std::pair<size_t, wgpu::BindGroup>> fCachedSingleBufferBindGroups;
 };
 
 } // namespace skgpu::graphite
diff --git a/src/gpu/graphite/dawn/DawnCommandBuffer.cpp b/src/gpu/graphite/dawn/DawnCommandBuffer.cpp
index 02618e3..4198b68 100644
--- a/src/gpu/graphite/dawn/DawnCommandBuffer.cpp
+++ b/src/gpu/graphite/dawn/DawnCommandBuffer.cpp
@@ -951,43 +951,66 @@
 }
 
 void DawnCommandBuffer::syncUniformBuffers() {
-    static constexpr int kNumBuffers = DawnGraphicsPipeline::kNumUniformBuffers;
+    if (!fBoundUniformBuffersDirty) {
+        return;
+    }
+    fBoundUniformBuffersDirty = false;
 
-    if (fBoundUniformBuffersDirty) {
-        fBoundUniformBuffersDirty = false;
+    bool usePushConstants = fSharedContext->dawnCaps()->
+            resourceBindingRequirements().fUsePushConstantsForIntrinsicConstants;
 
-        std::array<uint32_t, kNumBuffers> dynamicOffsets;
-        std::array<std::pair<const DawnBuffer*, uint32_t>, kNumBuffers> boundBuffersAndSizes;
+    // We expect to have up to 3 uniforms in this bind group.
+    static constexpr int kMaxUniformsInGroup = 3;
+    // Until/unless uniform bind group structure gets reorganized, this should be equivalent to the
+    // size of our bound uniform array.
+    SkASSERT(kMaxUniformsInGroup == fBoundUniforms.size());
 
-        std::array<bool, kNumBuffers> enabled = {
-                !fSharedContext->dawnCaps()
-                         ->resourceBindingRequirements()
-                         .fUsePushConstantsForIntrinsicConstants,  // intrinsic uniforms
-                fActiveGraphicsPipeline->hasCombinedUniforms(),    // paint AND renderstep uniforms!
-                fActiveGraphicsPipeline->hasGradientBuffer(),      // gradient SSBO
+    wgpu::BindGroup bindGroup;
+    std::array<uint32_t, kMaxUniformsInGroup> dynamicOffsets {0};
+    // Check if we can use an optimized route for single-uniform buffer bind groups:
+    if (usePushConstants &&
+        !fActiveGraphicsPipeline->hasGradientBuffer() &&
+        fActiveGraphicsPipeline->hasCombinedUniforms()) {
+        const BindBufferInfo& bufferInfo =
+                fBoundUniforms[DawnGraphicsPipeline::kCombinedUniformIndex];
+        bindGroup = fResourceProvider->findOrCreateSingleUniformBindGroup(bufferInfo);
+        dynamicOffsets[DawnGraphicsPipeline::kCombinedUniformIndex] = bufferInfo.fOffset;
+    } else {
+        std::array<bool, kMaxUniformsInGroup> enabled = {
+                !usePushConstants,                              // intrinsic uniforms
+                fActiveGraphicsPipeline->hasCombinedUniforms(), // paint AND renderstep uniforms!
+                fActiveGraphicsPipeline->hasGradientBuffer(),   // gradient SSBO
+        };
+        constexpr uint32_t kBindingIndices[] = {
+            DawnGraphicsPipeline::kIntrinsicUniformBufferIndex,
+            DawnGraphicsPipeline::kCombinedUniformIndex,
+            DawnGraphicsPipeline::kGradientBufferIndex,
         };
 
-        for (int i = 0; i < kNumBuffers; ++i) {
+        std::array<wgpu::BindGroupEntry, kMaxUniformsInGroup> bindGroupEntries {};
+        for (int i = 0; i < kMaxUniformsInGroup; ++i) {
+            bindGroupEntries[i].binding = kBindingIndices[i];
             if (enabled[i] && fBoundUniforms[i]) {
-                boundBuffersAndSizes[i].first =
-                        static_cast<const DawnBuffer*>(fBoundUniforms[i].fBuffer);
-                boundBuffersAndSizes[i].second = fBoundUniforms[i].fSize;
+                bindGroupEntries[i].size = fBoundUniforms[i].fSize;
+                bindGroupEntries[i].buffer =
+                        static_cast<const DawnBuffer*>(fBoundUniforms[i].fBuffer)->dawnBuffer();
                 dynamicOffsets[i] = fBoundUniforms[i].fOffset;
             } else {
                 // Unused or null binding
-                boundBuffersAndSizes[i].first = nullptr;
-                dynamicOffsets[i] = 0;
+                bindGroupEntries[i].buffer = fResourceProvider->getOrCreateNullBuffer();
             }
         }
 
-        auto bindGroup =
-                fResourceProvider->findOrCreateUniformBuffersBindGroup(boundBuffersAndSizes);
-
-        fActiveRenderPassEncoder.SetBindGroup(DawnGraphicsPipeline::kUniformBufferBindGroupIndex,
-                                              bindGroup,
-                                              dynamicOffsets.size(),
-                                              dynamicOffsets.data());
+        const auto& groupLayouts = fActiveGraphicsPipeline->dawnGroupLayouts();
+        bindGroup = fResourceProvider->createBindGroup(
+                bindGroupEntries,
+                groupLayouts[DawnGraphicsPipeline::kUniformBufferBindGroupIndex]);
     }
+
+    fActiveRenderPassEncoder.SetBindGroup(DawnGraphicsPipeline::kUniformBufferBindGroupIndex,
+                                          bindGroup,
+                                          dynamicOffsets.size(),
+                                          dynamicOffsets.data());
 }
 
 void DawnCommandBuffer::setScissor(const Scissor& scissor) {
diff --git a/src/gpu/graphite/dawn/DawnCommandBuffer.h b/src/gpu/graphite/dawn/DawnCommandBuffer.h
index f288881..989d66e 100644
--- a/src/gpu/graphite/dawn/DawnCommandBuffer.h
+++ b/src/gpu/graphite/dawn/DawnCommandBuffer.h
@@ -161,7 +161,7 @@
 
     bool fBoundUniformBuffersDirty = false;
 
-    std::array<BindBufferInfo, DawnGraphicsPipeline::kNumUniformBuffers> fBoundUniforms;
+    std::array<BindBufferInfo, DawnGraphicsPipeline::kMaxNumUniformBuffers> fBoundUniforms;
 
     wgpu::CommandEncoder fCommandEncoder;
     wgpu::RenderPassEncoder fActiveRenderPassEncoder;
diff --git a/src/gpu/graphite/dawn/DawnGraphicsPipeline.h b/src/gpu/graphite/dawn/DawnGraphicsPipeline.h
index 6761334..45b4dce 100644
--- a/src/gpu/graphite/dawn/DawnGraphicsPipeline.h
+++ b/src/gpu/graphite/dawn/DawnGraphicsPipeline.h
@@ -44,10 +44,14 @@
     inline static constexpr unsigned int kTextureBindGroupIndex = 1;
     inline static constexpr unsigned int kBindGroupCount = 2;
 
+    // TODO(b/512814646): WASM does not support push constant usage, meaning that we often have 2
+    // uniform buffers within one BindGroup. This is unideal since we can store single-uniform
+    // BindGroups on DawnBuffers. Consider reorganizing uniform buffers such that we can more often
+    // only have one uniform buffer per BindGroup.
     inline static constexpr unsigned int kIntrinsicUniformBufferIndex = 0;
     inline static constexpr unsigned int kCombinedUniformIndex = 1;
     inline static constexpr unsigned int kGradientBufferIndex = 2;
-    inline static constexpr unsigned int kNumUniformBuffers = 3;
+    inline static constexpr unsigned int kMaxNumUniformBuffers = 3;
 
     inline static constexpr unsigned int kIntrinsicUniformSize = 32;
 
diff --git a/src/gpu/graphite/dawn/DawnResourceProvider.cpp b/src/gpu/graphite/dawn/DawnResourceProvider.cpp
index 6c6fcd6..6c777ea 100644
--- a/src/gpu/graphite/dawn/DawnResourceProvider.cpp
+++ b/src/gpu/graphite/dawn/DawnResourceProvider.cpp
@@ -31,7 +31,6 @@
 namespace {
 
 constexpr uint32_t kBufferBindingSizeAlignment = 16;
Loading diff…

Original Bug Report

reported by [email protected]

Potential Stale BindGroup UAF in Graphite Dawn via 32-bit ID Wraparound

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 without the security team.

Overview: Skia Graphite uses a 32-bit atomic counter to assign IDs to resources, which serve as keys for Dawn bind group caches. An attacker can force this counter to wrap around, causing a cache collision that retrieves a stale bind group pointing to a destroyed texture view. Because Chromium disables Dawn validation in release builds, this causes a potential Use-After-Free at the GPU driver level.

Affected files:

  • third_party/skia/src/gpu/graphite/Resource.cpp
  • third_party/skia/src/gpu/graphite/dawn/DawnResourceProvider.cpp
  • third_party/skia/src/gpu/graphite/dawn/DawnTexture.cpp
  • third_party/skia/src/gpu/graphite/dawn/DawnBuffer.cpp

Estimated timestamp from git blame: 2024-08-05

Vulnerability Details

In Skia Graphite, skgpu::graphite::Resource assigns a uniqueID to each resource upon construction using a global 32-bit atomic counter (std::atomic<uint32_t> nextID). The Dawn backend relies on these IDs to cache wgpu::BindGroup objects. For example, DawnResourceProvider::findOrCreateSingleTextureSamplerBindGroup derives its cache key directly from the uniqueID of the provided sampler and texture.

When a DawnTexture is destroyed, its underlying wgpu::Texture and associated views (e.g., VkImageView in the Vulkan backend) are explicitly destroyed. However, the wgpu::BindGroup referencing those destroyed views remains alive in Skia’s fSingleTextureSamplerBindGroups cache. This cache is typically only cleared during OS memory pressure or when the GPU has been entirely idle for >1 second.

If the 32-bit uniqueID counter wraps around, a newly allocated texture can receive an ID identical to a previously destroyed texture. If sampled with the same sampler, Skia’s cache lookup will succeed, and the stale wgpu::BindGroup will be retrieved and bound to the command buffer.

Crucially, Chromium’s integration of Skia Graphite defaults to skipping Dawn’s frontend validation in Release builds (kSkiaGraphiteDawnSkipValidation is true). Consequently, Dawn’s QueueBase::SubmitInternal bypasses the checks that would normally detect a destroyed resource. The command buffer is passed directly to the backend (e.g., Vulkan), where the driver will dereference the freed VkImageView handle, resulting in a Use-After-Free.

Potential Steps to Trigger

Note: These are suggested steps to theoretically trigger the vulnerability; a fully working proof-of-concept has not yet been executed by our tooling.

  1. Initialize & Cache: Using JavaScript (Canvas2D or WebGL), an attacker allocates a texture and a sampler, then performs a draw call to force Skia to generate and cache a wgpu::BindGroup.
  2. Destroy Texture: The attacker drops all references to the texture in JavaScript. Once Skia’s pending commands complete, the underlying Dawn texture and Vulkan handles are destroyed, but the bind group remains in Skia’s cache.
  3. Spin the Counter: The attacker executes a tight JavaScript loop to rapidly allocate ~4.29 billion resources, forcing the 32-bit counter to wrap around.
    • To do this without causing an Out-Of-Memory (OOM) crash, the attacker can repeatedly call putImageData() with varying sizes slightly above 64KB. This routes to UploadBufferManager, which allocates non-shareable DawnBuffer resources.
    • Varying the sizes ensures cache misses (creating new uniqueIDs), while Skia’s internal ResourceCache automatically purges older buffers to stay under budget.
    • Occasional small draw calls are interleaved to prevent the GPU from entering the 1-second idle state, avoiding a cache reset.
  4. Trigger Collision: Once the counter wraps to the original texture’s ID, the attacker allocates a new target texture. It receives the colliding uniqueID.
  5. Execute UAF: The attacker issues a draw call using the original sampler and the new texture. Skia fetches the stale wgpu::BindGroup and submits it. Due to validation being disabled, the GPU driver executes the command using the freed handle, potentially allowing the attacker to read cross-origin GPU memory if they successfully reallocated the freed view descriptor over sensitive data.

Suggested Fix

  1. Widen the Counter: The most robust fix is to upgrade skgpu::graphite::Resource::nextID and the UniqueID class to use a 64-bit integer (uint64_t). This makes wraparound practically impossible during the lifetime of an application.
  2. Explicit Invalidation: Alternatively, DawnResourceProvider should be notified when a DawnTexture or DawnBuffer is destroyed so it can proactively remove any dependent wgpu::BindGroups from fSingleTextureSamplerBindGroups and fUniformBufferBindGroupCache.

Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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