Overview

Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Skia
DescriptionUse after free in Skia
ComponentSkia
Sub-componentGraphite
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker520514458
Fix commit6a4be3addd0a (skia) +14/-8
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-05 (per vendor advisory)
Fix landed2026-06-12 (public git)
Disclosed2026-07-29
Patch gap47 days — fix public in source before the advisory

Background

`skgpu::graphite`
Skia’s next-generation GPU backend that records draw commands and translates them into native GPU API calls, here targeting Dawn.
Dawn
Chrome’s WebGPU implementation; wgpu::CreateBindGroupLayout consumes descriptor structs whose optional extensions are supplied through the nextInChain pointer field.
`wgpu::StaticSamplerBindingLayout`
an immutable/static-sampler extension struct chained off a wgpu::BindGroupLayoutEntry via nextInChain, so its storage must outlive the CreateBindGroupLayout call.
`SkTBlockList`
a Skia container that appends elements into fixed blocks, guaranteeing each element a stable address across growth, unlike skia_private::TArray/std::vector which reallocate.

Root Cause Analysis

In DawnGraphicsPipeline::Make, Graphite collected wgpu::StaticSamplerBindingLayout structs for immutable samplers into a growable skia_private::TArray and stored a pointer to each appended element into the corresponding wgpu::BindGroupLayoutEntry::nextInChain. Because a TArray reallocates its backing storage when it grows past capacity, appending a second (or later) static sampler moved the previously appended elements, invalidating the earlier nextInChain pointers so they dangled into freed storage. When Dawn later dereferenced those chained pointers during CreateBindGroupLayout, it performed a use-after-free read in the GPU process. The condition only manifested when more than one static sampler was present and the append forced a reallocation, which is why the subject notes it “likely never happened” in practice with the usual 0-1 immutable samplers.

The fix replaces the container with SkTBlockList<wgpu::StaticSamplerBindingLayout, 1>, whose elements retain stable addresses across growth, keeping every chained pointer valid until Dawn reads it.

Key insight
The bug is a lifetime/address-stability mismatch: a container chosen for its convenience (TArray) reallocates on growth, but its elements’ addresses were treated as stable because they had been handed out as raw nextInChain pointers that must survive until a later API consumption point.

Attack Path

  1. Web content drives a WebGPU/Graphite workload whose pipeline requires two or more immutable/static samplers in a single bind group layout.
  2. Graphite appends each wgpu::StaticSamplerBindingLayout to the growable TArray and writes the element’s current address into a BindGroupLayoutEntry::nextInChain.
  3. A subsequent append exceeds the TArray capacity, triggering reallocation that relocates and frees the earlier elements, leaving prior nextInChain pointers dangling.
  4. Graphite calls wgpu::CreateBindGroupLayout, and Dawn walks the nextInChain chain, reading from the freed storage — a use-after-free in the GPU process.
  5. With controlled reallocation timing and heap grooming, the freed slot could be reoccupied to influence what Dawn reads, escalating the read toward exploitable behavior.

Impact Assessment

  • Use-after-free read in the GPU process reachable from WebGPU-exercising web content, potentially leaking or corrupting adjacent heap data consumed by Dawn.
  • Practical reachability is low because it requires multiple static samplers plus a capacity-forcing reallocation, but the sandboxed GPU process is a valuable target and controlled heap reuse could raise severity.

Files Changed

  • src/gpu/graphite/dawn/DawnGraphicsPipeline.cpp

Audit Directions

  • Address-stability of chained pointers
    audit all nextInChain and other raw-pointer-into-container patterns where an element’s address is stored and consumed later; ensure the backing container does not reallocate (prefer SkTBlockList, node-based, or reserved-capacity storage).
  • Deferred-consumption lifetimes across API boundaries
    flag any struct whose pointer is passed into Dawn/WebGPU descriptors and read at a later Create* call, verifying the pointed-to storage outlives that call and is not moved by intervening appends.
  • Container migration regressions
    when refactoring std::vector/TArray usage, check whether any caller relied on element-address stability before assuming a growable container is a safe substitute.
From 6a4be3addd0a6e271e68a6a1bb873877e24813e9 Mon Sep 17 00:00:00 2001
From: Michael Ludwig <michaelludwig@google.com>
Date: Fri, 12 Jun 2026 11:24:39 -0400
Subject: [PATCH] [graphite] Use stable collection for static bindings

Since the layouts are passed by pointer in the `nextInChain` field,
their addresses need to stay valid until the BindGroupLayout is created.
With vector, if it ever grew, that would not remain the case. Since
there are usually only 0 to 1 immutable samplers, this likely never
happened (and also why it uses a built-in storage for 1).

Also removes the include for vector and uses TArray (we had been mixing
both throughout the file).

Bug: 520514458
Fixed: 520514458
Change-Id: I44c9566c68ab0e6c6d659ea77a423ddc50d53c76
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1264157
Reviewed-by: Thomas Smith <thomsmit@google.com>
Auto-Submit: Michael Ludwig <michaelludwig@google.com>
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
---

diff --git a/src/gpu/graphite/dawn/DawnGraphicsPipeline.cpp b/src/gpu/graphite/dawn/DawnGraphicsPipeline.cpp
index d5f6f61..2d771ef 100644
--- a/src/gpu/graphite/dawn/DawnGraphicsPipeline.cpp
+++ b/src/gpu/graphite/dawn/DawnGraphicsPipeline.cpp
@@ -10,7 +10,8 @@
 #include "include/gpu/graphite/TextureInfo.h"
 #include "include/gpu/graphite/dawn/DawnGraphiteTypes.h"
 #include "include/private/SkLog.h"
-#include "include/private/SkTemplates.h"
+#include "include/private/SkTArray.h"
+#include "src/base/SkTBlockList.h"
 #include "src/core/SkTraceEvent.h"
 #include "src/gpu/SkSLToBackend.h"
 #include "src/gpu/Swizzle.h"
@@ -34,7 +35,8 @@
 #include "src/sksl/ir/SkSLProgram.h"
 
 #include <atomic>
-#include <vector>
+
+using namespace skia_private;
 
 namespace skgpu::graphite {
 
@@ -152,7 +154,7 @@
 
 size_t create_vertex_attributes(SkSpan<const Attribute> attrs,
                                 int shaderLocationOffset,
-                                std::vector<wgpu::VertexAttribute>* out) {
+                                TArray<wgpu::VertexAttribute>* out) {
     SkASSERT(out && out->empty());
     out->resize(attrs.size());
     size_t vertexAttributeOffset = 0;
@@ -499,12 +501,16 @@
                 !(samplerDescArrPtr && samplerDescArrPtr->at(0).isImmutable())) {
                 groupLayouts[1] = sharedContext->getSingleTextureSamplerBindGroupLayout();
             } else {
-                std::vector<wgpu::BindGroupLayoutEntry> entries(numTexturesAndSamplers);
+                TArray<wgpu::BindGroupLayoutEntry> entries;
+                entries.reset(numTexturesAndSamplers);
+
 #if !defined(__EMSCRIPTEN__)
                 // Static sampler layouts are passed into Dawn by address and therefore must stay
                 // alive until the BindGroupLayoutDescriptor is created. So, store them outside of
-                // the loop that iterates over each BindGroupLayoutEntry.
-                skia_private::TArray<wgpu::StaticSamplerBindingLayout> staticSamplerLayouts;
+                // the loop that iterates over each BindGroupLayoutEntry. Use a TBlockList so that
+                // any append StaticSamplerBindingLayouts have a stable address in case we have to
+                // grow the collection.
+                SkTBlockList<wgpu::StaticSamplerBindingLayout, 1> staticSamplerLayouts;
 
                 // Note that the number of samplers is equivalent to numTexturesAndSamplers / 2. So,
                 // a sampler's index within any container that only pertains to sampler information
@@ -598,7 +604,7 @@
     // Vertex state
     std::array<wgpu::VertexBufferLayout, kNumVertexBuffers> vertexBufferLayouts;
     // Static data buffer layout
-    std::vector<wgpu::VertexAttribute> staticDataAttributes;
+    TArray<wgpu::VertexAttribute> staticDataAttributes;
     {
         auto arrayStride = create_vertex_attributes(step->staticAttributes(),
                                                     0,
@@ -622,7 +628,7 @@
     }
 
     // Append data buffer layout
-    std::vector<wgpu::VertexAttribute> appendDataAttributes;
+    TArray<wgpu::VertexAttribute> appendDataAttributes;
     {
         // Note: the shaderLocationOffset in this function call needs to be the staticAttributeSize
         auto arrayStride = create_vertex_attributes(step->appendAttributes(),
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Heap Use-After-Free in DawnGraphicsPipeline due to staticSamplerLayouts reallocation

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: A potential heap use-after-free vulnerability exists in Skia’s Graphite Dawn backend (DawnGraphicsPipeline.cpp) due to the reallocation of a TArray holding static sampler layouts while pointers to its elements are still held in a descriptor chain. When the layout is subsequently processed during bind group layout creation, the deallocated memory is traversed, potentially leading to a GPU process crash or code execution.

Affected files:

  • third_party/skia/src/gpu/graphite/dawn/DawnGraphicsPipeline.cpp

Estimated timestamp from git blame: 2024-10-03

Description

A potential heap use-after-free (UAF) vulnerability has been identified in DawnGraphicsPipeline::Make() within Skia’s Graphite Dawn backend (third_party/skia/src/gpu/graphite/dawn/DawnGraphicsPipeline.cpp).

The issue stems from the lifecycle management of skia_private::TArray<wgpu::StaticSamplerBindingLayout> staticSamplerLayouts during the creation of a bind group layout.

Detailed Analysis

In DawnGraphicsPipeline::Make(), a local vector is declared to hold static sampler layouts:

skia_private::TArray<wgpu::StaticSamplerBindingLayout> staticSamplerLayouts;

As the loop iterates over textures and samplers, if a sampler is immutable, a layout is added to the array and its address is saved into the stack-allocated entries array:

wgpu::StaticSamplerBindingLayout& immutableSamplerBinding =
        staticSamplerLayouts.emplace_back();
...
entries[i].nextInChain = &immutableSamplerBinding;

Because staticSamplerLayouts is default-constructed with a capacity of 0 and is not pre-reserved, appending elements can trigger a vector reallocation. When TArray::emplace_back() exhausts its capacity, it reallocates the underlying buffer via growAndConstructAtEnd() -> installDataAndUpdateCapacity(), move-constructs elements to the new buffer, and frees the old buffer via sk_free().

If a reallocation occurs, any pointers already stored in entries[*].nextInChain pointing to the previous array storage become dangling.

Later, Dawn processes these entries inside device.CreateBindGroupLayout(&groupLayoutDesc). In release builds, DeviceBase::CreateBindGroupLayout calls ConvertAndExpandBGLEntries(), which invokes the generated Unpack<BindGroupLayoutEntry>() function. This function traverses the linked list starting at the dangling nextInChain pointer.

If an attacker is able to reclaim and populate the freed heap memory before the unpack traversal, they could control the structure’s sType and sampler pointer. When StaticSamplerBindingInfo::From() is called, it constructs a Ref<SamplerBase>{layout.sampler}, which increments and decrements the reference count at an attacker-controlled offset and potentially triggers a virtual method call on destruction, leading to arbitrary code execution within the GPU process.

Triggering Preconditions

On 64-bit systems, the first allocation of staticSamplerLayouts is rounded up to a minimum capacity of 8 elements (256 bytes, fitting PartitionAlloc’s 256-byte bucket). Thus, a pipeline containing 9 or more immutable (Vulkan YCbCr) samplers is required to trigger a reallocation. Under normal operation, immutable samplers are typically produced only for textures backed by a YCbCr descriptor (such as Android AHardwareBuffer SharedImages).

Potential Steps to Reproduce (Theoretical)

Note: These are potential steps based on source code analysis; our tools do not currently have the capability to run code or verify this dynamically.

  1. From a compromised renderer, serialize OOP-R paint commands that reference 9 or more distinct, unique AHardwareBuffer-backed YCbCr images in a single paint shader/filter tree.
  2. Ensure the resulting pipeline description is unique to prevent hitting the pipeline cache.
  3. Once the raster command is executed, the GPU process calls DawnGraphicsPipeline::Make(). On the 9th immutable sampler, staticSamplerLayouts reallocates, leaving dangling pointers in entries[0..7].nextInChain.
  4. Concurrently, spray the PartitionAlloc 256-byte bucket to reclaim the freed block and overwrite its contents with forged layout descriptions.
  5. When device.CreateBindGroupLayout() parses the entries, it traverses the forged chained layouts, performing refcount increments and virtual calls.

Suggested Fix

To resolve this issue, reserve sufficient capacity in staticSamplerLayouts prior to entering the loop. Since the maximum possible number of static samplers is bounded by numTexturesAndSamplers / 2, pre-allocating or reserving this capacity prevents any subsequent reallocation:

skia_private::TArray<wgpu::StaticSamplerBindingLayout> staticSamplerLayouts;
staticSamplerLayouts.reserve(numTexturesAndSamplers / 2);

Evaluated with Chrome root at commit: e9507a33bb4148ee071aaaf8a7e9ad68770359bf


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